Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/project-preview/run-history.suite.ts
T
suzmii 53afc85319 修正 AGC 测试按新的 Lexical 聊天输入区驱动与断言
- appSurface/harness 补齐三处 jsdom 缺口:ClipboardEvent、DragEvent、Range.prototype.getBoundingClientRect;否则 Lexical 的粘贴通路直接抛 ReferenceError / TypeError
- appSurface/harness 的 submitChat 改为 async:全选并让编辑器吸收选区、清空、走产品真实 paste 通路写入,再让出一帧让 React 追平 draft,最后点发送
- 新增 composerText / composerValue / composerDisabled / setComposerText 助手,按原生控件与 Lexical contenteditable 两种 DOM 口径读写输入区
- 15 个测试文件中 116 处 toHaveProperty('value', …) 断言等义改写为 await composerText() / await composerValue();1 处 placeholder 断言改查输入区占位文案;2 处 disabled 断言改用 composerDisabled(同时覆盖 data-disabled 与 contenteditable=false);9 处 fireEvent.change 写入改用 setComposerText
- 328 处 submitChat 调用补 await,9 个非 async 用例补 async
- Godot 输入区键盘语义用例按 Lexical 实际行为改写:Shift+Enter 与组合态 Enter 由编辑器消化并插入换行、不发送、草稿保留
- 未改动 src 下任何产品代码,未放宽或删除断言
2026-09-10 11:46:02 +08:00

730 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
composerText,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
it,
renderAppAt,
screen,
submitChat,
vi,
waitFor,
within,
} from '../harness';
export function registerProjectRunHistoryTests() {
it('loads recent run history in the developer project window', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const makeTrace = (
runId: string,
status: string,
passes: number,
stopReason: string,
lifecycleStatus?: string,
updatedAt = passes,
) =>
({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId,
commandId: 'game.generate_draft',
status,
lifecycleStatus,
passes,
maxPasses: 3,
toolCallCount: 0,
maxToolCalls: 128,
stopReason,
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'preview',
error: null,
updatedAt,
}) satisfies GameCreationAgentRunTrace;
const traces = new Map([
[
'.agent/run.latest.json',
makeTrace('run-current', 'running', 1, 'planning'),
],
[
'.agent/runs/2026-07-02-new.json',
makeTrace('run-new', 'passed', 2, 'evaluator-passed', 'done', 600),
],
[
'.agent/runs/2026-07-01-old.json',
makeTrace(
'run-old',
'failed',
3,
'max-passes-exhausted',
undefined,
700,
),
],
[
'.agent/runs/2026-06-30-mid.json',
makeTrace('run-mid', 'running', 1, 'planning', undefined, 500),
],
[
'.agent/runs/2026-06-29-four.json',
makeTrace(
'run-four',
'failed',
3,
'max-passes-exhausted',
undefined,
400,
),
],
[
'.agent/runs/2026-06-28-five.json',
makeTrace('run-five', 'passed', 1, 'preview', 'done', 300),
],
[
'.agent/runs/2026-06-27-hidden.json',
makeTrace(
'run-hidden',
'failed',
3,
'max-passes-exhausted',
undefined,
200,
),
],
[
'.agent/runs/2026-06-26-hidden.json',
makeTrace(
'run-older-hidden',
'failed',
3,
'max-passes-exhausted',
undefined,
100,
),
],
]);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/2026-07-02-new.json',
kind: 'file',
size: 120,
},
{
path: '.agent/runs/2026-07-01-old.json',
kind: 'file',
size: 110,
},
{
path: '.agent/runs/2026-06-30-mid.json',
kind: 'file',
size: 100,
},
{
path: '.agent/runs/2026-06-29-four.json',
kind: 'file',
size: 90,
},
{
path: '.agent/runs/2026-06-28-five.json',
kind: 'file',
size: 80,
},
{
path: '.agent/runs/2026-06-27-hidden.json',
kind: 'file',
size: 70,
},
{
path: '.agent/runs/2026-06-26-hidden.json',
kind: 'file',
size: 60,
},
],
};
}
if (command === 'read_local_project_file') {
const trace = traces.get(String(args?.relativePath ?? ''));
if (!trace) {
throw new Error(
`missing trace ${String(args?.relativePath ?? '')}`,
);
}
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
const runHistory = await screen.findByLabelText('Agent run history');
expect(screen.getByText(/run-new/)).not.toBeNull();
expect(
screen.getByText(/passed \/ done · 2\/3 轮 · evaluator-passed/),
).not.toBeNull();
const oldRunButton = within(runHistory).getByText(
'run-old · failed · 3/3 轮 · max-passes-exhausted · updated: 700 · .agent/runs/2026-07-01-old.json · 110B',
);
expect(oldRunButton).not.toBeNull();
expect(screen.getByText(/updated: 700/)).not.toBeNull();
expect(
screen.getByText(/\.agent\/runs\/2026-07-01-old\.json/),
).not.toBeNull();
expect(screen.queryByText('还有 2 个历史 run')).toBeNull();
expect(screen.getByText(/run-hidden/)).not.toBeNull();
expect(runHistory.querySelector('button')?.textContent).toContain(
'run-old',
);
expect(runHistory.querySelector('[aria-current="true"]')).toBeNull();
fireEvent.click(oldRunButton.closest('button') as Element);
expect(
await screen.findByText('run: failed · 3/3 轮 · max-passes-exhausted'),
).not.toBeNull();
expect(oldRunButton.closest('button')?.getAttribute('aria-current')).toBe(
'true',
);
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/2026-07-01-old.json',
commandId: 'agent.trace_read',
});
});
it('uses run history for agent status cards when the latest trace is missing', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-history-only',
commandId: 'game.generate_draft',
status: 'running',
lifecycleStatus: 'running',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'planning',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [
{
pass: 1,
agent: 'Generator',
phase: 'generate',
taskId: 'code-prototype',
group: 'code',
role: 'Code',
status: 'running',
inputPaths: ['.agent/spec.md'],
outputPaths: ['game/index.html'],
summary: 'Generator 历史草案生成',
toolCalls: [],
},
],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: ['code-prototype'],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'generator',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/run-history-only.json',
kind: 'file',
size: 120,
},
],
};
}
if (command === 'read_local_project_file') {
const relativePath = String(args?.relativePath ?? '');
if (relativePath === '.agent/run.latest.json') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
return {
path: relativePath,
absolutePath: `/tmp/authorized-game/${relativePath}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
const runHistory = await screen.findByLabelText('Agent run history');
expect(within(runHistory).getByText(/run-history-only/)).not.toBeNull();
expect(screen.getByText('Generator 历史草案生成')).not.toBeNull();
});
it('keeps run history out of the regular project chat window', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-hidden-from-chat',
commandId: 'game.generate_draft',
status: 'passed',
lifecycleStatus: 'done',
passes: 1,
maxPasses: 3,
toolCallCount: 0,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'preview',
error: null,
updatedAt: 1,
} satisfies GameCreationAgentRunTrace;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/run-hidden-from-chat.json',
kind: 'file',
size: 100,
},
],
};
}
if (command === 'read_local_project_file') {
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('list_local_project_files', {
projectPath: '/tmp/authorized-game',
});
});
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/run-hidden-from-chat.json',
commandId: 'agent.trace_read',
});
});
expect(screen.queryByLabelText('最近 run')).toBeNull();
expect(screen.queryByText(/run-hidden-from-chat/)).toBeNull();
const runReadCountBeforeRunsCommand = invoke.mock.calls.filter(
([command]) => command === 'read_local_project_file',
).length;
await submitChat('/runs');
expect(await screen.findByText(/Run 历史读取命令:/)).not.toBeNull();
expect(
screen.getByText(
/run-hidden-from-chat .* \.agent\/runs\/run-hidden-from-chat\.json .*\/read \.agent\/runs\/run-hidden-from-chat\.json/,
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '读取首个历史 Run' }));
expect(await composerText()).toBe(
'/read .agent/runs/run-hidden-from-chat.json',
);
expect(
invoke.mock.calls.filter(
([command]) => command === 'read_local_project_file',
),
).toHaveLength(runReadCountBeforeRunsCommand);
});
it('confirms before opening a run history trace when policy requires it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const makeTrace = (runId: string, status: string, updatedAt: number) =>
({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId,
commandId: 'game.generate_draft',
status,
lifecycleStatus: status === 'passed' ? 'done' : undefined,
passes: 1,
maxPasses: 3,
toolCallCount: 0,
maxToolCalls: 128,
stopReason: status === 'passed' ? 'evaluator-passed' : 'planning',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'preview',
error: null,
updatedAt,
}) satisfies GameCreationAgentRunTrace;
const traces = new Map([
['.agent/run.latest.json', makeTrace('run-current', 'running', 500)],
['.agent/runs/2026-07-01-old.json', makeTrace('run-old', 'passed', 400)],
]);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.trace_read'],
},
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/2026-07-01-old.json',
kind: 'file',
size: 110,
},
],
};
}
if (command === 'read_local_project_file') {
const trace = traces.get(String(args?.relativePath ?? ''));
if (!trace) {
throw new Error(
`missing trace ${String(args?.relativePath ?? '')}`,
);
}
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText(/run-old/)).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByText(/run-old/).closest('button') as Element);
expect(await screen.findByText('agent.trace_read')).not.toBeNull();
expect(
screen.getByText(
'读取 /tmp/authorized-game 的 .agent/runs/2026-07-01-old.json',
),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/2026-07-01-old.json',
commandId: 'agent.trace_read',
});
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/2026-07-01-old.json',
commandId: 'agent.trace_read',
});
});
});
it('shows more run history entries on demand', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const makeTrace = (runId: string, updatedAt: number) =>
({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId,
commandId: 'game.generate_draft',
status: 'passed',
lifecycleStatus: 'done',
passes: 1,
maxPasses: 3,
toolCallCount: 0,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'preview',
error: null,
updatedAt,
}) satisfies GameCreationAgentRunTrace;
const runFiles = Array.from({ length: 22 }, (_, index) => {
const number = String(index + 1).padStart(2, '0');
return {
path: `.agent/runs/run-${number}.json`,
kind: 'file',
size: 100 + index,
};
});
const traces = new Map<string, GameCreationAgentRunTrace>([
['.agent/run.latest.json', makeTrace('run-current', 1000)],
...runFiles.map(
(file, index) =>
[
file.path,
makeTrace(`run-${String(index + 1).padStart(2, '0')}`, index + 1),
] as const,
),
]);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: runFiles,
};
}
if (command === 'read_local_project_file') {
const trace = traces.get(String(args?.relativePath ?? ''));
if (!trace) {
throw new Error(
`missing trace ${String(args?.relativePath ?? '')}`,
);
}
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
const runHistory = await screen.findByLabelText('Agent run history');
expect(screen.getByText(/run-03/)).not.toBeNull();
expect(screen.queryByText(/run-02/)).toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/run-02.json',
});
expect(
screen.getByRole('button', {
name: '显示更多 · 还有 2 个历史 run',
}),
).not.toBeNull();
fireEvent.scroll(runHistory);
expect(await screen.findByText(/run-02/)).not.toBeNull();
expect(screen.getByText(/run-01/)).not.toBeNull();
expect(runHistory.querySelector('button')?.textContent).toContain('run-22');
expect(
screen.queryByRole('button', {
name: /显示更多/,
}),
).toBeNull();
});
}