import { describe, expect, it, vi } from "vitest"; import { createToolcraftBinaryAssetRepositoryEntry, type ToolcraftBinaryAssetRepositoryEntry, } from "../source-assets/repository/binary-asset-repository"; import { createToolcraftSourceAssetCoordinator } from "../source-assets/source-asset-coordinator"; import { createRepository, createStateHarness, deferred, type TestLease, } from "../source-assets/source-asset-coordinator-test-support"; import type { ToolcraftCommand, ToolcraftModelAsset } from "../state/types"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "./model-import-limit-values"; import type { ToolcraftModelWorkerAnalysisSummary } from "./model-import-types"; import { createToolcraftModelDocumentResourceRef, createToolcraftModelRepairPlanResourceRef, TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, TOOLCRAFT_MODEL_REPAIR_PLAN_CONTENT_TYPE, } from "./model-source-asset-handler-resources"; import type { ToolcraftModelWorkerClient, ToolcraftModelWorkerClientObservers, ToolcraftModelWorkerClientRepairRequest, } from "./worker/model-import-worker-client"; import type { ToolcraftModelRepairWorkerResult, ToolcraftModelWorkerGeometryTerminalResponse, } from "./worker/model-import-worker-protocol"; const SHA_DOCUMENT = `sha256:${"1".repeat(64)}`; const SHA_REPAIRED = `sha256:${"2".repeat(64)}`; const SHA_PLAN = `sha256:${"3".repeat(64)}`; const SHA_ENVELOPE = `sha256:${"4".repeat(64)}`; const SHA_SOURCE = `sha256:${"5".repeat(64)}`; const DOCUMENT_REF = createToolcraftModelDocumentResourceRef(SHA_DOCUMENT); const REPAIRED_REF = createToolcraftModelDocumentResourceRef(SHA_REPAIRED); const REPAIR_PLAN_REF = createToolcraftModelRepairPlanResourceRef({ envelopeDigest: SHA_ENVELOPE, planDigest: SHA_PLAN, }); function workerAnalysis(): ToolcraftModelWorkerAnalysisSummary { return { analyzerVersion: "toolcraft-topology-v1", diagnostics: [{ affectedCount: 0, code: "boundary-edges", explanation: "Repair completed.", severity: "info", }], limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, outcome: "clean", profile: "realtime-mesh", statistics: { boundaryEdgeCount: 0, componentCount: 1, decodedBytes: 64, edgeCount: 3, estimatedPeakWorkerBytes: 128, estimatedRepairBytes: 64, nodeCount: 1, nonManifoldEdgeCount: 0, nonManifoldVertexCount: 0, primitiveCount: 1, triangleCount: 1, unusedVertexCount: 0, vertexCount: 3, }, }; } function repairResult(): ToolcraftModelRepairWorkerResult { return { analysis: workerAnalysis(), canonicalDocument: new Uint8Array([7, 8, 9]).buffer, canonicalDocumentDigest: SHA_REPAIRED, operation: "repair", repairPlanDigest: SHA_PLAN, receiptDigest: SHA_SOURCE, }; } function repairableModel( overrides: Partial = {}, ): ToolcraftModelAsset { const analysis = { boundaryEdges: 2, diagnostics: [], disconnectedComponents: 1, nonManifoldEdges: 0, outcome: "repairable" as const, repairPlanRef: REPAIR_PLAN_REF, triangles: 1, vertices: 3, }; return { activeDocumentRef: DOCUMENT_REF, analysis, assetKind: "model", fileName: "mesh.glb", id: "model-1", layerId: "layer-1", lifecycle: "repairable", mimeType: "model/gltf-binary", originalAnalysis: analysis, originalDocumentRef: DOCUMENT_REF, position: { x: 0, y: 0 }, size: { height: 512, unit: "px", width: 512 }, sourceBundleDigest: SHA_SOURCE, sourceBundleRef: `toolcraft:model-source-bundle:${SHA_SOURCE}`, sourceTarget: "source.model", topologyProfile: "realtime-mesh", ...overrides, }; } function entry( ref: string, bytes: readonly number[], contentType: string, ): ToolcraftBinaryAssetRepositoryEntry { return createToolcraftBinaryAssetRepositoryEntry( ref, new Uint8Array(bytes), { contentType, durable: false }, ); } function repairLease( committedEntries: readonly ToolcraftBinaryAssetRepositoryEntry[] = [ entry(DOCUMENT_REF, [1, 2, 3], TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE), entry(REPAIR_PLAN_REF, [4, 5, 6], TOOLCRAFT_MODEL_REPAIR_PLAN_CONTENT_TYPE), ], ): TestLease { const committed = new Map(committedEntries.map((value) => [value.ref, value])); const staged = new Map(); return { commit: vi.fn(async () => undefined), get: vi.fn(async (ref: string) => staged.get(ref) ?? committed.get(ref) ?? null), put: vi.fn(async (ref, bytes, options) => { staged.set(ref, createToolcraftBinaryAssetRepositoryEntry(ref, bytes, options)); }), refs: vi.fn(() => [...staged.keys()]), rollback: vi.fn(async () => undefined), } satisfies TestLease; } type RepairImplementation = ( request: ToolcraftModelWorkerClientRepairRequest, observers?: ToolcraftModelWorkerClientObservers, ) => Promise; function workerClient(repair: RepairImplementation): ToolcraftModelWorkerClient & { repair: ReturnType>; } { return { cancel: vi.fn(), dispose: vi.fn(), extractModelPackage: vi.fn(), importModel: vi.fn(), repair: vi.fn(repair), reset: vi.fn(), }; } function successfulTerminal(jobId: string): ToolcraftModelWorkerGeometryTerminalResponse { return { generation: 1, jobId, kind: "result", result: repairResult(), }; } function setup( worker: ToolcraftModelWorkerClient, leases: readonly TestLease[] = [repairLease()], ) { const harness = createStateHarness(); harness.setState({ ...harness.getState(), mediaAssets: [repairableModel()], }); const repository = createRepository(leases); const coordinator = createToolcraftSourceAssetCoordinator({ createModelWorkerClient: () => worker, dispatch: harness.dispatch, getState: harness.getState, repository, }); return { coordinator, harness, repository }; } describe("model repair coordinator", () => { it("repairs through one lease, one CAS history patch, and exact document ref", async () => { const lease = repairLease(); const worker = workerClient(async (request, observers) => { observers?.onProgress?.({ generation: 1, jobId: request.jobId, kind: "progress", phase: "repairing", progress: 0.5, }); return successfulTerminal(request.jobId); }); const { coordinator, harness } = setup(worker, [lease]); await expect(coordinator.repairModel("model-1")).resolves.toEqual({ assetIds: ["model-1"], kind: "committed", }); expect(worker.repair).toHaveBeenCalledWith( { canonicalDocument: expect.any(ArrayBuffer), canonicalDocumentDigest: SHA_DOCUMENT, jobId: "source-asset-1", limits: TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, repairPlanEnvelope: { bytes: expect.any(ArrayBuffer), envelopeDigest: SHA_ENVELOPE, planDigest: SHA_PLAN, version: 1, }, }, expect.objectContaining({ signal: expect.any(AbortSignal) }), ); expect(lease.put).toHaveBeenCalledWith( REPAIRED_REF, new Uint8Array([7, 8, 9]), { contentType: TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE, durable: false }, ); expect(lease.commit).toHaveBeenCalledTimes(1); expect(lease.rollback).not.toHaveBeenCalled(); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: REPAIRED_REF, appliedRepairRecipeId: `toolcraft-safe-repair@1:${SHA_PLAN}`, lifecycle: "fixed", repairedDocumentRef: REPAIRED_REF, }); expect(harness.getState().history.undo).toHaveLength(1); expect(coordinator.getOperation("source.model")).toEqual({ phase: "idle", target: "source.model", }); await coordinator.dispose(); }); it("retains the original model on failure and allows a clean retry", async () => { const firstLease = repairLease(); const secondLease = repairLease(); const worker = workerClient(vi.fn() .mockResolvedValueOnce({ feedback: { category: "repair", code: "repair-worker-failed", message: "Repair could not finish.", }, generation: 1, jobId: "source-asset-1", kind: "error", }) .mockImplementationOnce(async (request) => successfulTerminal(request.jobId))); const { coordinator, harness } = setup(worker, [firstLease, secondLease]); await expect(coordinator.repairModel("model-1")).resolves.toMatchObject({ feedback: { category: "repair", code: "repair-worker-failed" }, kind: "rejected", }); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: DOCUMENT_REF, lastRepairError: { code: "repair-worker-failed" }, lifecycle: "repairable", }); expect(harness.getState().history.undo).toHaveLength(0); expect(firstLease.rollback).toHaveBeenCalledTimes(1); await expect(coordinator.repairModel("model-1")).resolves.toMatchObject({ kind: "committed", }); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: REPAIRED_REF, lifecycle: "fixed", }); expect(harness.getState().mediaAssets[0]).not.toHaveProperty("lastRepairError"); expect(secondLease.commit).toHaveBeenCalledTimes(1); await coordinator.dispose(); }); it("rejects a missing repair resource and rolls back without starting the worker", async () => { const lease = repairLease([ entry(DOCUMENT_REF, [1, 2, 3], TOOLCRAFT_MODEL_DOCUMENT_CONTENT_TYPE), ]); const worker = workerClient(async (request) => successfulTerminal(request.jobId)); const { coordinator, harness } = setup(worker, [lease]); await expect(coordinator.repairModel("model-1")).resolves.toMatchObject({ feedback: { category: "resource-unavailable", code: "model-repair-resource-unavailable", }, kind: "rejected", }); expect(worker.repair).not.toHaveBeenCalled(); expect(lease.rollback).toHaveBeenCalledTimes(1); expect(lease.commit).not.toHaveBeenCalled(); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: DOCUMENT_REF, lastRepairError: { code: "model-repair-resource-unavailable" }, lifecycle: "repairable", }); await coordinator.dispose(); }); it("collects committed repair resources when state dispatch fails", async () => { const lease = repairLease(); const repository = createRepository([lease]); const harness = createStateHarness(); harness.setState({ ...harness.getState(), mediaAssets: [repairableModel()], }); const worker = workerClient(async (request) => successfulTerminal(request.jobId)); const dispatch = vi.fn((command: ToolcraftCommand) => { if (command.type === "media.commitModelRepair") { throw new Error("State dispatch failed."); } harness.dispatch(command); }); const coordinator = createToolcraftSourceAssetCoordinator({ createModelWorkerClient: () => worker, dispatch, getState: harness.getState, repository, }); await expect(coordinator.repairModel("model-1")).resolves.toMatchObject({ feedback: { category: "repair", code: "model-repair-failed" }, kind: "rejected", }); expect(lease.commit).toHaveBeenCalledTimes(1); expect(lease.rollback).not.toHaveBeenCalled(); expect(repository.collect).toHaveBeenCalledTimes(1); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: DOCUMENT_REF, lifecycle: "repairable", }); await coordinator.dispose(); }); it("returns one promise and one worker job for concurrent repair requests", async () => { const gate = deferred(); const worker = workerClient(async () => gate.promise); const { coordinator, repository } = setup(worker); const first = coordinator.repairModel("model-1"); const second = coordinator.repairModel("model-1"); expect(second).toBe(first); await vi.waitFor(() => expect(worker.repair).toHaveBeenCalledTimes(1)); expect(repository.beginLease).toHaveBeenCalledTimes(1); gate.resolve(successfulTerminal("source-asset-1")); await expect(first).resolves.toMatchObject({ kind: "committed" }); await coordinator.dispose(); }); it("cancels an active repair and waits for worker settlement before rollback", async () => { const settled = deferred(); const lease = repairLease(); const worker = workerClient(async (request, observers) => new Promise((resolve) => { observers?.signal?.addEventListener("abort", () => { settled.promise.then(() => resolve({ generation: 1, jobId: request.jobId, kind: "cancelled", })); }, { once: true }); })); const { coordinator } = setup(worker, [lease]); const pending = coordinator.repairModel("model-1"); await vi.waitFor(() => expect(worker.repair).toHaveBeenCalledTimes(1)); expect(coordinator.getOperation("source.model")).toMatchObject({ phase: "repairing", target: "source.model", }); coordinator.cancelTarget("source.model"); expect(lease.rollback).not.toHaveBeenCalled(); settled.resolve(); await expect(pending).resolves.toEqual({ kind: "cancelled" }); expect(lease.rollback).toHaveBeenCalledTimes(1); expect(lease.commit).not.toHaveBeenCalled(); await coordinator.dispose(); }); it("rolls back a result whose model repair identity became stale", async () => { const gate = deferred(); const lease = repairLease(); const worker = workerClient(async () => gate.promise); const { coordinator, harness } = setup(worker, [lease]); const pending = coordinator.repairModel("model-1"); await vi.waitFor(() => expect(worker.repair).toHaveBeenCalledTimes(1)); const state = harness.getState(); const asset = state.mediaAssets[0]; if (!asset || asset.assetKind !== "model") throw new Error("Missing model"); harness.setState({ ...state, mediaAssets: [{ ...asset, analysis: { ...asset.analysis, repairPlanRef: `stale:${REPAIR_PLAN_REF}` }, }], }); gate.resolve(successfulTerminal("source-asset-1")); await expect(pending).resolves.toEqual({ kind: "cancelled" }); expect(lease.rollback).toHaveBeenCalledTimes(1); expect(lease.commit).not.toHaveBeenCalled(); expect(harness.getState().mediaAssets[0]).toMatchObject({ activeDocumentRef: DOCUMENT_REF, lifecycle: "repairable", }); await coordinator.dispose(); }); });