import { afterEach, describe, expect, it, vi } from "vitest"; import { PlatformApiClient } from "./client"; import type { GameClientBridgeManifestResponse, GameClientBridgeQueueRequest, GamePluginResponse, MarketplacePluginResponse } from "./types"; const now = "2026-07-20T08:00:00Z"; const later = "2026-07-20T09:00:00Z"; const status = { serverInstanceId: "server-1", pluginId: "game.scum", available: false, reason: "compatible companion is offline", profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: false, reason: "component heartbeat is unavailable", commandTypes: ["scum.announcement.send"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }] } as const; const pendingCommand = { id: "command-1", serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client", commandType: "scum.announcement.send", priority: 20, state: "pending", approvalState: "pending", requesterId: "user-1", auditReferences: ["audit-command-1"], expiresAt: later, createdAt: now, updatedAt: now } as const; const completedCommand = { ...pendingCommand, state: "succeeded", approvalState: "approved", resultSummary: "announcement delivered", result: { status: "succeeded", summary: "announcement delivered", payload: { delivered: true, recipientCount: 12 }, completedAt: later }, completedAt: later, updatedAt: later } as const; const cancellation = { commandId: pendingCommand.id, state: "cancelled", cancellation: { requestedBy: "user-1", reason: "maintenance window changed", cancelledAt: later }, auditReferences: ["audit-command-1", "audit-command-cancel-1"], updatedAt: later } as const; const snapshot = { id: "snapshot-1", serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client", type: "scum.players", schemaVersion: "1", streamKey: "current", sequence: 7, observedAt: now, payload: { players: [{ playerId: "player-1", displayName: "Moonlight" }] }, retention: { keepForSeconds: 3600, maxRecords: 24 }, auditReferences: ["audit-snapshot-1"], createdAt: now, expiresAt: later } as const; const manifestDeclaration: GameClientBridgeManifestResponse = { commands: [{ type: "scum.announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/commands/announcement.request.json", resultSchemaRef: "schemas/bridge/commands/announcement.result.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }], snapshots: [{ type: "scum.players", schemaVersion: "1", schemaRef: "schemas/bridge/snapshots/players.json", keepForSeconds: 3600, maxRecords: 24 }], queryTemplates: [{ key: "scum.player.search", title: "Search players", permission: "server.game-client.read", engine: "sqlite", transportKey: "scum-database", targetKey: "scum-db", parameterSchemaRef: "schemas/bridge/queries/player-search.request.json", resultSchemaRef: "schemas/bridge/queries/player-search.result.json", maxRows: 50, timeoutSeconds: 10 }], commandRetentionSeconds: 86400, maxCommands: 1000, pages: [{ pageKey: "operations", commandTypes: ["scum.announcement.send"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }], companion: { profileKey: "scum-client-manager", configTemplateKey: "client-config", configSchemaRef: "schemas/companion/config.schema.json", configFormat: "yaml", platformBaseUrlSource: "run-control", registrationProof: "hmac-sha256", proofMaterialSource: "component-package", proofMaterialEnv: "SCUM_COMPONENT_PROOF", sessionMode: "component-session", tlsPolicy: "verify-system-roots", heartbeatIntervalSeconds: 30, commandPollIntervalSeconds: 5, requestTimeoutSeconds: 15 } }; const pluginBridgeProjection: Pick & Pick = { gameClientBridge: manifestDeclaration }; describe("PlatformApiClient Game Client Bridge operator API", () => { afterEach(() => vi.unstubAllGlobals()); it("types plugin and marketplace manifest declarations with approval metadata", () => { expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ approvalLevel: "operator" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } }); expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i); }); it("uses only server-scoped operator routes and preserves bounded filters and bodies", async () => { const calls: Array<{ url: string; method: string; body?: unknown }> = []; vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); const method = init?.method ?? "GET"; calls.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined }); if (url.endsWith("/game-client-bridge")) return jsonResponse(status); if (url.includes("/game-client-bridge/commands?") && method === "GET") return jsonResponse({ items: [pendingCommand], count: 1 }); if (url.endsWith("/game-client-bridge/commands") && method === "POST") return jsonResponse(pendingCommand, 202); if (url.endsWith(`/game-client-bridge/commands/${pendingCommand.id}`) && method === "GET") return jsonResponse(completedCommand); if (url.endsWith(`/game-client-bridge/commands/${pendingCommand.id}/cancel`) && method === "POST") return jsonResponse(cancellation); if (url.includes("/game-client-bridge/snapshots?")) return jsonResponse({ items: [snapshot], count: 1 }); throw new Error(`unexpected request: ${method} ${url}`); })); const client = new PlatformApiClient("/api/v1", () => "operator-session"); const queueRequest: GameClientBridgeQueueRequest = { profileKey: "scum-client", commandType: "scum.announcement.send", payload: { message: "Restart in ten minutes", channels: ["global"] }, idempotencyKey: "announcement-1", priority: 20, expiresAt: later }; await expect(client.getGameClientBridgeStatus("server-1")).resolves.toMatchObject({ available: false, profiles: [{ profileKey: "scum-client" }] }); await expect(client.listGameClientBridgeCommands("server-1", { profileKey: "scum-client", state: "pending", commandType: "scum.announcement.send" })).resolves.toMatchObject({ count: 1 }); await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending", approvalState: "pending" }); await expect(client.getGameClientBridgeCommand("server-1", pendingCommand.id)).resolves.toMatchObject({ result: { status: "succeeded", payload: { delivered: true } } }); await expect(client.cancelGameClientBridgeCommand("server-1", pendingCommand.id, { reason: "maintenance window changed" })).resolves.toMatchObject({ state: "cancelled" }); await expect(client.listGameClientBridgeSnapshots("server-1", { profileKey: "scum-client", type: "scum.players", streamKey: "current", observedAfter: now, limit: 20 })).resolves.toMatchObject({ count: 1, items: [{ sequence: 7 }] }); expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([ "GET /api/v1/server-instances/server-1/game-client-bridge", "GET /api/v1/server-instances/server-1/game-client-bridge/commands?profileKey=scum-client&state=pending&commandType=scum.announcement.send", "POST /api/v1/server-instances/server-1/game-client-bridge/commands", "GET /api/v1/server-instances/server-1/game-client-bridge/commands/command-1", "POST /api/v1/server-instances/server-1/game-client-bridge/commands/command-1/cancel", "GET /api/v1/server-instances/server-1/game-client-bridge/snapshots?profileKey=scum-client&type=scum.players&streamKey=current&observedAfter=2026-07-20T08%3A00%3A00Z&limit=20" ]); expect(calls[2]?.body).toEqual(queueRequest); expect(calls[4]?.body).toEqual({ reason: "maintenance window changed" }); expect(JSON.stringify(calls)).not.toMatch(/sessionToken|componentKey|secretRef|hostPath|dsn|socket|credential|runEndpoint/i); expect(calls.every((call) => !call.url.includes("/companion/"))).toBe(true); }); it("URL-encodes server and command identifiers", async () => { const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => jsonResponse(completedCommand)); vi.stubGlobal("fetch", fetchMock); await new PlatformApiClient("/api/v1").getGameClientBridgeCommand("server/unsafe", "command/unsafe"); expect(String(fetchMock.mock.calls[0]?.[0])).toBe("/api/v1/server-instances/server%2Funsafe/game-client-bridge/commands/command%2Funsafe"); }); }); function jsonResponse(value: unknown, statusCode = 200): Response { return new Response(JSON.stringify(value), { status: statusCode, headers: { "Content-Type": "application/json" } }); }