import { vi } from "vitest"; import { encodeToolcraftModelDocument } from "../canonical/model-document-codec"; import { digestToolcraftModelDocument } from "../canonical/model-document-digest"; import { createValidModelDocument } from "../canonical/model-document-test-support"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import { digestModelSourceBytes } from "../model-source-digest"; import { analyzeToolcraftModel } from "../topology/model-topology-analysis"; import { snapshotToolcraftModelWorkerAnalysisSummary } from "./model-import-worker-diagnostics"; import type { ToolcraftModelWorkerRequest, ToolcraftModelWorkerResponse, } from "./model-import-worker-protocol"; import { createToolcraftModelWorkerDraftReceiptDigest, createToolcraftModelWorkerReceiptDigest, } from "./model-import-worker-receipt"; import type { ToolcraftModelWorkerLike } from "./model-import-worker-client"; type WorkerEventType = "error" | "message" | "messageerror"; type WorkerListener = EventListenerOrEventListenerObject; export class FakeWorker implements ToolcraftModelWorkerLike { readonly listeners = new Map>(); readonly removedListeners: WorkerListener[] = []; readonly posts: Array<{ message: ToolcraftModelWorkerRequest; transfer: Transferable[] }> = []; readonly terminate = vi.fn(); addEventListener(type: WorkerEventType, listener: WorkerListener): void { const listeners = this.listeners.get(type) ?? new Set(); listeners.add(listener); this.listeners.set(type, listeners); } removeEventListener(type: WorkerEventType, listener: WorkerListener): void { this.listeners.get(type)?.delete(listener); this.removedListeners.push(listener); } postMessage(message: ToolcraftModelWorkerRequest, transfer: Transferable[] = []): void { this.posts.push({ message, transfer }); } emitMessage(response: unknown): void { this.emit("message", new MessageEvent("message", { data: response })); } emitRemovedMessage(response: ToolcraftModelWorkerResponse): void { const event = new MessageEvent("message", { data: response }); for (const listener of this.removedListeners) this.call(listener, event); } emit(type: WorkerEventType, event = new Event(type)): void { for (const listener of this.listeners.get(type) ?? []) this.call(listener, event); } private call(listener: WorkerListener, event: Event): void { if (typeof listener === "function") listener(event); else listener.handleEvent(event); } } export function importRequest( jobId: string, original = new Uint8Array([1, 2, 3]).buffer, ) { const contentDigest = digestModelSourceBytes(new Uint8Array(original)); return { adapterVersion: "test-adapter@1", bundle: { aggregateDigest: "sha256:source", rootPath: "model.glb", sourceFiles: [{ bytes: original, contentDigest, mimeType: "model/gltf-binary", path: "model.glb", }], }, format: "glb" as const, geometryDecoderVersion: "test-decoder@1", jobId, limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, topologyProfile: "realtime-mesh" as const, }; } export function encodedDocumentBuffer(): ArrayBuffer { return copyArrayBuffer(encodeToolcraftModelDocument(createValidModelDocument())); } export function copyArrayBuffer(bytes: Uint8Array): ArrayBuffer { const copy = new Uint8Array(bytes.byteLength); copy.set(bytes); return copy.buffer; } export function deferred(): Readonly<{ promise: Promise; resolve: (value: Value) => void; }> { let resolve!: (value: Value) => void; const promise = new Promise((settle) => { resolve = settle; }); return { promise, resolve }; } export const forgedDigest = `sha256:${"0".repeat(64)}`; export const planDigest = `sha256:${"1".repeat(64)}`; export const topologyDigest = `sha256:${"2".repeat(64)}`; export function resultFor( request: ToolcraftModelWorkerRequest, digest?: string, ): ToolcraftModelWorkerResponse { const document = createValidModelDocument(); const resultWithoutReceipt = { analysis: snapshotToolcraftModelWorkerAnalysisSummary( analyzeToolcraftModel( document, "realtime-mesh", TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, ), ), appearanceResources: [], canonicalDocument: encodedDocumentBuffer(), canonicalDocumentDigest: digest ?? digestToolcraftModelDocument(document), operation: "decode-and-analyze" as const, }; return { generation: request.generation, jobId: request.jobId, kind: "result", result: { ...resultWithoutReceipt, receiptDigest: createToolcraftModelWorkerReceiptDigest({ generation: request.generation, jobId: request.jobId, result: resultWithoutReceipt, }), }, }; } export function draftFor( request: ToolcraftModelWorkerRequest, digest?: string, ): ToolcraftModelWorkerResponse { const document = createValidModelDocument(); const draftWithoutReceipt = { appearanceResources: [], canonicalDocument: encodedDocumentBuffer(), canonicalDocumentDigest: digest ?? digestToolcraftModelDocument(document), }; return { draft: { ...draftWithoutReceipt, receiptDigest: createToolcraftModelWorkerDraftReceiptDigest({ draft: draftWithoutReceipt, generation: request.generation, jobId: request.jobId, }), }, generation: request.generation, jobId: request.jobId, kind: "draft", }; } export function emitDecodeLifecycle( worker: FakeWorker, request: ToolcraftModelWorkerRequest, ): void { worker.emitMessage({ generation: request.generation, jobId: request.jobId, kind: "progress", phase: "decoding", progress: 0.9, }); worker.emitMessage(draftFor(request)); worker.emitMessage({ generation: request.generation, jobId: request.jobId, kind: "progress", phase: "analyzing", progress: 0.9, }); } export function completeDecode( worker: FakeWorker, request: ToolcraftModelWorkerRequest, ): void { emitDecodeLifecycle(worker, request); worker.emitMessage(resultFor(request)); }