import { describe, expect, it } from 'vitest'; import { beginResourceCanvasResourceEditSubmission, type PendingLocalProjectResourceEdit, pendingResourceEditKindLabel, resourceCanvasResourceEditPhaseLabel, type ResourceCanvasResourceEditSubmission, resourceCanvasResourceEditSubmissionBlocksResource, resourceCanvasResourceEditTaskElapsedMillis, resourceCanvasResourceEditTaskIsTerminal, resourceCanvasResourceEditTasks, resourceCanvasResourceEditTaskStatusFromPhase, settleResourceCanvasResourceEditSubmission, } from '../src/features/resource-canvas/resourceCanvasResourceEditTaskModel'; function pendingEdit( overrides: Partial = {}, ): PendingLocalProjectResourceEdit { return { operationId: 'op-1', editKind: 'image-reference', sourceResourceId: 'asset:source-art', assetName: 'source-art-编辑版', backgroundMode: null, screenColor: null, phase: 'accepted', createdAt: 1_700_000_000_000, ...overrides, }; } function liveSubmission( overrides: Partial = {}, ): ResourceCanvasResourceEditSubmission { return { operationId: 'op-1', prompt: '把嘴改小一点', assetName: 'source-art-编辑版', actionLabel: '快速编辑', resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, status: 'running', error: null, assetId: null, finishedAtMillis: null, ...overrides, }; } describe('资源派生任务:阶段映射', () => { it('按原生账本阶段给出面板状态', () => { expect(resourceCanvasResourceEditTaskStatusFromPhase('prepared')).toBe( 'queued', ); expect(resourceCanvasResourceEditTaskStatusFromPhase('accepted')).toBe( 'running', ); expect( resourceCanvasResourceEditTaskStatusFromPhase('remote-completed'), ).toBe('running'); expect( resourceCanvasResourceEditTaskStatusFromPhase('media-downloaded'), ).toBe('running'); expect(resourceCanvasResourceEditTaskStatusFromPhase('committed')).toBe( 'completed', ); expect(resourceCanvasResourceEditTaskStatusFromPhase('archived')).toBe( 'completed', ); expect(resourceCanvasResourceEditTaskStatusFromPhase('remote-failed')).toBe( 'failed', ); expect( resourceCanvasResourceEditTaskStatusFromPhase('reconciliation-required'), ).toBe('failed'); }); it('阶段文案与恢复入口同源,未知阶段原样透出', () => { expect(resourceCanvasResourceEditPhaseLabel('remote-completed')).toBe( '远程已完成', ); expect(resourceCanvasResourceEditPhaseLabel('committed')).toBe( '已写入项目', ); // 前端还没跟上的新档:给出阶段名,便于排障时一眼认出。 expect(resourceCanvasResourceEditPhaseLabel('pending-review')).toBe( 'pending-review', ); }); it('入口文案覆盖派生链路的几种 kind', () => { expect(pendingResourceEditKindLabel(pendingEdit())).toBe('快速编辑'); expect( pendingResourceEditKindLabel( pendingEdit({ editKind: 'character-animation' }), ), ).toBe('生成动画'); expect( pendingResourceEditKindLabel( pendingEdit({ editKind: 'background-music' }), ), ).toBe('生成背景音乐'); expect( pendingResourceEditKindLabel( pendingEdit({ editKind: 'background-removal', backgroundMode: 'flat', screenColor: 'auto', }), ), ).toBe('图片抠图 · 平面背景 · 自动背景色'); }); }); describe('资源派生任务:本地提交记录', () => { it('同一 operationId 只留一条,收口后按结果改写', () => { const begun = beginResourceCanvasResourceEditSubmission([], { operationId: 'op-1', assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, }); expect(begun).toHaveLength(1); expect(begun[0]?.status).toBe('running'); const rebegun = beginResourceCanvasResourceEditSubmission(begun, { operationId: 'op-1', assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_001_000, }); expect(rebegun).toHaveLength(1); const settled = settleResourceCanvasResourceEditSubmission( rebegun, 'op-1', { status: 'completed', assetId: 'edit-1', finishedAtMillis: 1_700_000_120_000, }, ); expect(settled[0]).toMatchObject({ status: 'completed', assetId: 'edit-1', error: null, }); }); it('失败收口带原因,未完成草稿的提交门随之放行', () => { const live = beginResourceCanvasResourceEditSubmission([], { operationId: 'op-1', assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, }); expect( resourceCanvasResourceEditSubmissionBlocksResource( live, 'assets/source-art.png', ), ).toBe(true); // 别的素材不受影响,其它入口(没有草稿键)也不受这条判据牵制。 expect( resourceCanvasResourceEditSubmissionBlocksResource( live, 'assets/other.png', ), ).toBe(false); const failed = settleResourceCanvasResourceEditSubmission(live, 'op-1', { status: 'failed', error: '登录已失效,请重新登录', finishedAtMillis: 1_700_000_090_000, }); expect( resourceCanvasResourceEditSubmissionBlocksResource( failed, 'assets/source-art.png', ), ).toBe(false); }); }); describe('资源派生任务:两套账本合并', () => { it('在途期间以后端阶段为准,收口后以本地结果为准', () => { const submissions = [liveSubmission()]; const inFlight = resourceCanvasResourceEditTasks({ submissions, pendingEdits: [pendingEdit({ phase: 'media-downloaded' })], }); expect(inFlight).toHaveLength(1); expect(inFlight[0]).toMatchObject({ operationId: 'op-1', status: 'running', phaseDetail: '结果已下载', prompt: '把嘴改小一点', restored: false, }); const settled = resourceCanvasResourceEditTasks({ submissions: settleResourceCanvasResourceEditSubmission( submissions, 'op-1', { status: 'completed', assetId: 'edit-1', finishedAtMillis: 1_700_000_120_000, }, ), pendingEdits: [], }); expect(settled[0]).toMatchObject({ status: 'completed', phaseDetail: '已产出新素材', assetId: 'edit-1', }); expect(resourceCanvasResourceEditTaskIsTerminal(settled[0]!)).toBe(true); }); it('原生有、本地没有的记录恢复成一条待办,但不占用已完成分栏', () => { const tasks = resourceCanvasResourceEditTasks({ submissions: [], pendingEdits: [ pendingEdit({ operationId: 'op-9', phase: 'remote-failed' }), ], }); expect(tasks).toHaveLength(1); expect(tasks[0]).toMatchObject({ operationId: 'op-9', status: 'failed', restored: true, actionLabel: '快速编辑', }); expect(tasks[0]?.error).toContain('远程明确失败'); // 恢复记录没有结束时间:不拿创建时间冒充耗时。 expect( resourceCanvasResourceEditTaskElapsedMillis(tasks[0]!, 1_700_000_500_000), ).toBeNull(); }); it('后端读回非数组时不炸,只按本地提交记录渲染', () => { const tasks = resourceCanvasResourceEditTasks({ submissions: [liveSubmission()], pendingEdits: undefined, }); expect(tasks).toHaveLength(1); expect(tasks[0]?.phaseDetail).toBe('已提交,等待远端受理'); }); it('新提交的排在前面', () => { const tasks = resourceCanvasResourceEditTasks({ submissions: [ liveSubmission({ operationId: 'op-new', createdAtMillis: 1_700_000_200_000, }), ], pendingEdits: [ pendingEdit({ operationId: 'op-old', createdAt: 1_700_000_000_000 }), ], }); expect(tasks.map((task) => task.operationId)).toEqual(['op-new', 'op-old']); }); });