Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasResourceEditTasks.test.ts
T
suzmii 2a2e4c1cea 按 review 修:动画在途拦截、样式守卫扩到三面板、窄屏差异显式放行、用例不再依赖 cwd
- 在途拦截补齐生成动画:提交记录的「属于哪张资源」从 draftKey 拆成 resourcePath,
  `resourceCanvasResourceEditSubmissionBlocksResource` 被快速编辑与生成动画共用;
  此前动画这条键对不上、拦不住,重开面板再提交会复用同一 operationId 卡在原生锁上,
  或(改了提示词)直接多出一笔付费生成
- 样式缺口守卫扩到三块面板(动画面板 / 快速编辑面板 / 生图面板),判据改为按
  「选择器分支 + 声明」逐条比对;两个面板的实测缺口一并照抄补上(快速编辑面板的
  `overflow: auto → visible` 与窄屏底栏两条按宿主差异显式登记,不静默漂移)
- 窄屏只搬动画面板那一支:网页端 ≤760px 会把快速编辑浮层也变成整宽底栏,AGC 的
  快速编辑是「居中贴资源卡」的绝对定位,跟随它会压住栏目工具栏与任务侧栏
- 用例路径不再依赖 cwd:新增 tests/repoPath.ts(按 import.meta.url 反推仓库根),
  19 个用例文件的 `resolve(process.cwd(), …)` 统一改用它;顺手修掉
  `HOST_STYLE_SHEETS.filter(existsSync)` 把仓库相对路径交给 cwd 解析的假红
- 清掉两处死代码:不可达的 `quickEditPanel.status === 'generating'` 判据与
  未被使用的 prune 导出
2026-09-21 01:23:35 +08:00

273 lines
8.4 KiB
TypeScript

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> = {},
): 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> = {},
): 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']);
});
});