import { expect, test } from "@playwright/test"; import path from "node:path"; // HTTP/client behavior only. Main is stubbed, so these are not native parity evidence. for (const mode of ["success", "queued", "failed", "cancelled", "bad-hash", "missing-hash", "bad-binding", "unknown-status", "stale-main", "timeout", "cancel", "http-error"]) { test(`SERVER_NATIVE async client: ${mode}`, async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async ({ mode, protocolUrl }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const { canonicalServerOperatorJobRequest, canonicalServerOperatorProperties } = await import(protocolUrl); const hash = async (bytes: ArrayBuffer) => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (n) => n.toString(16).padStart(2, "0")).join(""); const encode = (value: string) => new TextEncoder().encode(value).buffer; const source = encode("transport-source"); const output = encode("transport-result"); const request: any = { schemaVersion: 1, route: "SERVER_NATIVE", jobId: crypto.randomUUID(), sourceBlendSha256: await hash(source), sourceBlendByteLength: source.byteLength, sourceRevision: 1, blenderBuild: { version: "test", buildSha256: "a".repeat(64) }, operator: "object.test", properties: {}, propertiesSha256: await hash(encode(canonicalServerOperatorProperties({}))), timeoutMs: 1000, memoryBytes: 64 * 1024 * 1024, resultBinding: { kind: "snapshot", key: "test" }, context: "BACKGROUND" }; request.requestSha256 = await hash(encode(canonicalServerOperatorJobRequest(request))); const final: any = { ...request, status: "SUCCEEDED", resultSha256: "a".repeat(64), resultBlendSha256: await hash(output), resultBlendByteLength: output.byteLength, resultBlendBase64: btoa("transport-result") }; if (mode === "failed") final.status = "FAILED"; if (mode === "cancelled") final.status = "CANCELLED"; if (mode === "bad-hash") final.resultBlendSha256 = "0".repeat(64); if (mode === "missing-hash") delete final.resultBlendSha256; if (mode === "bad-binding") final.requestSha256 = "0".repeat(64); if (mode === "unknown-status") final.status = "UNKNOWN"; const originalFetch = window.fetch; let reads = 0, cancels = 0, opened = 0, sourceSaved = 0; window.fetch = async (_url, init) => { const action = new Headers(init?.headers).get("X-Server-Operator-Action"); if (action === "cancel") cancels++; if (action === "read") { reads++; if (mode === "timeout" || mode === "cancel") { return new Promise((_resolve, reject) => { if (init?.signal?.aborted) reject(init.signal.reason); else init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }); }); } if (mode === "http-error") return new Response("unavailable", { status: 503 }); } return Response.json({ status: { ready: true }, result: action === "submit" ? { ...request, status: mode === "queued" ? "QUEUED" : "RUNNING" } : action === "cancel" ? { ...request, status: "CANCELLED" } : final }, { status: action === "submit" ? 202 : 200 }); }; const client = new WebEngineClient({ timeoutMs: 5000 }); client.saveBlend = async () => { sourceSaved++; return source.slice(0); }; client.snapshot = async () => ({ snapshot: { revision: mode === "stale-main" ? 2 : 1 } } as any); client.applyServerBlendResult = async () => { opened++; return {} as any; }; const controller = new AbortController(); const started = performance.now(); const cancellation = mode === "cancel" ? setTimeout(() => controller.abort(new Error("user cancelled")), 400) : undefined; try { const job = await client.submitServerOperatorJob(request, controller.signal); return { status: job.job.status, reads, cancels, opened, sourceSaved, elapsed: performance.now() - started, error: null }; } catch (error: any) { return { status: null, reads, cancels, opened, sourceSaved, elapsed: performance.now() - started, error: error.message ?? String(error) }; } finally { clearTimeout(cancellation); window.fetch = originalFetch; client.terminate(); } }, { mode, protocolUrl: `/@fs${path.resolve(import.meta.dirname, "../../protocol/server-operator-job.ts")}` }); expect(result.reads).toBeGreaterThan(0); expect(result.sourceSaved).toBe(1); if (mode === "success" || mode === "queued") { expect(result.status).toBe("SUCCEEDED"); expect(result.opened).toBe(1); expect(result.cancels).toBe(0); } else { expect(result.error).toBeTruthy(); expect(result.opened).toBe(0); expect(result.cancels).toBeGreaterThan(0); } if (mode === "timeout") expect(result.elapsed).toBeLessThan(4500); if (mode === "cancel") expect(result.elapsed).toBeLessThan(1500); }); }