import { describe, expect, it, vi } from "vitest"; import { createValidModelDocument } from "../canonical/model-document-test-support"; import { digestToolcraftModelDocument } from "../canonical/model-document-digest"; import { TOOLCRAFT_MODEL_IMPORT_LIMIT_CEILINGS } from "../model-import-limits"; import type { ToolcraftModelDecodeResult, ToolcraftModelFormatAdapter, } from "../model-import-types"; import type { ToolcraftModelWorkerRequest, ToolcraftModelWorkerResponse, } from "./model-import-worker-protocol"; import { createToolcraftModelWorkerRuntime } from "./model-import-worker-runtime"; import { decodeRequest, fakeRepairPlan, limits, repairRequest, } from "./model-import-worker-runtime-test-support"; describe("createToolcraftModelWorkerRuntime lifecycle and limits", () => { it("publishes one structurally valid draft between decode and analysis", async () => { const document = createValidModelDocument(); const posted: Array<{ response: ToolcraftModelWorkerResponse; transfer: readonly Transferable[]; }> = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode: vi.fn(async () => ({ appearanceResources: [], diagnostics: [], document, })), format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response, transfer = []) => posted.push({ response, transfer }), }); await runtime.handleMessage(decodeRequest(1)); const lifecycle = posted.map(({ response }) => response.kind === "progress" ? `${response.kind}:${response.phase}` : response.kind ); expect(lifecycle.filter((kind) => kind === "draft")).toHaveLength(1); expect(lifecycle.indexOf("draft")).toBeGreaterThan( lifecycle.lastIndexOf("progress:decoding"), ); expect(lifecycle.indexOf("draft")).toBeLessThan( lifecycle.indexOf("progress:analyzing"), ); expect(lifecycle.at(-1)).toBe("result"); const draft = posted.find(({ response }) => response.kind === "draft"); expect(draft?.response).toMatchObject({ draft: { canonicalDocumentDigest: digestToolcraftModelDocument(document) }, kind: "draft", }); expect(draft?.transfer).toHaveLength(1); if (draft?.response.kind !== "draft") throw new Error("Expected model draft."); expect(draft.transfer[0]).toBe(draft.response.draft.canonicalDocument); }); it("evicts bounded decode entries and a fresh runtime starts with no generation-local cache", async () => { const document = createValidModelDocument(); const decode = vi.fn(async () => ({ appearanceResources: [], diagnostics: [], document, })); const adapter: ToolcraftModelFormatAdapter = { adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }; const createRuntime = () => createToolcraftModelWorkerRuntime({ adapters: [adapter], cacheLimits: { maxAggregateBytes: 1024 * 1024, maxEntriesPerStage: 1 }, post: vi.fn(), }); const firstRuntime = createRuntime(); const firstBase = decodeRequest(1, "realtime-mesh", 1); const secondBase = decodeRequest(2, "realtime-mesh", 2); if (firstBase.kind !== "decode-and-analyze" || secondBase.kind !== "decode-and-analyze") { throw new Error("Expected decode requests."); } const first: ToolcraftModelWorkerRequest = { ...firstBase, payload: { ...firstBase.payload, bundle: { ...firstBase.payload.bundle, aggregateDigest: "sha256:first" }, }, }; const second: ToolcraftModelWorkerRequest = { ...secondBase, payload: { ...secondBase.payload, bundle: { ...secondBase.payload.bundle, aggregateDigest: "sha256:second" }, }, }; await firstRuntime.handleMessage(first); await firstRuntime.handleMessage(second); await firstRuntime.handleMessage(decodeRequest(3, "realtime-mesh", 3)); expect(firstRuntime.inspectCacheSizes().decode).toBe(1); firstRuntime.dispose(); const secondRuntime = createRuntime(); await secondRuntime.handleMessage(decodeRequest(4, "realtime-mesh", 3)); expect(decode).toHaveBeenCalledTimes(4); }); it("enforces transferred-source preflight before invoking the decoder", async () => { const decode = vi.fn(async () => ({ appearanceResources: [], diagnostics: [], document: createValidModelDocument(), })); const responses: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response) => responses.push(response), }); const baseRequest = decodeRequest(1); if (baseRequest.kind !== "decode-and-analyze") throw new Error("Expected decode request."); const request: ToolcraftModelWorkerRequest = { ...baseRequest, payload: { ...baseRequest.payload, limits: { ...limits, maxSourceBytes: 2 }, }, }; await runtime.handleMessage(request); expect(decode).not.toHaveBeenCalled(); expect(responses.at(-1)).toMatchObject({ kind: "error", feedback: { category: "resource-limit", code: "source-byte-limit-exceeded" }, }); }); it.each([ ["widened", { ...limits, maxSourceBytes: TOOLCRAFT_MODEL_IMPORT_LIMIT_CEILINGS.maxSourceBytes + 1, }], ["NaN", { ...limits, maxSourceBytes: Number.NaN }], ["unknown", { ...limits, hostileLimit: 1 }], ] as const)( "rejects %s worker-ingress limits before adapter invocation", async (_label, hostileLimits) => { const decode = vi.fn(async () => ({ appearanceResources: [], diagnostics: [], document: createValidModelDocument(), })); const responses: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response) => responses.push(response), }); const base = decodeRequest(1); if (base.kind !== "decode-and-analyze") throw new Error("Expected decode request."); const request = { ...base, payload: { ...base.payload, limits: hostileLimits }, } as unknown as ToolcraftModelWorkerRequest; await runtime.handleMessage(request); expect(decode).not.toHaveBeenCalled(); expect(responses.at(-1)).toMatchObject({ kind: "error", feedback: { category: "resource-limit", code: "invalid-model-import-limits" }, }); }, ); it("rejects worker limits that omit a protected package field", async () => { const decode = vi.fn(async () => ({ appearanceResources: [], diagnostics: [], document: createValidModelDocument(), })); const responses: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response) => responses.push(response), }); const base = decodeRequest(1); if (base.kind !== "decode-and-analyze") { throw new Error("Expected decode request."); } const hostileLimits = { ...limits } as Record; delete hostileLimits.maxArchiveEntries; const request = { ...base, payload: { ...base.payload, limits: hostileLimits }, } as unknown as ToolcraftModelWorkerRequest; await runtime.handleMessage(request); expect(decode).not.toHaveBeenCalled(); expect(responses.at(-1)).toMatchObject({ feedback: { category: "resource-limit", code: "invalid-model-import-limits", }, kind: "error", }); }); it("rejects invalid repair limits before repair invocation", async () => { const repair = vi.fn(() => createValidModelDocument()); const responses: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [], post: (response) => responses.push(response), repair, }); const base = repairRequest(1, fakeRepairPlan()); if (base.kind !== "repair") throw new Error("Expected repair request."); const request = { ...base, payload: { ...base.payload, limits: { ...limits, maxDecodedBytes: Number.NaN }, }, } as unknown as ToolcraftModelWorkerRequest; await runtime.handleMessage(request); expect(repair).not.toHaveBeenCalled(); expect(responses.at(-1)).toMatchObject({ kind: "error", feedback: { category: "resource-limit", code: "invalid-model-import-limits" }, }); }); it.each([ ["infinite", { maxAggregateBytes: Infinity, maxEntriesPerStage: 1 }], ["NaN", { maxAggregateBytes: Number.NaN, maxEntriesPerStage: 1 }], ["zero", { maxAggregateBytes: 0, maxEntriesPerStage: 1 }], ["negative", { maxAggregateBytes: -1, maxEntriesPerStage: 1 }], ["oversized", { maxAggregateBytes: Number.MAX_SAFE_INTEGER, maxEntriesPerStage: 1, }], ] as const)("rejects %s runtime cache limits", (_label, cacheLimits) => { expect(() => createToolcraftModelWorkerRuntime({ adapters: [], cacheLimits, post: vi.fn(), })).toThrow(/worker cache limits/u); }); it("maps errors to bounded typed feedback, supports retry, and never surfaces cancellation as feedback", async () => { const document = createValidModelDocument(); const decode = vi.fn() .mockRejectedValueOnce(new Error("x".repeat(10_000))) .mockResolvedValueOnce({ appearanceResources: [], diagnostics: [], document }); const posted: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response) => posted.push(response), }); await runtime.handleMessage(decodeRequest(1)); await runtime.handleMessage(decodeRequest(2)); const error = posted.find((response) => response.kind === "error"); expect(error).toMatchObject({ kind: "error", feedback: { category: "format", code: "model-decode-failed" }, }); if (error?.kind !== "error") throw new Error("Expected typed error."); expect(error.feedback.message.length).toBeLessThanOrEqual(240); expect(posted.at(-1)?.kind).toBe("result"); }); it("cooperatively suppresses a late async result after cancellation", async () => { let resolveDecode!: (value: ToolcraftModelDecodeResult) => void; const decode = vi.fn(() => new Promise((resolve) => { resolveDecode = resolve; })); const posted: ToolcraftModelWorkerResponse[] = []; const runtime = createToolcraftModelWorkerRuntime({ adapters: [{ adapterVersion: "test-adapter@1", decode, format: "glb", rootExtensions: [".glb"], workerCapable: true, }], post: (response) => posted.push(response), }); const pending = runtime.handleMessage(decodeRequest(1)); await Promise.resolve(); await runtime.handleMessage({ generation: 1, jobId: "decode-1", kind: "cancel" }); resolveDecode({ appearanceResources: [], diagnostics: [], document: createValidModelDocument(), }); await pending; expect(posted.filter(({ kind }) => kind === "cancelled")).toHaveLength(1); expect(posted.some(({ kind }) => kind === "error")).toBe(false); expect(posted.some(({ kind }) => kind === "result")).toBe(false); }); });