import { afterEach, describe, expect, it, vi } from "vitest"; import { PlatformApiClient, setPlatformApiSessionToken } from "./client"; import type { AiProviderResponse, GamePluginResponse, JobResponse, MarketplacePluginResponse, RunEndpointResponse, ServerInstanceResponse } from "./types"; const provider: AiProviderResponse = { id: "ai.openai", name: "OpenAI", kind: "openai", baseUrl: "https://api.openai.com/v1", apiKeyRef: "secret://providers/openai", models: ["gpt-4.1"], defaultModel: "gpt-4.1", relayMode: "direct", timeoutMs: 30000, status: "active", redactionPolicy: "default" }; const plugin: GamePluginResponse = { id: "game.example", name: "Example Server", version: "0.1.0", serverType: "example", serverDisplayName: "Example Server", manifestRef: "artifact://manifests/game.example/0.1.0", createFormSchemaRef: "schemas/create-form.schema.json", requiredRunCapabilities: ["process.start", "logs.read"], declaredPermissions: ["server.read", "server.logs.read", "ai.invoke"], permissions: { ai: true, logs: true, files: false, jobs: false, artifacts: false }, lifecycleActions: { start: "actions/start.json" }, bridgeActions: ["server.instances.read", "logs.query", "ai.invoke"], pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }], tags: ["example"], aiPurposes: ["logs.diagnose"], status: "installed" }; const marketplacePlugin: MarketplacePluginResponse = { id: "game.example", name: "Example Server", description: "Development plugin", version: "0.1.0", serverType: "example", serverDisplayName: "Example Server", supportedOs: ["linux", "darwin"], manifestRef: "artifact://manifests/game.example/0.1.0", createFormSchemaRef: "schemas/create-form.schema.json", capabilities: ["process.install", "process.start", "logs.read"], declaredPermissions: ["server.read", "server.logs.read", "ai.invoke"], permissions: { ai: true, logs: true, files: false, jobs: false, artifacts: false }, lifecycleActions: { install: "actions/install.json", start: "actions/start.json", stop: "actions/stop.json" }, bridgeActions: ["server.instances.read", "logs.query", "ai.invoke"], pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }], tags: ["example"], aiPurposes: ["logs.diagnose"], status: "installed", source: "platform-registry" }; const server: ServerInstanceResponse = { id: "server-1", pluginId: "game.example", pluginVersion: "0.1.0", runEndpointId: "run-local", name: "Example Survival #1", ownerUserId: "user-owner", adminUserIds: ["user-admin-1"], state: "running", configVersion: 1, createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" }; const endpoint: RunEndpointResponse = { id: "run-local", displayName: "Local Run", version: "0.1.0", status: "online", capabilities: ["process.install", "process.start", "process.stop"], capacity: { maxJobs: 4, runningJobs: 0, queuedJobs: 1 }, lastHeartbeatAt: "2026-07-03T00:00:00Z" }; const job: JobResponse = { id: "job-1", serverInstanceId: server.id, runEndpointId: endpoint.id, capability: "process.start", idempotencyKey: "idem-start", state: "queued", progress: { percent: 0, message: "queued" }, createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" }; const artifact = { id: "artifact-1", ownerKind: "job", ownerId: job.id, sizeBytes: 18, checksum: "sha256:artifactchecksum", state: "available", createdAt: "2026-07-03T00:00:00Z", updatedAt: "2026-07-03T00:00:00Z" }; describe("PlatformApiClient AI providers", () => { afterEach(() => { setPlatformApiSessionToken(null); vi.restoreAllMocks(); }); it("calls AI provider management endpoints with named contracts", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.endsWith("/api/v1/ai-providers") && (!init?.method || init.method === "GET")) { return jsonResponse({ items: [provider], count: 1 }); } if (url.endsWith("/api/v1/ai-providers") && init?.method === "POST") { return jsonResponse(provider); } if (url.endsWith("/api/v1/ai-providers/ai.openai") && init?.method === "PUT") { return jsonResponse({ ...provider, name: "OpenAI Relay" }); } if (url.endsWith("/api/v1/ai-providers/ai.openai/status") && init?.method === "POST") { return jsonResponse({ ...provider, status: "disabled" }); } if (url.endsWith("/api/v1/ai-providers/ai.openai/test") && init?.method === "POST") { return jsonResponse({ providerId: provider.id, mode: "metadata", success: true, message: "metadata validation passed" }); } if (url.endsWith("/api/v1/ai-providers/ai.openai/models")) { return jsonResponse({ providerId: provider.id, defaultModel: provider.defaultModel, models: provider.models }); } throw new Error(`unexpected request: ${url}`); }); vi.stubGlobal("fetch", fetchMock); const client = new PlatformApiClient(); await expect(client.listAiProviders()).resolves.toMatchObject({ count: 1 }); await expect(client.createAiProvider(provider)).resolves.toMatchObject({ id: provider.id }); await expect(client.updateAiProvider(provider.id, { ...provider, name: "OpenAI Relay" })).resolves.toMatchObject({ name: "OpenAI Relay" }); await expect(client.setAiProviderStatus(provider.id, { status: "disabled" })).resolves.toMatchObject({ status: "disabled" }); await expect(client.testAiProvider(provider.id)).resolves.toMatchObject({ success: true, mode: "metadata" }); await expect(client.listAiProviderModels(provider.id)).resolves.toMatchObject({ models: ["gpt-4.1"] }); expect(fetchMock).toHaveBeenCalledTimes(6); }); it("calls console shell resource endpoints with named contracts", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.endsWith("/healthz")) { return jsonResponse({ service: "platform", status: "ok", version: "0.1.0", time: "2026-07-03T00:00:00Z" }); } if (url.endsWith("/api/v1/game-plugins")) { return jsonResponse({ items: [plugin], count: 1 }); } if (url.endsWith("/api/v1/server-instances") && (!init?.method || init.method === "GET")) { return jsonResponse({ items: [server], count: 1 }); } if (url.endsWith("/api/v1/metrics/platform")) { return jsonResponse({ cpuPercent: 28, memoryPercent: 42, diskPercent: 19, source: "platform-derived", collectedAt: "2026-07-03T00:00:00Z" }); } if (url.endsWith("/api/v1/metrics/server-instances")) { return jsonResponse({ items: [ { serverInstanceId: server.id, online: true, playerCount: 5, maxPlayers: 20, tps: 19.8, latencyMs: 42, cpuPercent: 31, memoryPercent: 44, diskPercent: 22, source: "platform-derived", collectedAt: "2026-07-03T00:00:00Z" } ], count: 1 }); } if (url.endsWith("/api/v1/server-instances/server-1/config")) { return jsonResponse({ serverInstanceId: server.id, configVersion: 1, format: "properties", key: "server.properties", content: "server.name=Example Survival #1\n", source: "platform-derived", updatedAt: "2026-07-03T00:00:00Z" }); } if (url.endsWith("/api/v1/server-instances/server-1/config/diff") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n" }); return jsonResponse({ serverInstanceId: server.id, configVersion: 1, key: "server.properties", currentContent: "server.name=Example Survival #1\n", proposedContent: "server.name=Example Survival #2\n", diff: [ { kind: "removed", oldNumber: 1, content: "server.name=Example Survival #1" }, { kind: "added", newNumber: 1, content: "server.name=Example Survival #2" } ], hasChanges: true, source: "platform-review", reviewedAt: "2026-07-03T00:00:00Z" }); } if (url.endsWith("/api/v1/server-instances/server-1/config/approve") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n", idempotencyKey: "idem-config" }); return jsonResponse({ status: "queued", preview: { serverInstanceId: server.id, configVersion: 1, key: "server.properties", currentContent: "server.name=Example Survival #1\n", proposedContent: "server.name=Example Survival #2\n", diff: [{ kind: "added", newNumber: 1, content: "server.name=Example Survival #2" }], hasChanges: true, source: "platform-review", reviewedAt: "2026-07-03T00:00:00Z" }, job: { ...job, id: "job-config-write", capability: "config.write", targetKey: "server.properties", inputRef: "input://server-config/server-1/server.properties/v1" } }); } if (url.endsWith("/api/v1/file-operations/dispatch") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ serverInstanceId: server.id, operation: "read", key: "logs/latest.log", idempotencyKey: "idem-file" }); return jsonResponse({ status: "queued", serverInstanceId: server.id, operation: "read", key: "logs/latest.log", job: { ...job, id: "job-file-read", capability: "files.read", targetKey: "logs/latest.log" } }); } if (url.endsWith("/api/v1/run/endpoints")) { return jsonResponse({ items: [endpoint], count: 1 }); } if (url.endsWith("/api/v1/jobs")) { return jsonResponse({ items: [job], count: 1 }); } if (url.endsWith("/api/v1/jobs?serverInstanceId=server-1")) { return jsonResponse({ items: [job], count: 1 }); } if (url.endsWith("/api/v1/artifacts?ownerKind=job&ownerId=job-1&state=available")) { return jsonResponse({ items: [artifact], count: 1 }); } if (url.endsWith("/api/v1/artifacts/artifact-1/download") && init?.method === "POST") { return jsonResponse({ artifactId: artifact.id, ownerKind: artifact.ownerKind, ownerId: artifact.ownerId, filename: "artifact-1.bin", contentType: "application/octet-stream", sizeBytes: artifact.sizeBytes, checksum: artifact.checksum, state: artifact.state, downloadUrl: "/api/v1/artifacts/artifact-1/content", expiresAt: "2026-07-03T00:15:00Z", rangeSupported: true, chunkSizeBytes: 1048576, storageBehavior: "platform-memory-transfer-session" }); } if (url.endsWith("/api/v1/artifacts/artifact-1/content?offset=0&limit=8")) { return new Response(new TextEncoder().encode("artifact").buffer, { status: 206, headers: { "Content-Type": "application/octet-stream", "Content-Length": "8", "Content-Range": "bytes 0-7/18", "X-Artifact-Id": artifact.id, "X-Artifact-Checksum": artifact.checksum, "X-Artifact-Content-Checksum": "sha256:chunkchecksum", "X-Artifact-Storage": "platform-memory-transfer-session" } }); } if (url.endsWith("/api/v1/server-instances/workflows/create") && init?.method === "POST") { return jsonResponse({ accepted: true, action: "create", instance: { ...server, state: "installing" }, job: { ...job, capability: "process.install" } }); } if (url.endsWith("/api/v1/server-instances/server-1/start") && init?.method === "POST") { return jsonResponse({ accepted: true, action: "start", instance: server, job }); } if (url.endsWith("/api/v1/server-instances/server-1/stop") && init?.method === "POST") { return jsonResponse({ accepted: true, action: "stop", instance: server, job: { ...job, capability: "process.stop" } }); } if (url.endsWith("/api/v1/server-instances/server-1/administrators/candidates") && (!init?.method || init.method === "GET")) { return jsonResponse({ items: [{ id: "user-2", displayName: "Helper", status: "active", roles: ["server-admin"] }], count: 1 }); } if (url.endsWith("/api/v1/server-instances/server-1/administrators") && init?.method === "POST") { return jsonResponse({ ...server, adminUserIds: [...server.adminUserIds, "user-2"] }); } if (url.endsWith("/api/v1/server-instances/server-1/administrators/user-2") && init?.method === "DELETE") { return jsonResponse({ ...server, adminUserIds: [] }); } if (url.endsWith("/api/v1/plugin-bridge/authorize") && init?.method === "POST") { return jsonResponse({ pluginId: plugin.id, routeKey: "logs", action: "logs.query", allowed: true, requiredPermissions: ["server.logs.read"], effectivePermissions: ["server.logs.read"] }); } if (url.endsWith("/api/v1/plugin-bridge/execute") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ requestId: "req-bridge", pluginId: plugin.id, routeKey: "logs", serverInstanceId: server.id, action: "logs.query", payload: { logStreamId: "log-1" } }); return jsonResponse({ requestId: "req-bridge", pluginId: plugin.id, routeKey: "logs", serverInstanceId: server.id, action: "logs.query", status: "ok", result: { entryCount: "0" } }); } if (url.endsWith("/api/v1/ai/invocations") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" }); return jsonResponse({ requestId: "ai-1", purpose: "config.suggest", providerId: "ai.openai", model: "gpt-4.1", status: "ok", recommendation: "Review before applying.", configRecommendation: { key: "server.properties", suggestedConfig: "server.name=Example Survival #1\npvp=false\n", diffSummary: "review required" }, usage: { providerId: "ai.openai", model: "gpt-4.1", inputTokens: 20, outputTokens: 12, mocked: true } }); } throw new Error(`unexpected request: ${url}`); }); vi.stubGlobal("fetch", fetchMock); const client = new PlatformApiClient(); await expect(client.health()).resolves.toMatchObject({ status: "ok" }); await expect(client.listGamePlugins()).resolves.toMatchObject({ count: 1 }); await expect(client.listServerInstances()).resolves.toMatchObject({ count: 1 }); await expect(client.getPlatformResourceUsage()).resolves.toMatchObject({ source: "platform-derived", cpuPercent: 28 }); await expect(client.listServerMetrics()).resolves.toMatchObject({ count: 1, items: [{ serverInstanceId: server.id, online: true }] }); await expect(client.getServerConfig(server.id)).resolves.toMatchObject({ content: "server.name=Example Survival #1\n" }); await expect( client.previewServerConfigDiff(server.id, { expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n" }) ).resolves.toMatchObject({ hasChanges: true, source: "platform-review" }); await expect( client.approveServerConfigWrite(server.id, { expectedConfigVersion: 1, key: "server.properties", proposedContent: "server.name=Example Survival #2\n", idempotencyKey: "idem-config" }) ).resolves.toMatchObject({ status: "queued", job: { capability: "config.write", targetKey: "server.properties" } }); await expect(client.dispatchFileOperation({ serverInstanceId: server.id, operation: "read", key: "logs/latest.log", idempotencyKey: "idem-file" })).resolves.toMatchObject({ status: "queued", job: { capability: "files.read", targetKey: "logs/latest.log" } }); await expect(client.listRunEndpoints()).resolves.toMatchObject({ count: 1 }); await expect(client.listJobs()).resolves.toMatchObject({ count: 1 }); await expect(client.listJobs(server.id)).resolves.toMatchObject({ count: 1 }); await expect(client.listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" })).resolves.toMatchObject({ count: 1, items: [{ id: artifact.id }] }); await expect(client.openArtifactDownload(artifact.id)).resolves.toMatchObject({ downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true }); await expect(client.readArtifactContent(artifact.id, 0, 8)).resolves.toMatchObject({ contentLength: 8, contentRange: "bytes 0-7/18", checksum: artifact.checksum }); await expect(client.createServerWorkflow({ id: "server-2", pluginId: plugin.id, runEndpointId: endpoint.id, name: "Server 2", idempotencyKey: "idem-create" })).resolves.toMatchObject({ action: "create" }); await expect(client.startServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-start" })).resolves.toMatchObject({ action: "start" }); await expect(client.stopServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-stop" })).resolves.toMatchObject({ action: "stop" }); await expect(client.listServerAdministratorCandidates(server.id)).resolves.toMatchObject({ count: 1 }); await expect(client.addServerAdministrator(server.id, { userId: "user-2" })).resolves.toMatchObject({ adminUserIds: ["user-admin-1", "user-2"] }); await expect(client.removeServerAdministrator(server.id, "user-2")).resolves.toMatchObject({ adminUserIds: [] }); await expect(client.authorizePluginBridge({ pluginId: plugin.id, routeKey: "logs", action: "logs.query" })).resolves.toMatchObject({ allowed: true }); await expect( client.executePluginBridge({ requestId: "req-bridge", pluginId: plugin.id, routeKey: "logs", serverInstanceId: server.id, action: "logs.query", payload: { logStreamId: "log-1" } }) ).resolves.toMatchObject({ status: "ok", result: { entryCount: "0" } }); await expect( client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" }) ).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } }); expect(fetchMock).toHaveBeenCalledTimes(24); }); it("calls plugin marketplace endpoints with filter and state contracts", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.endsWith("/api/v1/plugin-marketplace/plugins?status=installed&serverType=example&capability=logs.read&keyword=example")) { return jsonResponse({ items: [marketplacePlugin], count: 1 }); } if (url.endsWith("/api/v1/plugin-marketplace/plugins/game.example") && (!init?.method || init.method === "GET")) { return jsonResponse(marketplacePlugin); } if (url.endsWith("/api/v1/plugin-marketplace/plugins/game.example/state") && init?.method === "POST") { expect(JSON.parse(String(init.body))).toEqual({ action: "disable" }); return jsonResponse({ ...marketplacePlugin, status: "disabled" }); } throw new Error(`unexpected request: ${url}`); }); vi.stubGlobal("fetch", fetchMock); const client = new PlatformApiClient(); await expect(client.listMarketplacePlugins({ status: "installed", serverType: "example", capability: "logs.read", keyword: "example" })).resolves.toMatchObject({ count: 1 }); await expect(client.getMarketplacePlugin(marketplacePlugin.id)).resolves.toMatchObject({ source: "platform-registry" }); await expect(client.setMarketplacePluginState(marketplacePlugin.id, { action: "disable" })).resolves.toMatchObject({ status: "disabled" }); expect(fetchMock).toHaveBeenCalledTimes(3); }); it("surfaces config diff preview failures from the platform", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.endsWith("/api/v1/server-instances/server-1/config/diff") && init?.method === "POST") { return new Response(JSON.stringify({ code: "validation", message: "expectedConfigVersion must match server instance" }), { status: 400, headers: { "Content-Type": "application/json" } }); } throw new Error(`unexpected request: ${url}`); }); vi.stubGlobal("fetch", fetchMock); const client = new PlatformApiClient(); await expect(client.previewServerConfigDiff(server.id, { expectedConfigVersion: 0, key: "server.properties", proposedContent: "changed=true\n" })).rejects.toThrow( "expectedConfigVersion must match server instance" ); }); it("keeps raw key fields out of provider responses", () => { expect("apiKey" in provider).toBe(false); expect("rawApiKey" in provider).toBe(false); expect(provider.apiKeyRef).toBe("secret://providers/openai"); }); it("calls auth endpoints and attaches bearer sessions", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.endsWith("/api/v1/auth/login") && init?.method === "POST") { return jsonResponse({ user: { id: "user-admin", displayName: "Operator", status: "active", roles: ["platform-admin"] }, sessionId: "session-token", status: "authenticated", message: "登录成功" }); } if (url.endsWith("/api/v1/users/current")) { expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer session-token"); return jsonResponse({ id: "user-admin", displayName: "Operator", status: "active", roles: ["platform-admin"] }); } if (url.endsWith("/api/v1/auth/logout") && init?.method === "POST") { expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer session-token"); return new Response(null, { status: 204 }); } throw new Error(`unexpected request: ${url}`); }); vi.stubGlobal("fetch", fetchMock); const client = new PlatformApiClient(); const login = await client.login({ account: "operator.local@example.test", password: "operator-local" }); expect(login.sessionId).toBe("session-token"); setPlatformApiSessionToken(login.sessionId ?? null); await expect(client.getCurrentUser()).resolves.toMatchObject({ id: "user-admin" }); await expect(client.logout()).resolves.toBeUndefined(); expect(fetchMock).toHaveBeenCalledTimes(3); }); }); function jsonResponse(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }); }