import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ToolcraftControlSchema } from "../schema/types"; import { createLease, createRepository, createStateHarness, testFile, } from "./source-asset-coordinator-test-support"; import { createToolcraftSourceAssetCoordinator } from "./source-asset-coordinator"; import { toolcraftImageSourceAssetHandler } from "./image-source-asset-handler"; import { createToolcraftMediaResourceRef } from "./media-resource-ref"; import { createMemoryToolcraftBinaryAssetRepository, } from "./repository/memory-binary-asset-repository"; import { createToolcraftSourceAssetRegistry } from "./source-asset-registry"; import type { ToolcraftSourceAssetPrepareContext } from "./source-asset-types"; const mediaFileMocks = vi.hoisted(() => ({ isToolcraftImageFile: vi.fn(() => true), readImportedImageFile: vi.fn(), })); vi.mock("../react/canvas/media-file", () => mediaFileMocks); function createContext( canvasFrame: ToolcraftSourceAssetPrepareContext["canvasFrame"], ): ToolcraftSourceAssetPrepareContext { const file = testFile("image.png", "image/png"); const target = "source.image"; const claim = { handlerKind: "image" as const, rootFileNames: [file.name], specificity: 100, }; const control: ToolcraftControlSchema = { assetKind: "image", target, type: "fileDrop", }; return { batch: { files: [file], origin: "canvas", position: { x: 120, y: -40 }, target, }, canvasFrame, claim, control, jobId: "image-frame-test", plan: { claim, logicalAssetCount: 1, replaceExisting: true, sourceTarget: target, target, }, reportOperation: vi.fn(), signal: new AbortController().signal, stageResource: vi.fn(async () => undefined), }; } beforeEach(() => { vi.clearAllMocks(); mediaFileMocks.readImportedImageFile.mockResolvedValue({ bytes: new Uint8Array([1, 2, 3]), size: { height: 32, unit: "px", width: 64 }, }); }); describe("image source asset scene geometry", () => { it.each([ { expectedFallback: { height: 200, unit: "px", width: 300 }, frame: { kind: "finite" as const, size: { height: 200, unit: "px" as const, width: 300 }, }, }, { expectedFallback: { height: 512, unit: "px", width: 512 }, frame: { kind: "infinite" as const }, }, ])("uses the active $frame.kind decode fallback", async ({ expectedFallback, frame }) => { const prepared = await toolcraftImageSourceAssetHandler.prepare( createContext(frame), ); expect(mediaFileMocks.readImportedImageFile).toHaveBeenCalledWith( expect.any(File), expectedFallback, expect.any(AbortSignal), ); expect(prepared.assets[0]).toMatchObject({ position: { x: 120, y: -40 }, size: { height: 32, unit: "px", width: 64 }, }); }); }); describe("image source asset batch repository adoption", () => { it("commits every logical image and layer while identical bytes share one leased ref", async () => { const harness = createStateHarness(); const lease = createLease(); const repository = createRepository([lease]); const coordinator = createToolcraftSourceAssetCoordinator({ dispatch: harness.dispatch, getState: harness.getState, registry: createToolcraftSourceAssetRegistry([ toolcraftImageSourceAssetHandler, ]), repository, }); const files = Array.from({ length: 10 }, (_, index) => testFile(`image-${index + 1}.png`, "image/png"), ); await expect( coordinator.importBatch( { files, origin: "panel", target: "source.images", }, { assetKind: "image", multiple: true, target: "source.images", type: "fileDrop", }, ), ).resolves.toMatchObject({ assetIds: Array.from({ length: files.length }, (_, index) => `media-${index + 1}`), kind: "committed", }); const sharedRef = createToolcraftMediaResourceRef( "image", new Uint8Array([1, 2, 3]), ); expect(lease.refs()).toEqual([sharedRef]); expect(lease.commit).toHaveBeenCalledTimes(1); expect(lease.rollback).not.toHaveBeenCalled(); expect(harness.getState().mediaAssets).toHaveLength(files.length); expect(harness.getState().layers).toHaveLength(files.length); await coordinator.dispose(); }); it("rolls back a partially staged image batch without publishing its shared ref", async () => { const harness = createStateHarness(); const repository = createMemoryToolcraftBinaryAssetRepository(); const coordinator = createToolcraftSourceAssetCoordinator({ dispatch: harness.dispatch, getState: harness.getState, registry: createToolcraftSourceAssetRegistry([ toolcraftImageSourceAssetHandler, ]), repository, }); const sharedBytes = new Uint8Array([1, 2, 3]); const sharedRef = createToolcraftMediaResourceRef("image", sharedBytes); mediaFileMocks.readImportedImageFile.mockImplementation(async (file: File) => file.name === "broken.png" ? null : { bytes: sharedBytes, size: { height: 32, unit: "px", width: 64 }, }, ); await expect( coordinator.importBatch( { files: [ testFile("first.png", "image/png"), testFile("broken.png", "image/png"), testFile("third.png", "image/png"), ], origin: "panel", target: "source.images", }, { assetKind: "image", multiple: true, target: "source.images", type: "fileDrop", }, ), ).resolves.toMatchObject({ kind: "rejected" }); await expect(repository.get(sharedRef)).resolves.toBeNull(); expect(harness.getState().mediaAssets).toHaveLength(0); expect(harness.getState().layers).toHaveLength(0); await coordinator.dispose(); }); });