Files
Genarrative/apps/ai-game-creator-shell/tests/projectNamingGenerationTaskRow.test.tsx
T
suzmii a554b0c7ca 把 AI 项目命名接进「生成任务」体系:账本 v2 任务类型、命名任务入队与推进、面板命名行
- 账本 v2:记录新增 taskType(asset-generation / project-naming,缺省 asset-generation,旧壳按素材任务读回),kind 改为可选;schema 升 agc-asset-generation-task.v2,读取同时接受 v1 与 v2。
- 新增 enqueue_local_project_naming_task / update_local_project_naming_task:命名任务入队时登记 live、终态写盘后摘除(写盘失败只回滚本轮插入),中断残留按命名口径收口为 failed(文案不含「目标素材」)。
- 首页命名链路改为推进这条任务:入队 → 命名中 → 已完成(已应用 AI 名称 / 建议名与现状一致 / 用户已手动改名而跳过)或失败(无可用名称 / 自动改名失败,均保留兜底名);账本只作展示旁路,推进失败只写诊断日志。
- 前端模型与面板支持命名行:按 taskType 把命名记录从素材任务分支剔除,命名行固定展示名「AI 项目命名」、只在终态显示结论、不渲染缩略图/提示词/定位动作;命令名写字面量以满足 check-config 静态枚举门禁。
- 工作台订阅账本变更事件做一次受控快照读取刷新命名行(不新增固定频率轮询);另修:命名行的 useLiveNow 收进独立组件,避免行种类变化导致 hooks 数量变化。
- 测试:homeProjectNamingAsync 12 例(新增入队→命名中→终态推进序列断言)、projectNamingGenerationTaskRow 3 例、appSurface 建项后命名行可见 + 事件刷新出终态结论 + 标题/列表同步;Rust asset_generation_task 22 例(含 naming_task_stays_in_flight_across_ledger_reads_until_terminal、legacy_v1_ledger_reads_back_*)。
- 文档:decision-log 改写为「生成任务体系里的后台任务」、pitfalls 新增 taskType/live 集合条目、实施计划专题补任务接入口径。
- 门禁:AGC 全量 1925 passed / 17 skipped;cargo 定向 22 + 6;npm run agc:typecheck(含 check:tests:types 与 check-config);cargo fmt --check;check:encoding;git diff --check。
2026-10-04 16:59:59 +08:00

160 lines
5.4 KiB
TypeScript

/** @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ResourceCanvasProjectNamingTask } from '../src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel';
import { ResourceCanvasAssetGenerationTasksPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView';
import { cleanup, render, screen, within } from './appSurface/harness';
/**
* 「生成任务」侧栏里的「AI 项目命名」行。
*
* 它与素材生成共用同一份账本、同一个事件与同一套卡片样式,但有两条自己的口径:
* 1. **展示名固定**为「AI 项目命名」:不渲染素材缩略图 / 提示词 / 派发与「定位到素材」;
* 2. **结论只在终态渲染**:进行中由状态徽章表达(排队中 / 命名中),终态才显示后端给的结论
* (已应用 AI 名称 / 已跳过 / 失败保留兜底名)。
*/
afterEach(cleanup);
function namingTask(
overrides: Partial<ResourceCanvasProjectNamingTask> = {},
): ResourceCanvasProjectNamingTask {
return {
taskId: 'project-naming:local-project-1',
projectId: 'local-project-1',
status: 'running',
phaseDetail: '正在等待 AI 命名结果。',
createdAtMillis: 1000,
startedAtMillis: 1200,
finishedAtMillis: null,
error: null,
...overrides,
};
}
function renderPanel(tasks: readonly ResourceCanvasProjectNamingTask[]) {
render(
<ResourceCanvasAssetGenerationTasksPanelView
tasks={[]}
namingTasks={tasks}
open
onToggleOpen={vi.fn()}
onFocusTask={vi.fn()}
/>,
);
}
describe('生成任务侧栏里的 AI 项目命名行', () => {
it('进行中只给状态徽章,不渲染结论、缩略图、提示词或定位动作', () => {
renderPanel([namingTask()]);
const row = document.querySelector(
'[data-task-source="project-naming"]',
) as HTMLElement | null;
expect(row).not.toBeNull();
expect(within(row!).getByText('AI 项目命名')).not.toBeNull();
expect(within(row!).getByText('命名中')).not.toBeNull();
// 进行中不显示后端阶段文案;也没有素材任务的缩略图/提示词/派发按钮。
expect(within(row!).queryByText('正在等待 AI 命名结果。')).toBeNull();
expect(within(row!).queryByRole('button', { name: /定位素材/ })).toBeNull();
expect(row!.querySelector('img')).toBeNull();
});
it('终态渲染可自解释的结论(已应用 / 已跳过 / 失败保留兜底名)', () => {
const { unmount } = render(
<ResourceCanvasAssetGenerationTasksPanelView
tasks={[]}
namingTasks={[
namingTask({
status: 'completed',
phaseDetail: '已应用 AI 名称「星河远征」。',
finishedAtMillis: 2200,
}),
]}
open
onToggleOpen={vi.fn()}
onFocusTask={vi.fn()}
/>,
);
const appliedRow = document.querySelector(
'[data-task-source="project-naming"]',
) as HTMLElement | null;
expect(within(appliedRow!).getByText('已完成')).not.toBeNull();
expect(
within(appliedRow!).getByText('已应用 AI 名称「星河远征」。'),
).not.toBeNull();
unmount();
render(
<ResourceCanvasAssetGenerationTasksPanelView
tasks={[]}
namingTasks={[
namingTask({
status: 'completed',
phaseDetail:
'当前名称已不是本次创建的兜底名(用户已手动改名),已跳过 AI 命名。',
finishedAtMillis: 2200,
}),
]}
open
onToggleOpen={vi.fn()}
onFocusTask={vi.fn()}
/>,
);
const skippedRow = document.querySelector(
'[data-task-source="project-naming"]',
) as HTMLElement | null;
expect(
within(skippedRow!).getByText(
'当前名称已不是本次创建的兜底名(用户已手动改名),已跳过 AI 命名。',
),
).not.toBeNull();
});
it('失败行显示失败徽章与原因,并计入在途计数', () => {
const { unmount } = render(
<ResourceCanvasAssetGenerationTasksPanelView
tasks={[]}
namingTasks={[
namingTask({
status: 'failed',
phaseDetail: 'AI 命名请求失败或没有返回可用名称,已保留兜底名。',
finishedAtMillis: 2200,
error: '自动项目命名超时(超过 15 秒)',
}),
]}
open
onToggleOpen={vi.fn()}
onFocusTask={vi.fn()}
/>,
);
const row = document.querySelector(
'[data-task-source="project-naming"]',
) as HTMLElement | null;
expect(within(row!).getByText('失败')).not.toBeNull();
expect(
within(row!).getByText(
'AI 命名请求失败或没有返回可用名称,已保留兜底名。',
),
).not.toBeNull();
expect(within(row!).getByRole('alert').textContent).toContain(
'自动项目命名超时',
);
unmount();
// 收起态:在途的命名任务要出现在右上角开关的计数里(与素材任务同一个计数)。
render(
<ResourceCanvasAssetGenerationTasksPanelView
tasks={[]}
namingTasks={[namingTask()]}
open={false}
onToggleOpen={vi.fn()}
onFocusTask={vi.fn()}
/>,
);
expect(screen.getByLabelText('生成任务').textContent).toContain(
'生成任务 · 1',
);
});
});