53afc85319
- 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 下任何产品代码,未放宽或删除断言
286 lines
9.8 KiB
TypeScript
286 lines
9.8 KiB
TypeScript
import {
|
|
composerText,
|
|
createGameCreationAppManifest,
|
|
createGameCreationAppSeedTasks,
|
|
emptyProjectPolicy,
|
|
expect,
|
|
fireEvent,
|
|
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
type GameCreationAgentRunTrace,
|
|
it,
|
|
renderAppAt,
|
|
screen,
|
|
vi,
|
|
waitFor,
|
|
within,
|
|
} from '../harness';
|
|
|
|
export function registerProjectRunStatusTests() {
|
|
it('shows and refreshes the current project run status in the main window header', async () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'local-project-draft',
|
|
'未命名游戏原型',
|
|
);
|
|
let runReadCount = 0;
|
|
const makeTrace = (
|
|
status: string,
|
|
passes: number,
|
|
stopReason: string,
|
|
lifecycleStatus?: string,
|
|
) =>
|
|
({
|
|
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
runId: 'run-header-status',
|
|
commandId: 'game.generate_draft',
|
|
status,
|
|
lifecycleStatus,
|
|
passes,
|
|
maxPasses: 3,
|
|
toolCallCount: 0,
|
|
maxToolCalls: 128,
|
|
stopReason,
|
|
goal: '做一个厨房弹幕游戏',
|
|
coordination: 'Planner -> Generator',
|
|
steps: [],
|
|
artifacts: Array.from({ length: 9 }, (_, index) => ({
|
|
path: `exports/artifact-${index + 1}.json`,
|
|
sizeBytes: index + 1,
|
|
checksum: `fnv1a64:artifact-${index + 1}`,
|
|
})),
|
|
taskGraph: {
|
|
goal: '做一个厨房弹幕游戏',
|
|
readyTaskIds: ['preview-playtest'],
|
|
activeTaskIds: ['code-prototype'],
|
|
carriedTaskIds: ['design-director'],
|
|
repairFocus: [],
|
|
repairRoutes: [
|
|
{
|
|
issue: '缺少输入监听',
|
|
taskIds: ['code-prototype', 'quality-review'],
|
|
reason: 'code-runtime',
|
|
},
|
|
],
|
|
tasks: createGameCreationAppSeedTasks(),
|
|
},
|
|
passPlans: [
|
|
{
|
|
pass: 1,
|
|
mode: 'repair',
|
|
summary: '继续程序返工',
|
|
activeTaskIds: ['code-prototype', 'quality-review'],
|
|
carriedTaskIds: ['design-director'],
|
|
dependencyWaves: [['code-prototype'], ['quality-review']],
|
|
repairFocus: ['缺少输入监听'],
|
|
repairRoutes: [
|
|
{
|
|
issue: '缺少输入监听',
|
|
taskIds: ['code-prototype', 'quality-review'],
|
|
reason: 'code-runtime',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
nextStep: 'preview',
|
|
error: null,
|
|
updatedAt: runReadCount,
|
|
}) satisfies GameCreationAgentRunTrace;
|
|
const invoke = vi.fn(
|
|
async (command: string, args?: Record<string, unknown>) => {
|
|
if (command === 'append_local_permission_log') {
|
|
return {};
|
|
}
|
|
if (command === 'read_project_permission_policy') {
|
|
return emptyProjectPolicy();
|
|
}
|
|
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 === 'read_local_project_file') {
|
|
runReadCount += 1;
|
|
return {
|
|
path: '.agent/run.latest.json',
|
|
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
|
|
content: JSON.stringify(
|
|
runReadCount > 1
|
|
? makeTrace('passed', 2, 'evaluator-passed')
|
|
: makeTrace('running', 1, 'planning', 'pending'),
|
|
),
|
|
};
|
|
}
|
|
if (command === 'list_local_project_files') {
|
|
return { projectPath: String(args?.projectPath ?? ''), files: [] };
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
|
|
|
|
expect(
|
|
await screen.findByText('run: running / pending · 1/3 轮 · planning'),
|
|
).not.toBeNull();
|
|
expect(screen.getByLabelText('Agent task graph').textContent).toContain(
|
|
'active: 程序组 / Code 生成可运行原型(code-prototype)',
|
|
);
|
|
expect(screen.getByLabelText('Agent task graph').textContent).toContain(
|
|
'carry-over: 设计实现组 / Director 拆解创作方向(design-director)',
|
|
);
|
|
expect(screen.getByLabelText('Agent task graph').textContent).toContain(
|
|
'ready: 程序组 / Playtest 预览并试玩验收(preview-playtest)',
|
|
);
|
|
expect(screen.getByLabelText('Agent task graph').textContent).toContain(
|
|
'route: 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review) · code-runtime',
|
|
);
|
|
expect(screen.getByLabelText('Agent pass plans').textContent).toContain(
|
|
'pass 1: repair · active 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review) · carry 设计实现组 / Director 拆解创作方向(design-director) · waves 程序组 / Code 生成可运行原型(code-prototype) / 程序组 / Review 执行质量评审(quality-review) · repair 缺少输入监听 · routes code-runtime: 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review)',
|
|
);
|
|
expect(screen.getByLabelText('Agent artifacts').textContent).toContain(
|
|
'exports/artifact-8.json',
|
|
);
|
|
expect(screen.getByLabelText('Agent artifacts').textContent).not.toContain(
|
|
'exports/artifact-9.json',
|
|
);
|
|
expect(screen.getByLabelText('Agent artifacts').textContent).toContain(
|
|
'还有 1 个产物',
|
|
);
|
|
fireEvent.click(
|
|
within(screen.getByLabelText('Agent artifacts')).getByRole('button', {
|
|
name: '填入读取产物 exports/artifact-1.json',
|
|
}),
|
|
);
|
|
expect(await composerText()).toBe('/read exports/artifact-1.json');
|
|
fireEvent.click(screen.getByRole('button', { name: '刷新状态' }));
|
|
|
|
expect(
|
|
await screen.findByText('run: passed · 2/3 轮 · evaluator-passed'),
|
|
).not.toBeNull();
|
|
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
|
projectPath: '/tmp/authorized-game',
|
|
relativePath: '.agent/run.latest.json',
|
|
commandId: 'agent.trace_read',
|
|
});
|
|
});
|
|
|
|
it('requires project policy confirmation before refreshing trace from the panel', async () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'local-project-draft',
|
|
'未命名游戏原型',
|
|
);
|
|
const trace: GameCreationAgentRunTrace = {
|
|
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
runId: 'run-trace-panel-confirm',
|
|
commandId: 'game.generate_draft',
|
|
status: 'running',
|
|
lifecycleStatus: 'running',
|
|
passes: 1,
|
|
maxPasses: 3,
|
|
toolCallCount: 1,
|
|
maxToolCalls: 128,
|
|
stopReason: 'planning',
|
|
goal: '做一个厨房弹幕游戏',
|
|
coordination: 'filesystem',
|
|
steps: [],
|
|
artifacts: [],
|
|
taskGraph: {
|
|
goal: '做一个厨房弹幕游戏',
|
|
readyTaskIds: [],
|
|
activeTaskIds: [],
|
|
carriedTaskIds: [],
|
|
repairFocus: [],
|
|
repairRoutes: [],
|
|
tasks: createGameCreationAppSeedTasks(),
|
|
},
|
|
passPlans: [],
|
|
nextStep: 'planner',
|
|
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 {
|
|
path: '.agent/policy.json',
|
|
policy: {
|
|
deniedCommands: [],
|
|
confirmCommands: ['agent.trace_read'],
|
|
},
|
|
};
|
|
}
|
|
if (command === 'read_local_project_file') {
|
|
return {
|
|
path: '.agent/run.latest.json',
|
|
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
|
|
content: JSON.stringify(trace),
|
|
};
|
|
}
|
|
if (command === 'list_local_project_files') {
|
|
return { projectPath: String(args?.projectPath ?? ''), files: [] };
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
|
|
|
|
expect(
|
|
await screen.findByText('run: running / running · 1/3 轮 · planning'),
|
|
).not.toBeNull();
|
|
invoke.mockClear();
|
|
|
|
fireEvent.click(
|
|
within(screen.getByLabelText('Agent run trace')).getByRole('button', {
|
|
name: '刷新',
|
|
}),
|
|
);
|
|
|
|
expect(await screen.findByText('agent.trace_read')).not.toBeNull();
|
|
expect(invoke).not.toHaveBeenCalledWith(
|
|
'read_local_project_file',
|
|
expect.anything(),
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
|
projectPath: '/tmp/authorized-game',
|
|
relativePath: '.agent/run.latest.json',
|
|
commandId: 'agent.trace_read',
|
|
});
|
|
});
|
|
});
|
|
}
|