import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import { collectToolcraftBinaryMediaHydrationJobs } from "../source-assets/binary-media-hydration"; import { createToolcraftSourceAssetCoordinator } from "../source-assets/source-asset-coordinator"; import { createMemoryToolcraftBinaryAssetRepository } from "../source-assets/repository/memory-binary-asset-repository"; import { createToolcraftState } from "./create-template-state"; import { createToolcraftPersistenceSnapshot, parseToolcraftPersistenceSnapshot, } from "./persistence"; import { toolcraftReducer } from "./reducer"; function createSchema() { return defineToolcraft({ canvas: { enabled: true }, identity: { id: "legacy-media-migration", title: "Legacy Media Migration" }, panels: { controls: { sections: [ { controls: { source: { defaultValue: null, target: "source.image", type: "fileDrop", }, }, }, ], title: "Controls", }, }, }); } describe("legacy persistence media migration", () => { it("moves legacy image data urls into the repository before publishing v2", async () => { const schema = createSchema(); if (schema.persistence.storage !== "localStorage") { throw new Error("Expected local persistence"); } const legacyAsset = { dataUrl: "data:image/png;base64,AQID", fileName: "legacy.png", id: "legacy-image", layerId: "legacy-layer", mimeType: "image/png", position: { x: 0, y: 0 }, size: { height: 1, unit: "px", width: 1 }, } as const; const rawSnapshot = JSON.stringify({ state: { mediaAssets: [legacyAsset] }, version: 1, }); let state = createToolcraftState( schema, parseToolcraftPersistenceSnapshot(schema, rawSnapshot), ); const repository = createMemoryToolcraftBinaryAssetRepository(); const coordinator = createToolcraftSourceAssetCoordinator({ dispatch: (command) => { state = toolcraftReducer(state, command); }, getState: () => state, repository, }); expect(state.mediaAssets[0]).toMatchObject({ assetKind: "image", lifecycle: "restoring", resourceRef: expect.stringMatching(/^media:image:sha256:/u), }); expect(state.mediaAssets[0]).not.toHaveProperty("dataUrl"); await coordinator.hydrateBinaryMedia( collectToolcraftBinaryMediaHydrationJobs([legacyAsset]), ); const migratedAsset = state.mediaAssets[0]; expect(migratedAsset).toMatchObject({ assetKind: "image", lifecycle: "ready", resourceRef: expect.stringMatching(/^media:image:sha256:/u), }); if (!migratedAsset || migratedAsset.assetKind === "model") { throw new Error("Expected migrated binary media"); } await expect(repository.get(migratedAsset.resourceRef)).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), contentType: "image/png", durable: true, }); const snapshot = createToolcraftPersistenceSnapshot( state, schema.persistence, ); expect(snapshot?.version).toBe(2); expect(JSON.stringify(snapshot)).not.toContain("data:image/"); await coordinator.dispose(); }); it("marks only missing restored refs unavailable", async () => { const schema = createSchema(); let state = createToolcraftState(schema, { mediaAssets: [ { assetKind: "image", fileName: "available.png", id: "available", layerId: "available-layer", lifecycle: "restoring", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: "media:image:available", }, { assetKind: "file", fileName: "missing.txt", id: "missing", layerId: "missing-layer", lifecycle: "restoring", mimeType: "text/plain", position: { x: 0, y: 0 }, resourceRef: "media:file:missing", }, ], }); const repository = createMemoryToolcraftBinaryAssetRepository(); const lease = await repository.beginLease("seed-available-media"); await lease.put("media:image:available", new Uint8Array([1]), { contentType: "image/png", durable: true, }); await lease.commit(); const coordinator = createToolcraftSourceAssetCoordinator({ dispatch: (command) => { state = toolcraftReducer(state, command); }, getState: () => state, repository, }); await coordinator.hydrateBinaryMedia([]); expect(state.mediaAssets).toEqual([ expect.objectContaining({ id: "available", lifecycle: "ready" }), expect.objectContaining({ error: expect.objectContaining({ code: "binary-resource-unavailable" }), id: "missing", lifecycle: "unavailable", }), ]); await coordinator.dispose(); }); });