import { IDBFactory } from 'fake-indexeddb'; import { BrowserAssetStore } from '@openmaic/storage'; import type { Slide } from '@openmaic/dsl'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { MediaTask } from '@/lib/store/media-generation'; import type { Scene, Stage } from '@/lib/types/stage'; import type { MediaFileRecord } from '@/lib/utils/database'; const mocks = vi.hoisted(() => ({ probePresence: vi.fn(), settings: vi.fn(), mediaPut: vi.fn(), mediaGet: vi.fn(), mediaDelete: vi.fn(), getPool: vi.fn(), document: null as ReturnType | null, stageState: { stage: null, scenes: [] } as { stage: ReturnType['stage'] | null; scenes: ReturnType['scenes']; }, stageSetState: vi.fn(), markStagePersistenceDirty: vi.fn(), beforeDocumentWork: vi.fn(), accessDocument: vi.fn(), listDocuments: vi.fn(), loadDocument: vi.fn(), otherDocuments: new Map>(), mediaRows: new Map(), serverBacked: false, })); vi.mock('@/lib/media/asset-pool-config', () => ({ isAssetPoolServerBacked: () => mocks.serverBacked, })); vi.mock('@/lib/store/settings', () => ({ useSettingsStore: { getState: mocks.settings }, })); vi.mock('@/lib/utils/database', () => ({ mediaFileKey: (stageId: string, ref: string) => `${stageId}:${ref}`, db: { mediaFiles: { put: mocks.mediaPut, get: mocks.mediaGet, delete: mocks.mediaDelete, }, }, })); vi.mock('@/lib/media/asset-pool', () => ({ getAssetPool: mocks.getPool, putAsset: (...args: unknown[]) => mocks.getPool().put(...args), replaceAsset: (...args: unknown[]) => mocks.getPool().replace(...args), removeAsset: (...args: unknown[]) => mocks.getPool().remove(...args), })); // Production runs one realm unless a peer answers; individual tests override // this to model an unavailable probe or a live peer. vi.mock('@/lib/media/stage-realm-presence', () => ({ probeStageRealmPresence: mocks.probePresence, })); vi.mock('@/lib/document-store', () => ({ accessDocument: mocks.accessDocument, getDocumentStore: () => ({ listDocuments: mocks.listDocuments, loadDocument: mocks.loadDocument, }), mutateDocument: async ( _stageId: string, work: (document: unknown, store: { saveDocument: (next: unknown) => Promise }) => unknown, ) => ( mocks.beforeDocumentWork(), work(mocks.document, { saveDocument: async (next) => { mocks.document = structuredClone(next) as ReturnType; }, }) ), })); vi.mock('@/lib/store/stage', () => ({ useStageStore: { getState: () => mocks.stageState, setState: mocks.stageSetState, }, markStagePersistenceDirty: mocks.markStagePersistenceDirty, })); import { generateMediaForOutlines, mediaRetryTarget, reconcileCompletedMediaForScene, retryMediaTask, } from '@/lib/media/media-orchestrator'; import { buildRestoredMediaTasks } from '@/lib/classroom/load-classroom'; import { useMediaGenerationStore } from '@/lib/store/media-generation'; import { markStageDeleted, unmarkStageDeleted } from '@/lib/utils/deleted-stages'; import { resolveSlideMediaState } from '@/components/slide-renderer/use-resolved-slide'; import { expectDocumentAssetOwnership } from './assert-stage-asset-ownership'; const stageId = 'stage-write-path'; const placeholder = 'gen_vid_1'; function documentWithMedia(ref = placeholder, type: 'image' | 'video' = 'video') { const element: { id: string; type: 'image' | 'video'; left: number; top: number; width: number; height: number; rotate: number; src: string; mediaRef?: string; poster?: string; autoplay?: boolean; fixedRatio?: boolean; } = type === 'video' ? { id: 'video-1', type: 'video', left: 0, top: 0, width: 100, height: 56, rotate: 0, src: ref, mediaRef: ref, autoplay: false, } : { id: 'image-1', type: 'image', left: 0, top: 0, width: 100, height: 100, rotate: 0, fixedRatio: true, src: ref, }; return { dslVersion: 1, stage: { id: stageId, name: 'Write paths', createdAt: 1, updatedAt: 1, ...(type === 'video' ? { videoManifest: { [ref]: { type: 'video', prompt: 'A short clip' } } } : {}), }, scenes: [ { id: 'scene-1', stageId, title: 'Scene', order: 1, type: 'slide', content: { type: 'slide', canvas: { id: 'slide-1', viewportSize: 1000, viewportRatio: 0.5625, background: { type: 'solid', color: '#fff' }, theme: { fontName: 'Arial', fontColor: '#111', backgroundColor: '#fff', themeColors: ['#111'], }, elements: [element], }, }, }, ], }; } function failedTask(ref: string, type: 'image' | 'video' = 'image'): MediaTask { return { elementId: ref, type, status: 'failed', prompt: type === 'image' ? 'A new image' : 'A short clip', params: { aspectRatio: '16:9' }, retryCount: 0, stageId, error: 'retry me', }; } function doneTask(ref: string, type: 'image' | 'video' = 'image'): MediaTask { return { ...failedTask(ref, type), status: 'done', objectUrl: 'blob:source', error: undefined, }; } describe('media orchestrator asset write paths', () => { let pool: BrowserAssetStore; let objectUrls: Map; let fetchMock: ReturnType; beforeEach(() => { pool = new BrowserAssetStore({ indexedDB: new IDBFactory(), dbName: `orchestrator-${crypto.randomUUID()}`, }); mocks.getPool.mockReset(); mocks.getPool.mockReturnValue(pool); mocks.probePresence.mockReset().mockResolvedValue('absent'); mocks.serverBacked = false; mocks.mediaRows.clear(); mocks.mediaPut.mockReset().mockImplementation(async (row) => { mocks.mediaRows.set(row.id, row); }); mocks.mediaGet.mockReset().mockImplementation(async (id) => mocks.mediaRows.get(id)); mocks.mediaDelete.mockReset().mockImplementation(async (id) => { mocks.mediaRows.delete(id); }); mocks.stageSetState.mockReset(); mocks.markStagePersistenceDirty.mockReset(); mocks.beforeDocumentWork.mockReset(); mocks.otherDocuments.clear(); mocks.listDocuments .mockReset() .mockImplementation(async () => [ ...(mocks.document ? [{ id: mocks.document.stage.id }] : []), ...[...mocks.otherDocuments.keys()].map((id) => ({ id })), ]); mocks.loadDocument .mockReset() .mockImplementation(async (id) => id === mocks.document?.stage.id ? mocks.document : (mocks.otherDocuments.get(id) ?? null), ); mocks.accessDocument.mockReset().mockImplementation(async () => ({ document: mocks.document, readOnlyLegacy: false, })); mocks.stageState = { stage: null, scenes: [] }; mocks.settings.mockReset().mockReturnValue({ imageGenerationEnabled: true, videoGenerationEnabled: true, imageProviderId: 'image-provider', imageModelId: 'image-model', imageProvidersConfig: {}, videoProviderId: 'video-provider', videoModelId: 'video-model', videoProvidersConfig: {}, }); useMediaGenerationStore.setState({ tasks: {} }); objectUrls = new Map(); vi.stubGlobal('URL', { createObjectURL: vi.fn((blob: Blob) => { const url = `blob:test-${objectUrls.size + 1}`; objectUrls.set(url, blob); return url; }), revokeObjectURL: vi.fn(), }); fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); }); afterEach(async () => { await pool.close(); vi.unstubAllGlobals(); }); function serveVideo(bytes = 'video-new') { fetchMock.mockImplementation(async (input: string, init?: RequestInit) => { if (input === '/api/generate/video') { return new Response( JSON.stringify({ success: true, result: { url: 'https://media.test/video', poster: 'https://media.test/poster', width: 1280, height: 720, duration: 5, }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ); } if (input === '/api/proxy-media') { const requested = JSON.parse(String(init?.body)).url as string; return requested.endsWith('/poster') ? new Response(new Blob(['poster-new'], { type: 'image/jpeg' }), { status: 200 }) : new Response(new Blob([bytes], { type: 'video/mp4' }), { status: 200 }); } throw new Error(`Unexpected fetch: ${input}`); }); } function serveImage(bytes = 'image-new') { fetchMock.mockImplementation(async (input: string) => { if (input === '/api/generate/image') { return new Response( JSON.stringify({ success: true, result: { url: 'https://media.test/image', width: 1024, height: 576 }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ); } if (input === '/api/proxy-media') { return new Response(new Blob([bytes], { type: 'image/png' }), { status: 200 }); } throw new Error(`Unexpected fetch: ${input}`); }); } async function resolvedText(ref: string): Promise { const url = await pool.resolve(ref); return url ? ((await objectUrls.get(url)?.text()) ?? null) : null; } async function assertOwnership(): Promise { if (!mocks.document) throw new Error('Expected a document'); await expectDocumentAssetOwnership({ document: mocks.document as unknown as Parameters< typeof expectDocumentAssetOwnership >[0]['document'], mediaRowIds: new Set(mocks.mediaRows.keys()), audioRowIds: new Set(), poolHas: async (ref) => { const url = await pool.resolve(ref); if (!url) return false; await pool.release(ref); return true; }, }); } it('converges when the scene is inserted before media completes', async () => { mocks.document = documentWithMedia(); const put = vi.spyOn(pool, 'put'); serveVideo(); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['video'], order: 1, mediaGenerations: [ { type: 'video', prompt: 'A short clip', elementId: placeholder, aspectRatio: '16:9' }, ], }, ], stageId, ); const element = mocks.document.scenes[0].content.canvas.elements[0]; const assetId = element.mediaRef as string; const posterAssetId = element.poster; expect(assetId).toMatch(/^ast_/); expect(element.src).toBe(assetId); expect(posterAssetId).toMatch(/^ast_/); expect(mocks.document.stage.videoManifest).toEqual({ [assetId]: { type: 'video', prompt: 'A short clip' }, }); expect(await resolvedText(assetId)).toBe('video-new'); expect(await resolvedText(posterAssetId as string)).toBe('poster-new'); expect(put).toHaveBeenCalledWith( expect.any(Blob), expect.objectContaining({ contentType: 'video/mp4', prompt: 'A short clip', dimensions: { width: 1280, height: 720 }, duration: 5, }), ); expect(mocks.mediaPut).toHaveBeenCalledWith( expect.objectContaining({ id: `${stageId}:${assetId}`, stageId, type: 'video' }), ); expect(mocks.mediaPut).toHaveBeenCalledWith( expect.objectContaining({ id: `${stageId}:${posterAssetId}`, stageId, type: 'image' }), ); expect(useMediaGenerationStore.getState().tasks[placeholder]).toBeUndefined(); expect(useMediaGenerationStore.getState().tasks[assetId]).toMatchObject({ elementId: assetId, status: 'done', }); await assertOwnership(); }); it('converges when media completes before the generated scene is inserted', async () => { const pendingDocument = documentWithMedia(placeholder, 'video'); const lateScene = structuredClone(pendingDocument.scenes[0]); pendingDocument.scenes = []; mocks.document = pendingDocument; serveVideo('early-media'); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['video'], order: 1, mediaGenerations: [ { type: 'video', prompt: 'A short clip', elementId: placeholder, aspectRatio: '16:9' }, ], }, ], stageId, ); const completed = Object.values(useMediaGenerationStore.getState().tasks)[0]; expect(completed).toMatchObject({ status: 'done', placeholderRef: placeholder, elementId: expect.stringMatching(/^ast_/), }); const reconciled = reconcileCompletedMediaForScene( lateScene as unknown as Scene, pendingDocument.stage as unknown as Stage, useMediaGenerationStore.getState().tasks, ); mocks.document = { ...pendingDocument, stage: reconciled.stage, scenes: [reconciled.scene], } as unknown as ReturnType; const insertedDocument = mocks.document; if (!insertedDocument) throw new Error('Expected the reconciled document'); const insertedRef = insertedDocument.scenes[0].content.canvas.elements[0].src; expect(insertedRef).toBe(completed.elementId); expect(insertedDocument.scenes[0].content.canvas.elements[0].poster).toBe( completed.posterAssetId, ); expect(insertedDocument.stage.videoManifest).toEqual({ [completed.elementId]: { type: 'video', prompt: 'A short clip' }, }); expect(await resolvedText(completed.elementId)).toBe('early-media'); expect(mocks.mediaPut).toHaveBeenCalledWith( expect.objectContaining({ id: `${stageId}:${completed.elementId}` }), ); }); it('makes zero API calls and zero allocations when a restored document no longer owns the placeholder', async () => { const assetId = await pool.put(new Blob(['persisted-image'], { type: 'image/png' })); const put = vi.spyOn(pool, 'put'); put.mockClear(); mocks.document = documentWithMedia(assetId, 'image'); useMediaGenerationStore.setState({ tasks: buildRestoredMediaTasks(stageId, [ { id: `${stageId}:${assetId}`, stageId, placeholderRef: placeholder, type: 'image', blob: new Blob(['persisted-image'], { type: 'image/png' }), mimeType: 'image/png', size: 15, prompt: 'A new image', params: '{}', createdAt: 1, }, ]), }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); expect(fetchMock).not.toHaveBeenCalled(); expect(put).not.toHaveBeenCalled(); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(assetId); }); it('runs a final document reconciliation before task re-keying when a scene lands late', async () => { const pendingDocument = documentWithMedia(placeholder, 'video'); const lateScene = structuredClone(pendingDocument.scenes[0]); pendingDocument.scenes = []; mocks.document = pendingDocument; serveVideo('late-window-media'); let documentScans = 0; mocks.beforeDocumentWork.mockImplementation(() => { documentScans += 1; if (documentScans !== 2 || !mocks.document) return; const pending = Object.values(useMediaGenerationStore.getState().tasks)[0]; expect(pending).toMatchObject({ status: 'generating', elementId: placeholder, }); mocks.document.scenes = [lateScene]; }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['video'], order: 1, mediaGenerations: [ { type: 'video', prompt: 'A short clip', elementId: placeholder, aspectRatio: '16:9' }, ], }, ], stageId, ); const completed = Object.values(useMediaGenerationStore.getState().tasks)[0]; expect(mocks.document.scenes[0].content.canvas.elements[0]).toMatchObject({ src: completed.elementId, mediaRef: completed.elementId, poster: completed.posterAssetId, }); }); it('rollback layer 1: leaves the document and Dexie untouched when pool allocation fails', async () => { mocks.document = documentWithMedia(placeholder, 'image'); const before = structuredClone(mocks.document); serveImage(); mocks.getPool.mockReturnValueOnce({ put: vi.fn().mockRejectedValue(new Error('pool write failed')), }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); expect(mocks.document).toEqual(before); expect(mocks.mediaPut).not.toHaveBeenCalled(); expect(useMediaGenerationStore.getState().tasks[placeholder]?.status).toBe('failed'); }); it('rollback layer 2: removes every fresh allocation when compatibility persistence fails', async () => { mocks.document = documentWithMedia(placeholder, 'video'); serveVideo('uncommitted-video'); const put = vi.spyOn(pool, 'put'); mocks.mediaPut.mockRejectedValueOnce(new Error('compatibility write failed')); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['video'], order: 1, mediaGenerations: [ { type: 'video', prompt: 'A short clip', elementId: placeholder, aspectRatio: '16:9' }, ], }, ], stageId, ); const allocated = await Promise.all(put.mock.results.map((result) => result.value)); expect(allocated).toHaveLength(2); for (const ref of allocated) expect(await pool.resolve(ref)).toBeNull(); expect(mocks.mediaRows.size).toBe(0); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(placeholder); }); it('rollback layer 3: removes pool and compatibility writes when document commit fails', async () => { mocks.document = documentWithMedia(placeholder, 'image'); const before = structuredClone(mocks.document); serveImage('uncommitted-image'); const put = vi.spyOn(pool, 'put'); mocks.beforeDocumentWork.mockImplementationOnce(() => { throw new Error('document write failed'); }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); const [allocated] = await Promise.all(put.mock.results.map((result) => result.value)); expect(await pool.resolve(allocated)).toBeNull(); expect(mocks.mediaRows.has(`${stageId}:${allocated}`)).toBe(false); expect(mocks.document).toEqual(before); }); it('replaces exclusively owned media bytes for a production-shaped targeted retry', async () => { const assetId = await pool.put(new Blob(['image-old'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); const before = structuredClone(mocks.document); const put = vi.spyOn(pool, 'put'); const replace = vi.spyOn(pool, 'replace'); const release = vi.spyOn(pool, 'release'); serveImage('image-replaced'); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); await retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); expect(replace).toHaveBeenCalledWith( assetId, expect.any(Blob), expect.objectContaining({ prompt: 'A new image', dimensions: { width: 1024, height: 576 }, }), ); expect(put).not.toHaveBeenCalled(); expect(release).toHaveBeenCalledWith(assetId); expect(await resolvedText(assetId)).toBe('image-replaced'); expect(mocks.document).toEqual(before); expect(mocks.mediaDelete).not.toHaveBeenCalled(); expect(mocks.mediaPut).toHaveBeenCalledWith( expect.objectContaining({ id: `${stageId}:${assetId}` }), ); expect(await mocks.mediaRows.get(`${stageId}:${assetId}`)?.blob.text()).toBe('image-replaced'); expect(useMediaGenerationStore.getState().tasks[assetId]?.status).toBe('done'); expect(mocks.listDocuments).toHaveBeenCalledTimes(2); expect(mocks.loadDocument).toHaveBeenCalledWith(stageId); await assertOwnership(); }); it('forks a targeted retry when peer presence cannot be probed', async () => { // No BroadcastChannel, binding not finished, or a constructor that threw all // report 'unknown'. Proving nothing about other realms must not authorize a // global mutation, so the write boundary itself has to fork. mocks.probePresence.mockResolvedValue('unknown'); const assetId = await pool.put(new Blob(['image-old'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); const replace = vi.spyOn(pool, 'replace'); serveImage('image-forked'); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); await retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); const rewritten = mocks.document.scenes[0].content.canvas.elements[0].src; expect(replace).not.toHaveBeenCalled(); expect(rewritten).toMatch(/^ast_/); expect(rewritten).not.toBe(assetId); expect(await resolvedText(rewritten)).toBe('image-forked'); // A peer's unflushed owner, had there been one, still resolves the old bytes. expect(await resolvedText(assetId)).toBe('image-old'); }); it('forks a targeted retry in server mode even when local ownership is exclusive', async () => { mocks.serverBacked = true; const assetId = await pool.put(new Blob(['image-old'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); const replace = vi.spyOn(pool, 'replace'); serveImage('image-server-fork'); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); await retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); const rewritten = mocks.document.scenes[0].content.canvas.elements[0].src; expect(replace).not.toHaveBeenCalled(); expect(rewritten).toMatch(/^ast_/); expect(rewritten).not.toBe(assetId); expect(await resolvedText(rewritten)).toBe('image-server-fork'); expect(await resolvedText(assetId)).toBe('image-old'); }); it('forks when an exclusive ref becomes shared while generation is pending', async () => { const assetId = await pool.put(new Blob(['image-original'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${assetId}`, { id: `${stageId}:${assetId}`, blob: new Blob(['image-original'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); const replace = vi.spyOn(pool, 'replace'); let finishGeneration!: (response: Response) => void; const generation = new Promise((resolve) => { finishGeneration = resolve; }); fetchMock.mockImplementation(async (input: string) => { if (input === '/api/generate/image') return generation; if (input === '/api/proxy-media') { return new Response(new Blob(['image-retried'], { type: 'image/png' }), { status: 200 }); } throw new Error(`Unexpected fetch: ${input}`); }); const retrying = retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); await vi.waitFor(() => { expect(useMediaGenerationStore.getState().tasks[assetId]?.status).toBe('generating'); }); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState.scenes.push(structuredClone(copy)); finishGeneration( new Response( JSON.stringify({ success: true, result: { url: 'https://media.test/image', width: 1024, height: 576 }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ), ); await retrying; const targetRef = mocks.document.scenes[0].content.canvas.elements[0].src; const copiedRef = mocks.document.scenes[1].content.canvas.elements[0].src; expect(targetRef).toMatch(/^ast_/); expect(targetRef).not.toBe(assetId); expect(copiedRef).toBe(assetId); expect(await resolvedText(targetRef)).toBe('image-retried'); expect(await resolvedText(copiedRef)).toBe('image-original'); expect(await mocks.mediaRows.get(`${stageId}:${assetId}`)?.blob.text()).toBe('image-original'); expect(replace).not.toHaveBeenCalled(); expect(mocks.listDocuments).toHaveBeenCalledTimes(2); }); it('forks when the duplicate exists only in the unflushed editor state', async () => { const assetId = await pool.put(new Blob(['image-original'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${assetId}`, { id: `${stageId}:${assetId}`, blob: new Blob(['image-original'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); const replace = vi.spyOn(pool, 'replace'); let finishGeneration!: (response: Response) => void; const generation = new Promise((resolve) => { finishGeneration = resolve; }); fetchMock.mockImplementation(async (input: string) => { if (input === '/api/generate/image') return generation; if (input === '/api/proxy-media') { return new Response(new Blob(['image-retried'], { type: 'image/png' }), { status: 200 }); } throw new Error(`Unexpected fetch: ${input}`); }); const retrying = retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); await vi.waitFor(() => { expect(useMediaGenerationStore.getState().tasks[assetId]?.status).toBe('generating'); }); // Duplication lands in the Zustand aggregate immediately; persistence is // still behind its debounce, so the stored document keeps a single owner. const copy = structuredClone(mocks.stageState.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.stageState.scenes.push(copy); expect(mocks.document.scenes).toHaveLength(1); finishGeneration( new Response( JSON.stringify({ success: true, result: { url: 'https://media.test/image', width: 1024, height: 576 }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ), ); await retrying; const targetRef = mocks.document.scenes[0].content.canvas.elements[0].src; expect(targetRef).toMatch(/^ast_/); expect(targetRef).not.toBe(assetId); expect(await resolvedText(targetRef)).toBe('image-retried'); // The unflushed copy still points at the original id, whose bytes are intact. expect(mocks.stageState.scenes[1].content.canvas.elements[0].src).toBe(assetId); expect(await resolvedText(assetId)).toBe('image-original'); expect(await mocks.mediaRows.get(`${stageId}:${assetId}`)?.blob.text()).toBe('image-original'); expect(replace).not.toHaveBeenCalled(); }); it('forks a targeted retry when another persisted document aliases the allocated ref', async () => { const assetId = await pool.put(new Blob(['shared-original'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; const other = documentWithMedia(assetId, 'image'); other.stage.id = 'stage-other'; other.scenes[0].stageId = 'stage-other'; mocks.otherDocuments.set('stage-other', other); mocks.mediaRows.set(`${stageId}:${assetId}`, { id: `${stageId}:${assetId}`, blob: new Blob(['shared-original'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); const replace = vi.spyOn(pool, 'replace'); serveImage('retried-target'); await retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); const retriedRef = mocks.document.scenes[0].content.canvas.elements[0].src; expect(retriedRef).toMatch(/^ast_/); expect(retriedRef).not.toBe(assetId); expect(other.scenes[0].content.canvas.elements[0].src).toBe(assetId); expect(await resolvedText(assetId)).toBe('shared-original'); expect(await resolvedText(retriedRef)).toBe('retried-target'); expect(await mocks.mediaRows.get(`${stageId}:${assetId}`)?.blob.text()).toBe('shared-original'); expect(mocks.mediaDelete).not.toHaveBeenCalledWith(`${stageId}:${assetId}`); expect(replace).not.toHaveBeenCalled(); }); it('forks instead of replacing when persisted ownership enumeration fails', async () => { const assetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.listDocuments.mockRejectedValueOnce(new Error('document store unavailable')); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); const replace = vi.spyOn(pool, 'replace'); serveImage('forked-bytes'); await retryMediaTask(assetId, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); const retriedRef = mocks.document.scenes[0].content.canvas.elements[0].src; expect(retriedRef).toMatch(/^ast_/); expect(retriedRef).not.toBe(assetId); expect(await resolvedText(assetId)).toBe('original-bytes'); expect(await resolvedText(retriedRef)).toBe('forked-bytes'); expect(replace).not.toHaveBeenCalled(); }); it('replaces an allocated video and its referenced poster without rewriting the document', async () => { const assetId = await pool.put(new Blob(['video-old'], { type: 'video/mp4' })); const posterAssetId = await pool.put(new Blob(['poster-old'], { type: 'image/jpeg' })); mocks.document = documentWithMedia(assetId, 'video'); mocks.document.scenes[0].content.canvas.elements[0].poster = posterAssetId; mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; const before = structuredClone(mocks.document); serveVideo('video-replaced'); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId, 'video') }, }); await retryMediaTask(assetId, { elementId: 'video-1', sceneId: 'scene-1', slideId: 'slide-1', }); expect(await resolvedText(assetId)).toBe('video-replaced'); expect(await resolvedText(posterAssetId)).toBe('poster-new'); expect(mocks.document).toEqual(before); expect(mocks.mediaPut).toHaveBeenCalledWith( expect.objectContaining({ id: `${stageId}:${assetId}`, poster: expect.any(Blob) }), ); await assertOwnership(); }); it('upgrades a legacy retry through the allocating path', async () => { mocks.document = documentWithMedia(placeholder, 'image'); const put = vi.spyOn(pool, 'put'); const replace = vi.spyOn(pool, 'replace'); serveImage('legacy-upgraded'); useMediaGenerationStore.setState({ tasks: { [placeholder]: failedTask(placeholder) } }); mocks.mediaRows.set(`${stageId}:${placeholder}`, { id: `${stageId}:${placeholder}`, blob: new Blob(), error: 'legacy failure', }); await retryMediaTask(placeholder, { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }); const assetId = mocks.document.scenes[0].content.canvas.elements[0].src as string; expect(assetId).toMatch(/^ast_/); expect(put).toHaveBeenCalled(); expect(replace).not.toHaveBeenCalled(); expect(mocks.mediaDelete).toHaveBeenCalledWith(`${stageId}:${placeholder}`); expect(await resolvedText(assetId)).toBe('legacy-upgraded'); await assertOwnership(); }); it('forks a duplicated media ref on targeted regeneration', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: failedTask(sharedAssetId) }, }); const sourceStatuses: Array = []; const unsubscribe = useMediaGenerationStore.subscribe((state) => { sourceStatuses.push(state.tasks[sharedAssetId]?.status); }); serveImage('copy-bytes'); mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); await retryMediaTask(sharedAssetId, { elementId: 'image-copy', sceneId: 'scene-copy', slideId: 'slide-copy', }); unsubscribe(); const [originalScene, copiedScene] = mocks.document.scenes; const originalRef = originalScene.content.canvas.elements[0].src; const copiedRef = copiedScene.content.canvas.elements[0].src; expect(originalRef).toBe(sharedAssetId); expect(copiedRef).toMatch(/^ast_/); expect(copiedRef).not.toBe(sharedAssetId); expect(await resolvedText(originalRef)).toBe('original-bytes'); expect(await resolvedText(copiedRef)).toBe('copy-bytes'); expect(mocks.mediaRows.has(`${stageId}:${sharedAssetId}`)).toBe(true); expect(useMediaGenerationStore.getState().tasks[copiedRef]?.placeholderRef).toBeUndefined(); expect(useMediaGenerationStore.getState().tasks[sharedAssetId]).toMatchObject({ status: 'failed', error: 'retry me', retryCount: 0, }); expect(sourceStatuses.length).toBeGreaterThan(0); expect(sourceStatuses.every((status) => status === 'failed')).toBe(true); expect(mocks.beforeDocumentWork).toHaveBeenCalledTimes(1); await assertOwnership(); }); it('removes a failed fork from both key spaces after its retry succeeds', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: doneTask(sharedAssetId) } }); fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({ success: false, error: 'fork generation failed', errorCode: 'UPSTREAM_TIMEOUT', }), { headers: { 'Content-Type': 'application/json' } }, ), ); const target = { elementId: 'image-copy', sceneId: 'scene-copy', slideId: 'slide-copy', }; await retryMediaTask(sharedAssetId, target); expect(mocks.mediaRows.get(`${stageId}:image-copy`)).toMatchObject({ error: 'fork generation failed', }); expect(useMediaGenerationStore.getState().tasks['image-copy']?.status).toBe('failed'); serveImage('recovered-fork'); await retryMediaTask(sharedAssetId, target); const recoveredRef = mocks.document.scenes[1].content.canvas.elements[0].src; expect(recoveredRef).toMatch(/^ast_/); expect(mocks.mediaRows.has(`${stageId}:image-copy`)).toBe(false); expect(useMediaGenerationStore.getState().tasks['image-copy']).toBeUndefined(); expect(useMediaGenerationStore.getState().tasks[recoveredRef]).toMatchObject({ elementId: recoveredRef, status: 'done', }); const restored = buildRestoredMediaTasks(stageId, [ ...mocks.mediaRows.values(), ] as unknown as MediaFileRecord[]); expect(restored['image-copy']).toBeUndefined(); expect(restored[recoveredRef]?.status).toBe('done'); const reloaded = resolveSlideMediaState( mocks.document.scenes[1].content.canvas as unknown as Slide, stageId, restored, ); expect(reloaded.byElementId['image-copy']).toMatchObject({ task: { elementId: recoveredRef, status: 'done' }, resolution: { kind: 'url' }, }); }); it('restores a fork failure after its retry fails without an error code', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: doneTask(sharedAssetId) } }); fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({ success: false, error: 'fork generation failed', errorCode: 'UPSTREAM_TIMEOUT', }), { headers: { 'Content-Type': 'application/json' } }, ), ); const target = { elementId: 'image-copy', sceneId: 'scene-copy', slideId: 'slide-copy', }; await retryMediaTask(sharedAssetId, target); expect(mocks.mediaRows.get(`${stageId}:image-copy`)).toMatchObject({ error: 'fork generation failed', errorCode: 'UPSTREAM_TIMEOUT', }); fetchMock.mockRejectedValueOnce(new Error('unstructured retry failure')); await retryMediaTask(sharedAssetId, target); const restored = buildRestoredMediaTasks(stageId, [ ...mocks.mediaRows.values(), ] as unknown as MediaFileRecord[]); expect(restored['image-copy']).toMatchObject({ status: 'failed', error: 'fork generation failed', errorCode: 'UPSTREAM_TIMEOUT', }); const reloaded = resolveSlideMediaState( mocks.document.scenes[1].content.canvas as unknown as Slide, stageId, restored, { assetUrls: { [sharedAssetId]: 'blob:source-pool' } }, ); expect(reloaded.byElementId['image-copy']).toMatchObject({ task: { elementId: 'image-copy', status: 'failed', error: 'fork generation failed' }, resolution: { kind: 'url', url: 'blob:source-pool', retryable: true }, }); }); it('scopes a shared retry to the scene and slide when element ids recur', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); mocks.document.scenes[0].content.canvas.elements[0].id = 'repeated-image'; const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: doneTask(sharedAssetId) } }); serveImage('scoped-copy'); await retryMediaTask(sharedAssetId, { elementId: 'repeated-image', sceneId: 'scene-copy', slideId: 'slide-copy', }); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(sharedAssetId); expect(mocks.document.scenes[1].content.canvas.elements[0].src).not.toBe(sharedAssetId); expect(useMediaGenerationStore.getState().tasks[sharedAssetId]).toMatchObject({ status: 'done', objectUrl: 'blob:source', retryCount: 0, }); await assertOwnership(); }); it('keeps sharers on source bytes while a targeted fork is generating', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: doneTask(sharedAssetId) } }); let finishGeneration!: (response: Response) => void; const generation = new Promise((resolve) => { finishGeneration = resolve; }); fetchMock.mockImplementation(async (input: string) => { if (input === '/api/generate/image') return generation; if (input === '/api/proxy-media') { return new Response(new Blob(['fork-bytes'], { type: 'image/png' }), { status: 200 }); } throw new Error(`Unexpected fetch: ${input}`); }); const retrying = retryMediaTask(sharedAssetId, { elementId: 'image-copy', sceneId: 'scene-copy', slideId: 'slide-copy', }); await vi.waitFor(() => { expect(useMediaGenerationStore.getState().tasks['image-copy']?.status).toBe('generating'); }); const tasks = useMediaGenerationStore.getState().tasks; const source = resolveSlideMediaState( mocks.document.scenes[0].content.canvas as unknown as Slide, stageId, tasks, { assetUrls: { [sharedAssetId]: 'blob:source-pool' } }, ); const target = resolveSlideMediaState( mocks.document.scenes[1].content.canvas as unknown as Slide, stageId, tasks, { assetUrls: { [sharedAssetId]: 'blob:source-pool' } }, ); expect(tasks[sharedAssetId]).toMatchObject({ status: 'done', objectUrl: 'blob:source' }); expect(source.byElementId['image-1'].resolution).toEqual({ kind: 'url', url: 'blob:source-pool', }); expect(source.slide.elements[0]).toMatchObject({ src: 'blob:source-pool' }); expect(target.byElementId['image-copy'].resolution).toEqual({ kind: 'pending' }); expect(target.slide.elements[0]).toMatchObject({ src: '' }); finishGeneration( new Response( JSON.stringify({ success: true, result: { url: 'https://media.test/image', width: 1024, height: 576 }, }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ), ); await retrying; expect(useMediaGenerationStore.getState().tasks[sharedAssetId]).toMatchObject({ status: 'done', objectUrl: 'blob:source', retryCount: 0, }); }); it('forks a shared ref owned by a scene whiteboard despite the scene slide id', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const scene = mocks.document.scenes[0] as (typeof mocks.document.scenes)[number] & { whiteboards: Array<(typeof mocks.document.scenes)[number]['content']['canvas']>; }; scene.whiteboards = [ { ...structuredClone(mocks.document.scenes[0].content.canvas), id: 'scene-whiteboard', elements: [ { ...structuredClone(mocks.document.scenes[0].content.canvas.elements[0]), id: 'whiteboard-image', type: 'image', src: sharedAssetId, }, ], }, ]; mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: failedTask(sharedAssetId) } }); serveImage('whiteboard-copy'); await retryMediaTask(sharedAssetId, { elementId: 'whiteboard-image', sceneId: 'scene-1', slideId: 'slide-1', }); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(sharedAssetId); const savedScene = mocks.document.scenes[0] as typeof scene; expect(savedScene.whiteboards[0].elements[0].src).not.toBe(sharedAssetId); expect(useMediaGenerationStore.getState().tasks[sharedAssetId]?.status).toBe('failed'); }); it('forks a shared ref owned by the stage whiteboard from a scene context', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const stage = mocks.document.stage as typeof mocks.document.stage & { whiteboard: Array<(typeof mocks.document.scenes)[number]['content']['canvas']>; }; stage.whiteboard = [ { ...structuredClone(mocks.document.scenes[0].content.canvas), id: 'stage-whiteboard', elements: [ { ...structuredClone(mocks.document.scenes[0].content.canvas.elements[0]), id: 'stage-whiteboard-image', type: 'image', src: sharedAssetId, }, ], }, ]; mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: failedTask(sharedAssetId) } }); serveImage('stage-whiteboard-copy'); await retryMediaTask(sharedAssetId, { elementId: 'stage-whiteboard-image', sceneId: 'scene-1', slideId: 'stage-whiteboard', }); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(sharedAssetId); const savedStage = mocks.document.stage as typeof stage; expect(savedStage.whiteboard[0].elements[0].src).not.toBe(sharedAssetId); expect(useMediaGenerationStore.getState().tasks[sharedAssetId]?.status).toBe('failed'); }); it('does not fall back to a stage-whiteboard element id when the target slide misses', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const stage = mocks.document.stage as typeof mocks.document.stage & { whiteboard: Array<(typeof mocks.document.scenes)[number]['content']['canvas']>; }; stage.whiteboard = [ { ...structuredClone(mocks.document.scenes[0].content.canvas), id: 'stage-whiteboard', elements: [ { ...structuredClone(mocks.document.scenes[0].content.canvas.elements[0]), id: 'stage-whiteboard-image', type: 'image', src: sharedAssetId, }, ], }, ]; mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: failedTask(sharedAssetId) } }); serveImage('must-not-be-attached'); await retryMediaTask(sharedAssetId, { elementId: 'stage-whiteboard-image', sceneId: 'missing-scene', slideId: 'missing-slide', }); expect(stage.whiteboard[0].elements[0].src).toBe(sharedAssetId); expect(useMediaGenerationStore.getState().tasks[sharedAssetId]?.status).toBe('failed'); expect(useMediaGenerationStore.getState().tasks['stage-whiteboard-image']).toMatchObject({ status: 'failed', errorCode: 'TARGET_MISSING', }); }); it('builds a safe retry target for a non-slide scene', () => { expect(mediaRetryTarget('image-1', 'scene-1', { type: 'text', markdown: 'Notes' })).toEqual({ elementId: 'image-1', sceneId: 'scene-1', }); }); it('refuses to fork a shared ref without the owning slide instance', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.mediaRows.set(`${stageId}:${sharedAssetId}`, { id: `${stageId}:${sharedAssetId}`, blob: new Blob(['original-bytes'], { type: 'image/png' }), }); useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: failedTask(sharedAssetId) } }); await retryMediaTask(sharedAssetId, { elementId: mocks.document.scenes[0].content.canvas.elements[0].id, sceneId: mocks.document.scenes[0].id, }); expect(fetchMock).not.toHaveBeenCalled(); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(sharedAssetId); expect(mocks.document.scenes[1].content.canvas.elements[0].src).toBe(sharedAssetId); await assertOwnership(); }); it('rollback layer 4: protects first-commit bytes when later reconciliation fails', async () => { mocks.document = documentWithMedia(placeholder, 'image'); serveImage('committed-before-reconciliation-failure'); let documentWrites = 0; mocks.beforeDocumentWork.mockImplementation(() => { documentWrites += 1; if (documentWrites === 2) throw new Error('second reconciliation failed'); }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); const committedRef = mocks.document.scenes[0].content.canvas.elements[0].src; expect(committedRef).toMatch(/^ast_/); expect(await resolvedText(committedRef)).toBe('committed-before-reconciliation-failure'); await assertOwnership(); }); it('protects document-committed refs when active-stage reconciliation throws', async () => { mocks.document = documentWithMedia(placeholder, 'image'); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; mocks.stageSetState.mockImplementationOnce(() => { throw new Error('active-stage reconciliation failed'); }); serveImage('committed-before-active-stage-failure'); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); const committedRef = mocks.document.scenes[0].content.canvas.elements[0].src; expect(mocks.stageSetState).toHaveBeenCalledOnce(); expect(committedRef).toMatch(/^ast_/); expect(await resolvedText(committedRef)).toBe('committed-before-active-stage-failure'); expect(mocks.mediaRows.has(`${stageId}:${committedRef}`)).toBe(true); }); it('rollback layer 5: final rewrite failure leaves no task that can resurrect removed bytes', async () => { const pendingDocument = documentWithMedia(placeholder, 'image'); const lateScene = structuredClone(pendingDocument.scenes[0]); pendingDocument.scenes = []; mocks.document = pendingDocument; serveImage('removed-after-final-rewrite-failure'); const put = vi.spyOn(pool, 'put'); let documentWrites = 0; mocks.beforeDocumentWork.mockImplementation(() => { documentWrites += 1; if (documentWrites === 2) throw new Error('second reconciliation failed'); }); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); const [removedRef] = await Promise.all(put.mock.results.map((result) => result.value)); expect(await pool.resolve(removedRef)).toBeNull(); expect(mocks.mediaRows.has(`${stageId}:${removedRef}`)).toBe(false); expect(useMediaGenerationStore.getState().tasks).toEqual({ [placeholder]: expect.objectContaining({ elementId: placeholder, status: 'failed', error: 'second reconciliation failed', }), }); const reconciled = reconcileCompletedMediaForScene( lateScene as unknown as Scene, pendingDocument.stage as unknown as Stage, useMediaGenerationStore.getState().tasks, ); expect(reconciled.scene.content.type).toBe('slide'); if (reconciled.scene.content.type !== 'slide') throw new Error('Expected a slide scene'); const restoredElement = reconciled.scene.content.canvas.elements[0]; expect(restoredElement.type).toBe('image'); if (restoredElement.type !== 'image') throw new Error('Expected an image element'); expect(restoredElement.src).toBe(placeholder); }); it('marks the targeted fork failed without mutating its shared source task', async () => { const sharedAssetId = await pool.put(new Blob(['original-bytes'], { type: 'image/png' })); mocks.document = documentWithMedia(sharedAssetId, 'image'); const copy = structuredClone(mocks.document.scenes[0]); copy.id = 'scene-copy'; copy.order = 2; copy.content.canvas.id = 'slide-copy'; copy.content.canvas.elements[0].id = 'image-copy'; mocks.document.scenes.push(copy); mocks.stageState = { stage: structuredClone(mocks.document.stage), scenes: structuredClone(mocks.document.scenes), }; useMediaGenerationStore.setState({ tasks: { [sharedAssetId]: doneTask(sharedAssetId) } }); const sourceStatuses: Array = []; const unsubscribe = useMediaGenerationStore.subscribe((state) => { sourceStatuses.push(state.tasks[sharedAssetId]?.status); }); fetchMock.mockRejectedValue(new Error('fork generation failed')); await retryMediaTask(sharedAssetId, { elementId: 'image-copy', sceneId: 'scene-copy', slideId: 'slide-copy', }); unsubscribe(); expect(useMediaGenerationStore.getState().tasks).toEqual({ [sharedAssetId]: expect.objectContaining({ elementId: sharedAssetId, status: 'done', objectUrl: 'blob:source', retryCount: 0, }), 'image-copy': expect.objectContaining({ elementId: 'image-copy', status: 'failed', error: 'fork generation failed', retryCount: 1, }), }); expect(sourceStatuses.length).toBeGreaterThan(0); expect(sourceStatuses.every((status) => status === 'done')).toBe(true); const failedTarget = resolveSlideMediaState( mocks.document.scenes[1].content.canvas as unknown as Slide, stageId, useMediaGenerationStore.getState().tasks, { assetUrls: { [sharedAssetId]: 'blob:source-pool' } }, ); expect(failedTarget.byElementId['image-copy']).toMatchObject({ task: { elementId: 'image-copy', status: 'failed', error: 'fork generation failed' }, resolution: { kind: 'url', url: 'blob:source-pool', retryable: true }, }); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toBe(sharedAssetId); expect(mocks.document.scenes[1].content.canvas.elements[0].src).toBe(sharedAssetId); }); it('falls back to task-based resume filtering when legacy document access is locked', async () => { mocks.document = documentWithMedia(placeholder, 'image'); mocks.accessDocument.mockRejectedValueOnce(new Error('legacy lock unavailable')); serveImage('generated-after-read-failure'); await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); expect(fetchMock).toHaveBeenCalledWith('/api/generate/image', expect.any(Object)); expect(mocks.document.scenes[0].content.canvas.elements[0].src).toMatch(/^ast_/); await assertOwnership(); }); it('persists a retryable failure when pool replacement succeeds before Dexie fails', async () => { const assetId = await pool.put(new Blob(['image-old'], { type: 'image/png' })); mocks.document = documentWithMedia(assetId, 'image'); useMediaGenerationStore.setState({ tasks: { [assetId]: failedTask(assetId) } }); serveImage('pool-new-dexie-missed'); mocks.mediaPut.mockRejectedValueOnce(new Error('Dexie write failed')); mocks.mediaGet.mockResolvedValue({ id: `${stageId}:${assetId}`, stageId, type: 'image', blob: new Blob(['image-old'], { type: 'image/png' }), mimeType: 'image/png', size: 9, prompt: 'A new image', params: '{}', createdAt: 1, }); const target = { elementId: 'image-1', sceneId: 'scene-1', slideId: 'slide-1', }; await retryMediaTask(assetId, target); expect(await resolvedText(assetId)).toBe('pool-new-dexie-missed'); expect(useMediaGenerationStore.getState().tasks[assetId]).toMatchObject({ status: 'failed', errorCode: 'MEDIA_COMPATIBILITY_STORE_LAGGED', error: expect.stringContaining('compatibility media store lagged'), }); expect(mocks.mediaPut).not.toHaveBeenCalledWith( expect.objectContaining({ errorCode: 'MEDIA_COMPATIBILITY_STORE_LAGGED' }), ); serveImage('stores-converged'); await retryMediaTask(assetId, target); expect(await resolvedText(assetId)).toBe('stores-converged'); expect(useMediaGenerationStore.getState().tasks[assetId]?.status).toBe('done'); expect(mocks.mediaPut).toHaveBeenLastCalledWith( expect.objectContaining({ id: `${stageId}:${assetId}`, blob: expect.any(Blob) }), ); }); it('drops a document rewrite fenced by a stage deletion epoch change', async () => { mocks.document = documentWithMedia(placeholder, 'image'); const before = structuredClone(mocks.document); serveImage(); mocks.beforeDocumentWork.mockImplementationOnce(() => markStageDeleted(stageId)); try { await generateMediaForOutlines( [ { id: 'outline-1', type: 'slide', title: 'Scene', description: 'Scene', keyPoints: ['image'], order: 1, mediaGenerations: [{ type: 'image', prompt: 'A new image', elementId: placeholder }], }, ], stageId, ); expect(mocks.document).toEqual(before); } finally { unmarkStageDeleted(stageId); } }); });