import { describe, expect, it, vi } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import type { ToolcraftControlSchema } from "../schema/types"; import { createToolcraftState } from "../state/create-template-state"; import type { ToolcraftCommand } from "../state/types"; import type { ToolcraftBinaryAssetRepository } from "./repository/binary-asset-repository"; import { createToolcraftSourceAssetCoordinator } from "./source-asset-coordinator"; import { createToolcraftSourceAssetPresentation } from "./source-asset-presentation"; import { createToolcraftSourceAssetRegistry } from "./source-asset-registry"; import type { ToolcraftPreparedSourceAssetRecord, ToolcraftSourceAssetBatch, ToolcraftSourceAssetHandler, } from "./source-asset-types"; const FILE_CONTROL: ToolcraftControlSchema = { assetKind: "file", multiple: true, target: "source.files", type: "fileDrop", }; function createRepository(): ToolcraftBinaryAssetRepository & { beginLease: ReturnType; } { return { beginLease: vi.fn(async () => { throw new Error("planning must not acquire a repository lease"); }), collect: vi.fn(async () => []), dispose: vi.fn(async () => undefined), get: vi.fn(async () => null), }; } function createPlanningHandler( failure: "invalid-plan" | "throw", ): ToolcraftSourceAssetHandler< "file", ToolcraftPreparedSourceAssetRecord<"file"> > { return { kind: "file", match: vi.fn((batch: ToolcraftSourceAssetBatch) => ({ handlerKind: "file" as const, rootFileNames: batch.files.map((file) => file.name), specificity: 1, })), plan: vi.fn((batch, control, claim) => { if (failure === "throw") { throw new Error("could not plan source files"); } return { claim, logicalAssetCount: 0, replaceExisting: control.multiple !== true, target: control.target, }; }), prepare: vi.fn(async () => { throw new Error("planning failure must not prepare"); }), present: vi.fn(() => { throw new Error("not used directly"); }), }; } describe("source asset coordinator planning feedback", () => { it.each(["throw", "invalid-plan"] as const)( "publishes %s failures to the target presentation", async (failure) => { const state = createToolcraftState( defineToolcraft({ canvas: { enabled: true }, panels: {} }), ); const repository = createRepository(); const handler = createPlanningHandler(failure); const coordinator = createToolcraftSourceAssetCoordinator({ dispatch: vi.fn((_command: ToolcraftCommand) => undefined), getState: () => state, registry: createToolcraftSourceAssetRegistry([handler]), repository, }); const listener = vi.fn(); coordinator.subscribe(listener); const outcome = await coordinator.importBatch( { files: [new File(["notes"], "notes.txt", { type: "text/plain" })], origin: "panel", target: FILE_CONTROL.target, }, FILE_CONTROL, ); expect(outcome).toMatchObject({ feedback: { category: "bundle", code: "planning-failed" }, kind: "rejected", }); const operation = coordinator.getOperation(FILE_CONTROL.target); expect(operation).toMatchObject({ feedback: { category: "bundle", code: "planning-failed" }, phase: "idle", target: FILE_CONTROL.target, }); expect( createToolcraftSourceAssetPresentation( "file", FILE_CONTROL, [], operation, ).feedback, ).toEqual(operation.feedback); expect(listener).toHaveBeenCalledTimes(1); expect(repository.beginLease).not.toHaveBeenCalled(); expect(handler.prepare).not.toHaveBeenCalled(); }, ); });