/** @vitest-environment jsdom */ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { renderToStaticMarkup } from "react-dom/server"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import hostSource from "./PluginPageHostPage.tsx?raw"; import type { GamePluginResponse } from "../api/types"; import type { PageComponentProps } from "../contracts/page"; import type { PluginPageWorkspaceActions } from "../contracts/pluginPageHost"; import { capabilitiesForRoles } from "../contracts/workspace"; import type { OperationTracker } from "../stores/operations"; import { PluginPageHostPage } from "./PluginPageHostPage"; const apiMocks = vi.hoisted(() => ({ deletePluginData: vi.fn(), executePluginBridge: vi.fn(), getGameClientBridgeCommand: vi.fn(), getGameClientBridgeStatus: vi.fn(), listGameClientBridgeCommands: vi.fn(), listGameClientBridgeSnapshots: vi.fn(), listGamePlugins: vi.fn(), listPluginData: vi.fn(), putPluginData: vi.fn(), queueGameClientBridgeCommand: vi.fn(), transactPluginData: vi.fn() })); const bundleMocks = vi.hoisted(() => ({ loadPluginPageBundle: vi.fn() })); vi.mock("../api/client", () => ({ platformApiClient: apiMocks })); vi.mock("../utils/pluginPageBundles", async (importOriginal) => ({ ...await importOriginal(), loadPluginPageBundle: bundleMocks.loadPluginPageBundle })); const operations: OperationTracker = { operations: [], begin: () => "operation-test", update: () => undefined, succeed: () => undefined, fail: () => undefined, isPending: () => false }; const plugin: GamePluginResponse = { id: "game.scum", name: "SCUM Server", version: "1.0.0", serverType: "scum", serverDisplayName: "SCUM Server", manifestRef: "artifact://manifests/game.scum/1.0.0", createFormSchemaRef: "schemas/create-form.schema.json", requiredRunCapabilities: [], declaredPermissions: ["server.read", "server.game-client.read", "server.game-client.command"], permissions: { ai: true, logs: false, files: false, jobs: true, artifacts: false, remoteAccess: false }, lifecycleActions: {}, bridgeActions: ["server.instances.read"], pages: [{ key: "players", title: "用户管理", path: "/players", bundleKey: "scum-server-plugin", bundleVersion: "1.0.1", bundleIntegritySha256: "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e", permissions: ["server.read", "server.game-client.read", "server.game-client.command"], bridgeActions: ["server.instances.read"] }], tags: ["scum"], aiPurposes: [], productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] }, gameClientBridge: { commands: [{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }], snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }], commandRetentionSeconds: 86400, maxCommands: 1000, pages: [{ pageKey: "players", commandTypes: ["announcement.send"], snapshotTypes: ["companion.health"] }] }, status: "installed" }; let root: Root | null = null; let container: HTMLDivElement | null = null; let capturedWorkspaceActions: PluginPageWorkspaceActions | undefined; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; beforeEach(() => { capturedWorkspaceActions = undefined; apiMocks.getGameClientBridgeStatus.mockResolvedValue({ available: true, features: [] }); apiMocks.listPluginData.mockResolvedValue({ items: [], count: 0 }); apiMocks.putPluginData.mockResolvedValue({ key: "user-1", value: { name: "Ada" } }); apiMocks.deletePluginData.mockResolvedValue(undefined); apiMocks.transactPluginData.mockResolvedValue({ items: [], count: 0 }); apiMocks.queueGameClientBridgeCommand.mockResolvedValue({ id: "command-1" }); apiMocks.getGameClientBridgeCommand.mockResolvedValue({ id: "command-1" }); apiMocks.listGameClientBridgeCommands.mockResolvedValue({ items: [], count: 0 }); apiMocks.listGameClientBridgeSnapshots.mockResolvedValue({ items: [], count: 0 }); apiMocks.executePluginBridge.mockResolvedValue({ requestId: "request-1", action: "server.instances.read", status: "ok", result: { state: "ready" } }); bundleMocks.loadPluginPageBundle.mockResolvedValue(({ workspaceActions }: { workspaceActions?: PluginPageWorkspaceActions }) => { capturedWorkspaceActions = workspaceActions; return
mock plugin bundle
; }); }); afterEach(async () => { if (root) await act(async () => root?.unmount()); container?.remove(); root = null; container = null; vi.clearAllMocks(); }); function props(serverId = "server-1"): PageComponentProps { const session = { id: "operator-1", displayName: "Operator", status: "active" as const, roles: ["platformAdmin" as const], capabilities: capabilitiesForRoles(["platformAdmin"]), profile: {}, source: "local" as const }; return { session, params: { pluginId: "game.scum", routeKey: "players", serverId }, operations, onNavigate: () => undefined, onLogout: async () => undefined, onProfileSave: async () => session, onThemePreferenceSave: async () => ({ userId: session.id, paletteId: "mecha-black", backgroundPresetId: "mecha-grid", persistence: "api", updatedAt: "2026-07-20T00:00:00Z" }) }; } describe("PluginPageHostPage", () => { it("renders a generic manifest-owned bundle declaration", () => { const html = renderToStaticMarkup(); expect(html).toContain("用户管理"); expect(html).toContain("平台托管上下文"); expect(html).toContain("scum-server-plugin@1.0.1"); expect(html).toContain("完整性"); expect(html).toContain("返回服务器"); expect(html).not.toMatch(/sessionToken|componentKey|hostPath|dsn|runSocket|credential/i); }); it("does not mount a bundle without client-side availability validation", () => { const html = renderToStaticMarkup(); expect(html).toContain("未绑定服务器"); expect(html).toContain("正在校验并加载插件页面 bundle"); }); it("remains a manifest-driven host without SCUM component imports or game branches", () => { expect(hostSource).toContain("loadPluginPageBundle"); expect(hostSource).not.toMatch(/ScumFileConfigWorkbench|GamePlayerIntelligencePanel|GameGiftCatalogPanel|ScumMapTrajectoryPanel|game\.scum/); }); it("keeps generic plugin data callbacks stable across parent operational refreshes", () => { expect(hostSource).toContain("readyPluginRef.current = readyPlugin"); expect(hostSource).toContain("hostContextRef.current = hostContext"); expect(hostSource).toContain("list: (collection, key) => platformApiClient.listPluginData(serverId, collection, key)"); expect(hostSource).toContain("put: (collection, key, value) => platformApiClient.putPluginData(serverId, collection, key, value)"); expect(hostSource).toContain("delete: (collection, key) => platformApiClient.deletePluginData(serverId, collection, key)"); expect(hostSource).toContain("transact: (collection, mutations) => platformApiClient.transactPluginData(serverId, collection, mutations)"); expect(hostSource).toContain("queue: (request) => platformApiClient.queueGameClientBridgeCommand(serverId, request)"); expect(hostSource).toContain("snapshots: (query) => platformApiClient.listGameClientBridgeSnapshots(serverId, query)"); expect(hostSource).toContain("createPluginBridgeDispatcher(context, platformApiClient)"); expect(hostSource).not.toContain("listSCUMPlayers:"); expect(hostSource).not.toContain("refreshWorkspace"); expect(hostSource).not.toContain("requestFile"); expect(hostSource).not.toContain("writeFile"); expect(hostSource).not.toContain("getDeclaredFileReadSnapshot"); expect(hostSource).toContain("}, [pluginId, serverId]);"); expect(hostSource).not.toContain("}, [hostContext, readyPlugin, serverId]);"); }); it("passes every generic data, dispatch, and game-client operation through the hosted bundle", async () => { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); await act(async () => { root?.render(); }); const actions = capturedWorkspaceActions; expect(actions?.pluginData).toBeDefined(); expect(actions?.gameClient).toBeDefined(); expect(actions?.dispatch).toBeDefined(); const mutations = [ { operation: "put" as const, key: "user-1", value: { name: "Ada" } }, { operation: "delete" as const, key: "user-2" } ]; const queueRequest = { profileKey: "scum-client", commandType: "reward.deliver", payload: { steamId: "redacted", giftKey: "starter" }, idempotencyKey: "gift:user-1:starter", expiresAt: "2026-08-15T12:00:00Z" }; const commandFilter = { profileKey: "scum-client", state: "pending" as const, commandType: "reward.deliver" }; const snapshotQuery = { profileKey: "scum-client", type: "scum.positions", streamKey: "current", limit: 50 }; const dispatchEnvelope = { requestId: "request-1", action: "server.instances.read" as const, payload: { view: "summary" } }; await actions?.pluginData?.list("scum_users", "user-1"); await actions?.pluginData?.put("scum_users", "user-1", { name: "Ada" }); await actions?.pluginData?.delete("scum_users", "user-2"); await actions?.pluginData?.transact("scum_users", mutations); await actions?.gameClient?.queue(queueRequest); await actions?.gameClient?.get("command-1"); await actions?.gameClient?.list(commandFilter); await actions?.gameClient?.snapshots(snapshotQuery); await expect(actions?.dispatch?.(dispatchEnvelope)).resolves.toMatchObject({ status: "ok", result: { state: "ready" } }); expect(apiMocks.listPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1"); expect(apiMocks.putPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1", { name: "Ada" }); expect(apiMocks.deletePluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-2"); expect(apiMocks.transactPluginData).toHaveBeenCalledWith("server-1", "scum_users", mutations); expect(apiMocks.queueGameClientBridgeCommand).toHaveBeenCalledWith("server-1", queueRequest); expect(apiMocks.getGameClientBridgeCommand).toHaveBeenCalledWith("server-1", "command-1"); expect(apiMocks.listGameClientBridgeCommands).toHaveBeenCalledWith("server-1", commandFilter); expect(apiMocks.listGameClientBridgeSnapshots).toHaveBeenCalledWith("server-1", snapshotQuery); expect(apiMocks.executePluginBridge).toHaveBeenCalledWith({ requestId: "request-1", pluginId: "game.scum", routeKey: "players", serverInstanceId: "server-1", action: "server.instances.read", aiPurpose: undefined, payload: { view: "summary" } }); }); });