Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts
T
AIGameCreator App 167b11d52e 完善自主 Swarm 无干预可玩交付
固定首批程序实现与质量只读职责,并在计划执行前阻断质量 Agent 项目写入
要求非只读专业 Agent 完成本人修改和对应 revision 验证后再交付
补齐 Provider 瞬态重试、最终回复和 Swarm CLI 终态收敛
升级 Provider action batch v3 并兼容 v2/v1 持久恢复
扩展确定性与真实外部 Provider 塔防验收、前端状态和项目文档
2026-07-23 01:06:54 +08:00

3133 lines
109 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 {
act,
agentRuntimeUserInputRequest,
App,
cleanup,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
createProjectSupervisorRuntimeHarness,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
it,
mockRoleAgentReply,
ProjectDevelopmentView,
projectSupervisorResponseStream,
React,
readFileSync,
render,
renderAppAt,
renderLauncherProjectsAt,
resolve,
screen,
submitChat,
vi,
waitFor,
within,
} from './harness';
export function registerProjectWorkbenchFoundationTests() {
it('renders the first project workbench slice with honest disabled run and local approval UI', () => {
const manifest = createGameCreationAppManifest(
'workbench-draft',
'陶泥儿工作台测试',
);
expect(
manifest.tasks.find((task) => task.id === 'design-foundation'),
).toMatchObject({
title: '确定玩法规格与界面原型',
artifacts: [
'memory/project.md',
'game/game_design.md',
'assets/ui-prototype.png',
],
acceptanceCriteria: [
'核心循环、胜负条件和第一版关卡目标明确,且已生成可读的 16:9 横屏界面原型图',
],
});
expect(
manifest.tasks.find((task) => task.id === 'art-asset-plan'),
).toMatchObject({
title: '生成首版美术素材',
artifacts: ['assets/manifest.art.json', 'assets/art-spritesheet.png'],
acceptanceCriteria: [
'角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记',
],
});
render(
React.createElement(ProjectDevelopmentView, {
projectName: '陶泥儿工作台测试',
projectPath: '/tmp/workbench-draft',
manifest,
attachments: [
{
fileName: 'broken-reference.png',
mediaType: 'image/png',
status: 'failed',
error: '图片解码失败',
},
],
recentRunStatus: null,
recentRunStopReason: null,
supervisor: React.createElement(
'div',
{ 'aria-label': '测试项目总控' },
'项目总控对话内容',
),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
expect(screen.getByLabelText('项目开发工作台')).not.toBeNull();
expect(screen.getByLabelText('项目主视窗')).not.toBeNull();
expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull();
expect(screen.getByLabelText('测试项目总控')).not.toBeNull();
expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull();
expect(screen.getByRole('article', { name: /策划 Agent/ })).not.toBeNull();
expect(screen.getByRole('article', { name: /美术 Agent/ })).not.toBeNull();
expect(screen.getByRole('article', { name: /程序 Agent/ })).not.toBeNull();
const runTab = screen.getByRole('tab', {
name: '运行',
}) as HTMLButtonElement;
expect(runTab.disabled).toBe(false);
expect(runTab.getAttribute('data-unavailable')).toBe('true');
fireEvent.click(runTab);
expect(runTab.getAttribute('aria-selected')).toBe('false');
expect(
screen.getByText('首个可运行原型尚未完成,运行视图暂不可用'),
).not.toBeNull();
expect(screen.getByLabelText('附件导入失败')).not.toBeNull();
expect(screen.getByText('broken-reference.png')).not.toBeNull();
expect(screen.getByText('图片解码失败')).not.toBeNull();
fireEvent.click(
screen.getByRole('button', {
name: '审批配置,当前严格审批',
}),
);
expect(
screen.getByRole('dialog', { name: '陶泥儿的操作权限' }),
).not.toBeNull();
const riskApproval = screen.getByRole('radio', { name: /风险审批/ });
fireEvent.click(riskApproval);
expect(riskApproval.getAttribute('aria-checked')).toBe('false');
expect(riskApproval.getAttribute('data-unavailable')).toBe('true');
expect(screen.getAllByText('Rank 规则待定,当前暂不可用')).toHaveLength(2);
fireEvent.click(screen.getByRole('button', { name: '完成' }));
expect(
screen.getByRole('button', {
name: '审批配置,当前严格审批',
}),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '更多小组' }));
expect(screen.getByRole('article', { name: /数值 Agent/ })).not.toBeNull();
expect(screen.getByRole('article', { name: /音频 Agent/ })).not.toBeNull();
expect(screen.getByRole('article', { name: /发布 Agent/ })).not.toBeNull();
});
it('keeps project chrome out, reorders resources by drag, and contains text receipts in a dialog', () => {
const manifest = createGameCreationAppManifest(
'workbench-resource-drag',
'不应显示的项目标题',
);
render(
React.createElement(ProjectDevelopmentView, {
projectName: '不应显示的项目标题',
projectPath: '/tmp/workbench-resource-drag',
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
agentRuntimeSummaries: [
{
group: 'art',
label: '美术 Agent',
status: 'completed',
statusLabel: '已完成',
currentTask: '本轮工作已完成',
currentAction: null,
waitingOn: null,
completedCount: 4,
totalCount: 4,
},
],
agentResults: [
{
agentId: 'design-foundation',
runId: 'design-result-run',
label: '玩法策划 Agent',
title: '玩法策划 Agent 文本回执',
content: '策划回执正文',
updatedAt: 1,
},
{
agentId: 'art-asset-plan',
runId: 'art-result-run',
label: '美术资源计划 Agent',
title: '美术资源计划 Agent 文本回执',
content:
'# 美术计划\n\n- 仅有美术计划\n- 没有图片文件\n\n**等待实际素材生成。**',
updatedAt: 2,
},
],
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
expect(screen.queryByText('不应显示的项目标题')).toBeNull();
expect(screen.queryByRole('button', { name: '回首页' })).toBeNull();
expect(screen.queryByRole('button', { name: '项目组' })).toBeNull();
expect(screen.getByText('仅完成计划')).not.toBeNull();
expect(
screen.getByText('美术资源计划已完成,尚未生成或登记图片'),
).not.toBeNull();
const transferData = new Map<string, string>();
const dataTransfer = {
effectAllowed: 'none',
dropEffect: 'none',
setData: (type: string, value: string) => transferData.set(type, value),
getData: (type: string) => transferData.get(type) ?? '',
};
let resourceCards = screen.getAllByTitle('拖动可调整当前会话中的排列');
expect(resourceCards[0]?.textContent).toContain(
'美术资源计划 Agent 文本回执',
);
fireEvent.dragStart(resourceCards[0]!, { dataTransfer });
fireEvent.dragOver(resourceCards[1]!, { dataTransfer, clientX: 10 });
fireEvent.drop(resourceCards[1]!, { dataTransfer, clientX: 10 });
resourceCards = screen.getAllByTitle('拖动可调整当前会话中的排列');
expect(resourceCards[0]?.textContent).toContain('玩法策划 Agent 文本回执');
expect(screen.getByText('已调整当前会话排列')).not.toBeNull();
fireEvent.click(resourceCards[1]!);
const receiptDialog = screen.getByRole('dialog', {
name: '美术资源计划 Agent 文本回执',
});
expect(
within(receiptDialog).getByRole('heading', { name: '美术计划' }),
).not.toBeNull();
expect(within(receiptDialog).getByText('仅有美术计划')).not.toBeNull();
expect(within(receiptDialog).getByText('没有图片文件')).not.toBeNull();
expect(
within(receiptDialog).getByText('等待实际素材生成。'),
).not.toBeNull();
expect(
receiptDialog.querySelector('.game-resource-focus-body'),
).not.toBeNull();
expect(receiptDialog.querySelector('ul')).not.toBeNull();
expect(receiptDialog.querySelector('strong')).not.toBeNull();
});
it('keeps the landscape workbench inside the viewport with internal chat scrolling', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toMatch(
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding-top:\s*40px/s,
);
expect(styles).toMatch(
/@media \(min-width: 761px\) and \(min-height: 680px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-conversation\s*\{[^}]*grid-template-rows:\s*minmax\(120px, 1fr\) minmax\(0, auto\) auto auto auto[^}]*height:\s*100%[^}]*overflow:\s*hidden/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\(190px, 30dvh, 300px\)[^}]*overflow-y:\s*auto/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px/s,
);
const agentDockRule = styles.match(/\.game-agent-dock\s*\{([^}]*)\}/s);
expect(agentDockRule?.[1]).toContain('overflow: visible;');
expect(agentDockRule?.[1]).not.toContain('overflow-x:');
expect(agentDockRule?.[1]).toContain('z-index: 30;');
expect(agentDockRule?.[1]).toContain('isolation: isolate;');
expect(styles).toMatch(
/@media \(max-width: 760px\)[\s\S]*?\.game-workbench-layout\s*\{[^}]*height:\s*auto/,
);
});
it('enables the run presentation and renders registered images in the resource viewer', async () => {
const manifest = createGameCreationAppManifest(
'workbench-runnable',
'可运行工作台',
);
const codePrototype = manifest.tasks.find(
(task) => task.id === 'code-prototype',
);
if (!codePrototype) {
throw new Error('missing code-prototype seed task');
}
codePrototype.status = 'completed';
manifest.preview = {
status: 'running',
url: 'http://127.0.0.1:4173',
port: 4173,
};
manifest.assets.push({
id: 'hero-art',
kind: 'character',
mediaType: 'image/png',
localPath: 'assets/hero.png',
source: {
kind: 'generated',
taskId: 'art-asset-plan',
},
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_image_preview') {
expect(args).toEqual({
projectPath: '/tmp/workbench-runnable',
relativePath: 'assets/hero.png',
});
return {
path: 'assets/hero.png',
mediaType: 'image/png',
byteLen: 12,
dataUrl:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: '可运行工作台',
projectPath: '/tmp/workbench-runnable',
manifest,
attachments: [],
recentRunStatus: 'completed',
recentRunStopReason: null,
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
const runTab = screen.getByRole('tab', {
name: '运行',
}) as HTMLButtonElement;
expect(runTab.disabled).toBe(false);
fireEvent.click(runTab);
expect(screen.getByLabelText('运行表现层')).not.toBeNull();
const previewFrame = screen.getByTitle(
'可运行工作台 游戏运行画面',
) as HTMLIFrameElement;
expect(previewFrame.getAttribute('src')).toBe('http://127.0.0.1:4173/');
expect(previewFrame.getAttribute('sandbox')).toBe(
'allow-scripts allow-same-origin allow-forms allow-pointer-lock',
);
expect(screen.getByLabelText('测试切片控件')).not.toBeNull();
expect(screen.getByLabelText('资源信息面板')).not.toBeNull();
expect(screen.getByLabelText('数值微调面板')).not.toBeNull();
fireEvent.click(screen.getByRole('tab', { name: '资源管理' }));
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
expect(screen.getByLabelText('资源类型视图')).not.toBeNull();
fireEvent.change(screen.getByLabelText('搜索项目资源'), {
target: { value: 'hero.png' },
});
fireEvent.click(screen.getByRole('button', { name: /hero\.png/ }));
expect(screen.getByLabelText('资源焦点')).not.toBeNull();
expect(screen.getAllByText('assets/hero.png').length).toBeGreaterThan(0);
const previewImage = await screen.findByRole('img', {
name: 'hero.png 图片预览',
});
expect(previewImage.getAttribute('src')).toBe(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
);
expect(invoke).toHaveBeenCalledTimes(1);
fireEvent.error(previewImage);
expect(screen.getByRole('alert').textContent).toBe(
'图片内容无法解码,请重新生成或替换该资源',
);
});
it('marks an unvalidated UI prototype as a candidate image', () => {
const manifest = createGameCreationAppManifest(
'workbench-ui-candidate',
'候选界面图测试',
);
manifest.assets.push({
id: 'ui-prototype-candidate',
kind: 'ui-prototype',
mediaType: 'image/png',
localPath: 'assets/ui-prototype.png',
source: {
kind: 'canvas',
taskId: 'design-foundation',
},
});
render(
React.createElement(ProjectDevelopmentView, {
projectName: '候选界面图测试',
projectPath: '/tmp/workbench-ui-candidate',
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
agentRuntimeSummaries: [
{
group: 'art',
label: '美术 Agent',
status: 'completed',
statusLabel: '已完成',
currentTask: '本轮工作已完成',
currentAction: null,
waitingOn: null,
completedCount: 4,
totalCount: 4,
},
],
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
const candidate = screen.getByRole('button', {
name: /ui-prototype\.png(待视觉验收)/,
});
expect(candidate.textContent).toContain('画板 · 候选界面图');
expect(candidate.textContent).toContain('assets/ui-prototype.png');
expect(screen.getByText('仅完成计划')).not.toBeNull();
expect(
screen.getByText('美术资源计划已完成,尚未生成或登记图片'),
).not.toBeNull();
});
it('refuses to embed a non-loopback game preview in the client workbench', () => {
const manifest = createGameCreationAppManifest(
'workbench-remote-preview',
'远程预览拒绝测试',
);
const codePrototype = manifest.tasks.find(
(task) => task.id === 'code-prototype',
);
if (!codePrototype) {
throw new Error('missing code-prototype seed task');
}
codePrototype.status = 'completed';
manifest.preview = {
status: 'running',
url: 'https://example.com/game',
port: 443,
};
render(
React.createElement(ProjectDevelopmentView, {
projectName: '远程预览拒绝测试',
projectPath: '/tmp/workbench-remote-preview',
manifest,
attachments: [],
recentRunStatus: 'completed',
recentRunStopReason: null,
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
expect(screen.queryByTitle('远程预览拒绝测试 游戏运行画面')).toBeNull();
expect(screen.getByText('客户端运行画面尚未载入')).not.toBeNull();
});
}
export function registerUserSurfaceBoundaryTests() {
it('keeps the user surface to chat, upload, config and command confirmation', async () => {
renderAppAt('/?main');
expect(screen.getByLabelText('聊天')).not.toBeNull();
expect(screen.getByLabelText('Agent 状态')).not.toBeNull();
const composerInput = screen.getByLabelText('创作想法');
expect(composerInput).not.toBeNull();
expect(screen.getByText('上传')).not.toBeNull();
expect(screen.getByRole('button', { name: '命令' })).not.toBeNull();
expect(screen.getByRole('button', { name: '能力' })).not.toBeNull();
expect(screen.getByRole('button', { name: '配置' })).not.toBeNull();
expect(screen.getByRole('button', { name: 'LLM状态' })).not.toBeNull();
expect(screen.getByRole('button', { name: '显示目录' })).not.toBeNull();
expect(screen.queryByLabelText('项目摘要')).toBeNull();
expect(
(screen.getByRole('button', { name: '灵感草稿' }) as HTMLButtonElement)
.disabled,
).toBe(false);
expect(
(screen.getByRole('button', { name: '项目状态' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '权限' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '审计' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '运行' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '资产' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '打开画板' }) as HTMLButtonElement)
.disabled,
).toBe(false);
expect(
(
screen.getByRole('button', {
name: '导入画板资产',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '任务' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '索引确认' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '资产登记确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '记忆写入确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '预览确认' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '打开预览确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '停止预览确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: 'Agent确认' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '读对话确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '存对话确认',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: 'Trace' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '文件' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '索引' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '记忆' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '短期记忆' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '记到黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '覆盖黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '清空黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '快照' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '快照列表' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '历史' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '白名单' }) as HTMLButtonElement)
.disabled,
).toBe(false);
expect(
(screen.getByRole('button', { name: '静态自检' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '启动预览' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '打开预览' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '预览状态' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '停止预览' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '刷新状态' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '刷新 Agent' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '状态' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '终止' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '重试' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '继续' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '继续说明' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '输出' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '活动' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '上下文包' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: /拆解创作方向/,
}) as HTMLButtonElement
).disabled,
).toBe(true);
fireEvent.click(screen.getByRole('button', { name: '打开画板' }));
expect(composerInput).toHaveProperty('value', '/canvas ');
fireEvent.click(screen.getByRole('button', { name: '灵感草稿' }));
expect(composerInput).toHaveProperty(
'value',
'像素风厨房弹幕小游戏:玩家用方向键躲避飞来的食材,收集调料加分,60 秒内尽量高分,失败后可一键重开。',
);
expect(screen.queryByText('game.generate_draft')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '白名单' }));
expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull();
expect(screen.getByRole('button', { name: '切换项目' })).not.toBeNull();
expect(screen.getByText('想做什么游戏?')).not.toBeNull();
expect(screen.getAllByText('暂无最近运行证据').length).toBeGreaterThan(0);
expect(screen.queryByLabelText('工作区管理')).toBeNull();
expect(screen.queryByLabelText('开发环境')).toBeNull();
expect(screen.queryByLabelText('运行时配置')).toBeNull();
expect(screen.queryByText('Agent 能力')).toBeNull();
expect(screen.queryByText('编排 Trace')).toBeNull();
});
}
export function registerProjectSupervisorSurfaceTests() {
it('loads and continues the active Project Supervisor Session in the standalone chat surface', async () => {
const projectPath = '/tmp/supervisor-chat-only-game';
const historyMessage = '已持久化的项目总控历史';
const harness = createProjectSupervisorRuntimeHarness({
projectPath,
expectedRunProfile: 'standard',
supervisorMessages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: historyMessage,
agentId: 'project-supervisor',
messageId: 'supervisor-chat-only-history',
updatedAt: 2000,
},
],
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
supervisorChatOnly: true,
}),
);
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
const messageList = within(surface).getByLabelText('项目总控消息');
expect(await within(messageList).findByText(historyMessage)).not.toBeNull();
expect(harness.invoke).toHaveBeenCalledWith(
'list_game_creator_agent_sessions',
{
projectPath,
agentId: 'project-supervisor',
},
);
expect(harness.invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath,
agentId: 'project-supervisor',
sessionId: harness.sessionId,
});
expect(
within(surface).getByRole('button', { name: '设置' }),
).not.toBeNull();
expect(screen.queryByLabelText('选择 Agent')).toBeNull();
expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull();
expect(screen.queryByLabelText('专业 Agent 协作状态')).toBeNull();
fireEvent.change(within(surface).getByLabelText('项目总控对话内容'), {
target: { value: '继续完成可玩原型' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
{
projectPath,
sessionId: harness.sessionId,
task: '继续完成可玩原型',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'standard',
},
);
});
expect(harness.invoke).not.toHaveBeenCalledWith(
'create_game_creator_agent_session',
expect.anything(),
);
expect(harness.invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_agent',
expect.anything(),
);
});
it('keeps an unsent standalone Project Supervisor draft across window navigation reloads', async () => {
const projectPath = '/tmp/supervisor-chat-only-draft';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
const renderSupervisorChat = () =>
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
supervisorChatOnly: true,
}),
);
renderSupervisorChat();
const firstSurface = await screen.findByLabelText('项目总控 Agent 纯聊天');
await within(firstSurface).findByLabelText('项目总控消息');
fireEvent.change(within(firstSurface).getByLabelText('项目总控对话内容'), {
target: { value: '这条草稿还没有发送' },
});
cleanup();
renderSupervisorChat();
const restoredInput = await screen.findByLabelText('项目总控对话内容');
expect(restoredInput).toHaveProperty('value', '这条草稿还没有发送');
});
it('shows and handles runtime recovery confirmation in the standalone Project Supervisor chat', async () => {
const projectPath = '/tmp/supervisor-chat-only-resume';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'resume_game_creator_agent_runtime_tasks') {
throw new Error('项目权限策略要求用户确认:agent.resume');
}
if (command === 'confirm_resume_game_creator_agent_runtime_tasks') {
return [];
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
supervisorChatOnly: true,
}),
);
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
const detail = await within(surface).findByText(
`恢复 ${projectPath} 中未完成的 Agent Runtime 任务`,
);
expect(within(surface).queryByText(/项目总控 Agent 恢复失败/)).toBeNull();
const confirmation = detail.closest('.pending-command');
expect(confirmation).not.toBeNull();
fireEvent.click(
within(confirmation as HTMLElement).getByRole('button', {
name: '确认',
}),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_resume_game_creator_agent_runtime_tasks',
{ projectPath },
);
expect(
within(surface).queryByText(
`恢复 ${projectPath} 中未完成的 Agent Runtime 任务`,
),
).toBeNull();
});
});
it('confirms and rejects pending actions in the standalone Project Supervisor chat', async () => {
const projectPath = '/tmp/supervisor-chat-only-confirmation';
const runId = 'supervisor-chat-only-confirmation-run';
const harness = createProjectSupervisorRuntimeHarness({
projectPath,
initialRuntime: {
runId,
status: 'waiting-for-confirmation',
phase: 'waiting-for-confirmation',
pendingToolAction: {
actionId: 'standalone-action-confirm',
actionFingerprint: 'standalone-fingerprint-confirm',
tool: 'file.write',
inputSummary: 'game/index.html',
reason: null,
requestedAt: 3000,
},
},
});
harness.setConfirmRuntime(
harness.runtimeState({
runId,
status: 'waiting-for-confirmation',
phase: 'waiting-for-confirmation',
pendingToolAction: {
actionId: 'standalone-action-reject',
actionFingerprint: 'standalone-fingerprint-reject',
tool: 'command.exec',
inputSummary: 'npm test',
reason: null,
requestedAt: 4000,
},
}),
);
harness.setRejectRuntime(
harness.runtimeState({
runId,
status: 'running',
phase: 'planning',
pendingToolAction: null,
}),
);
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
supervisorChatOnly: true,
}),
);
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
let pendingAction =
await within(surface).findByLabelText('项目总控 Agent 待确认动作');
expect(within(pendingAction).getByText('file.write')).not.toBeNull();
expect(within(pendingAction).getByText('game/index.html')).not.toBeNull();
fireEvent.click(
within(pendingAction).getByRole('button', { name: '确认' }),
);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'confirm_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
runId,
actionId: 'standalone-action-confirm',
note: '用户已确认待执行工具动作',
},
);
});
pendingAction =
await within(surface).findByLabelText('项目总控 Agent 待确认动作');
expect(within(pendingAction).getByText('command.exec')).not.toBeNull();
expect(within(pendingAction).getByText('npm test')).not.toBeNull();
fireEvent.click(
within(pendingAction).getByRole('button', { name: '拒绝' }),
);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'reject_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
runId,
actionId: 'standalone-action-reject',
note: '用户已拒绝待执行工具动作',
},
);
});
});
it('answers structured questions in the standalone Project Supervisor chat', async () => {
const projectPath = '/tmp/supervisor-chat-only-user-input';
const sessionId = 'supervisor-chat-only-user-input-session';
const runId = 'supervisor-chat-only-user-input-run';
const request = agentRuntimeUserInputRequest({
agentId: 'project-supervisor',
sessionId,
runId,
});
const harness = createProjectSupervisorRuntimeHarness({
projectPath,
sessionId,
initialRuntime: {
runId,
status: 'waiting-for-user-input',
phase: 'waiting-for-user-input',
currentTask: '准备首版角色规范图',
currentAction: '等待用户补充关键信息',
waitingOn: '你的澄清回答',
nextStep: '提交全部回答后继续同一 Run',
userInputRequest: request,
updatedAt: 6000,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
supervisorChatOnly: true,
}),
);
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
const card = await within(surface).findByLabelText('Needs input');
expect(within(card).getByText('1. 美术方向')).not.toBeNull();
expect(
within(card).getByText('首版角色规范图采用哪种美术方向?'),
).not.toBeNull();
expect(within(card).getByText('优先验证轮廓与动作可读性。')).not.toBeNull();
expect(
(
within(surface).getByLabelText(
'项目总控对话内容',
) as HTMLTextAreaElement
).disabled,
).toBe(true);
fireEvent.click(within(card).getByRole('button', { name: /像素风/ }));
fireEvent.click(within(card).getByRole('button', { name: '提交回答' }));
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'answer_game_creator_agent_runtime_user_input',
{
projectPath,
agentId: 'project-supervisor',
runId,
actionId: request.actionId,
requestId: request.requestId,
responseId: expect.stringMatching(/^app-user-input-/),
answers: { visual_direction: '像素风' },
},
);
});
await waitFor(() => {
expect(within(surface).queryByLabelText('Needs input')).toBeNull();
});
});
it('opens an existing project into the active Supervisor Session, restores history, then starts and steers the same run', async () => {
const projectPath = '/tmp/launcher-supervisor-game';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-supervisor-game',
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
supervisorMessages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '已恢复的项目总控历史',
agentId: 'project-supervisor',
messageId: 'launcher-supervisor-history',
updatedAt: 2000,
},
],
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'inspect_local_project_directory') {
return {
projectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: true,
projectName: 'launcher-supervisor-game',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
return supervisorHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: supervisorHarness.listen },
};
renderLauncherProjectsAt('/?launcher');
fireEvent.change(screen.getByLabelText('项目目录'), {
target: { value: projectPath },
});
fireEvent.click(screen.getByRole('button', { name: '打开' }));
const supervisorSurface = await screen.findByLabelText('项目总控对话');
expect(
await within(supervisorSurface).findByText('已恢复的项目总控历史'),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('list_game_creator_agent_sessions', {
projectPath,
agentId: 'project-supervisor',
});
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath,
agentId: 'project-supervisor',
sessionId: supervisorHarness.sessionId,
});
expect(screen.getByLabelText('专业 Agent 协作状态')).not.toBeNull();
expect(screen.queryByLabelText('选择 Agent')).toBeNull();
expect(screen.queryByRole('dialog', { name: 'Agent 对话' })).toBeNull();
expect(supervisorHarness.listen).not.toHaveBeenCalledWith(
'game-creator-agent-progress',
expect.any(Function),
);
const policyReadCountBeforeChat = invoke.mock.calls.filter(
([command]) => command === 'read_project_permission_policy',
).length;
fireEvent.change(screen.getByLabelText('项目需求'), {
target: { value: '先完成正式客户端玩法拆解' },
});
fireEvent.click(
within(supervisorSurface).getByRole('button', { name: '发送' }),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
{
projectPath,
sessionId: supervisorHarness.sessionId,
task: '先完成正式客户端玩法拆解',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
},
);
});
const startCall = invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const runId = String(startCall?.[1]?.runId ?? '');
await waitFor(() => {
expect(
(
within(supervisorSurface).getByRole('button', {
name: '发送',
}) as HTMLButtonElement
).disabled,
).toBe(false);
});
fireEvent.change(screen.getByLabelText('项目需求'), {
target: { value: '补充:优先复用现有素材' },
});
fireEvent.click(
within(supervisorSurface).getByRole('button', { name: '发送' }),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'steer_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
sessionId: supervisorHarness.sessionId,
runId,
steerId: expect.stringMatching(/^project-supervisor-steer-/),
instruction: '补充:优先复用现有素材',
runProfile: 'autonomous-game-build',
},
);
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
expect(
invoke.mock.calls.filter(
([command]) => command === 'read_project_permission_policy',
),
).toHaveLength(policyReadCountBeforeChat);
});
it('keeps the workbench professional Agent status truthful through polling without Tauri events', async () => {
const projectPath = '/tmp/launcher-runtime-status-game';
const supervisorRunId = 'supervisor-runtime-status-run';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-runtime-status-game',
);
expect(manifest.tasks.every((task) => task.status === 'pending')).toBe(
true,
);
let exposeProfessionalRuntimes = false;
let failProfessionalRuntimeReads = false;
let failProfessionalConversationReads = false;
let designConversationMode: 'initial' | 'updated' = 'initial';
let failProfessionalAction = false;
let failProfessionalRetry = true;
let professionalRuntimeReadCount = 0;
let professionalConversationReadCount = 0;
let releaseProfessionalRetry: (() => void) | null = null;
let professionalRuntimes: Array<Record<string, unknown>> = [];
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
initialRuntime: {
runId: supervisorRunId,
status: 'running',
phase: 'waiting-for-delegate-receipts',
currentTask: '协调专业 Agent 完成首版原型',
currentAction: '等待专业 Agent 返回真实 Runtime 状态',
waitingOn: '专业 Agent 回执',
updatedAt: 5000,
},
runtimeMapLoader: async () => {
professionalRuntimeReadCount += 1;
if (failProfessionalRuntimeReads) {
throw new Error('模拟专业 Runtime 瞬时读取失败');
}
return exposeProfessionalRuntimes ? professionalRuntimes : [];
},
});
professionalRuntimes = [
supervisorHarness.runtimeState({
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'design-runtime-status-session',
runId: 'design-runtime-status-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'design-runtime-status-delegation',
status: 'failed',
phase: 'failed',
currentTask: '拆解首版玩法',
currentAction: '策划 Runtime 已失败',
error:
'agentLlm.design-director 后台 Agent 工具计划调用 LLM 失败:kind=transport fingerprint=internal-error-fingerprint chars=545',
updatedAt: 6100,
}),
supervisorHarness.runtimeState({
agentId: 'art-director',
taskId: 'art-director',
sessionId: 'art-runtime-status-session',
runId: 'art-runtime-status-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'art-runtime-status-delegation',
status: 'waiting-for-confirmation',
phase: 'waiting-for-confirmation',
currentTask: '写入美术计划',
currentAction: '等待确认写入美术计划',
waitingOn: '用户确认 file.write',
pendingToolAction: {
actionId: 'art-file-write-action',
actionFingerprint: 'art-file-write-fingerprint',
tool: 'file.write',
inputSummary: 'path=assets/art-plan.md · contentChars=7325',
reason: null,
requestedAt: 6200,
},
updatedAt: 6200,
}),
supervisorHarness.runtimeState({
agentId: 'code-director',
taskId: 'code-director',
sessionId: 'code-runtime-status-session',
runId: 'code-runtime-status-run',
source: 'agent-delegate-retry',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'code-runtime-status-delegation',
status: 'completed',
phase: 'completed',
currentTask: '完成程序实现拆解',
currentAction: '程序方案已完成',
updatedAt: 6300,
}),
supervisorHarness.runtimeState({
agentId: 'art-polish',
taskId: 'art-polish',
sessionId: 'stale-art-session',
runId: 'stale-art-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: 'stale-supervisor-run',
delegationId: 'stale-art-delegation',
status: 'running',
phase: 'action',
currentTask: '旧父 Run 的美术任务不得显示',
currentAction: '旧父 Run 的状态不得显示',
updatedAt: 9999,
}),
];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'inspect_local_project_directory') {
return {
projectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: true,
projectName: 'launcher-runtime-status-game',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (
command === 'read_local_conversation' &&
args?.agentId === 'code-director'
) {
professionalConversationReadCount += 1;
if (failProfessionalConversationReads) {
throw new Error('模拟专业成果持久对话瞬时读取失败');
}
return {
path: `${projectPath}/.agent/conversations/agents/code-director.jsonl`,
agentId: 'code-director',
sessionId: 'code-runtime-status-session',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '已完成程序实现拆解,并给出可执行的模块边界。',
agentId: 'code-director',
messageId:
'agent-finalization-11111111111111111111111111111111',
updatedAt: 6300,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '新一轮程序任务失败,这不是可交付成果。',
agentId: 'code-director',
messageId: null,
updatedAt: 6400,
},
],
};
}
if (
command === 'read_local_conversation' &&
args?.agentId === 'design-director'
) {
professionalConversationReadCount += 1;
if (failProfessionalConversationReads) {
throw new Error('模拟专业成果持久对话瞬时读取失败');
}
const designFinalization =
designConversationMode === 'updated'
? {
content: '更新后的策划历史成果,替换同 Agent 的旧回执。',
messageId:
'agent-finalization-33333333333333333333333333333333',
updatedAt: 7200,
}
: {
content: '上一轮已经完成并验收的策划成果。',
messageId:
'agent-finalization-22222222222222222222222222222222',
updatedAt: 5900,
};
return {
path: `${projectPath}/.agent/conversations/agents/design-director.jsonl`,
agentId: 'design-director',
sessionId: 'design-runtime-status-session',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: designFinalization.content,
agentId: 'design-director',
messageId: designFinalization.messageId,
updatedAt: designFinalization.updatedAt,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '当前新一轮策划失败,不得覆盖旧成果。',
agentId: 'design-director',
messageId: null,
updatedAt: 6100,
},
],
};
}
if (
command === 'read_local_conversation' &&
args?.agentId === 'art-asset-plan'
) {
professionalConversationReadCount += 1;
if (failProfessionalConversationReads) {
throw new Error('模拟专业成果持久对话瞬时读取失败');
}
return {
path: `${projectPath}/.agent/conversations/agents/art-asset-plan.jsonl`,
agentId: 'art-asset-plan',
sessionId: 'art-history-session',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '历史美术资源计划回执,尚不等于图片资产。',
agentId: 'art-asset-plan',
messageId:
'agent-finalization-44444444444444444444444444444444',
updatedAt: 5800,
},
],
};
}
if (
command === 'read_local_conversation' &&
args?.agentId === 'balance-seed'
) {
professionalConversationReadCount += 1;
if (failProfessionalConversationReads) {
throw new Error('模拟专业成果持久对话瞬时读取失败');
}
return {
path: `${projectPath}/.agent/conversations/agents/balance-seed.jsonl`,
agentId: 'balance-seed',
sessionId: 'balance-history-session',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '历史数值方案回执。',
agentId: 'balance-seed',
messageId:
'agent-finalization-55555555555555555555555555555555',
updatedAt: 5700,
},
],
};
}
if (command === 'confirm_retry_game_creator_agent_runtime_task') {
if (failProfessionalRetry) {
throw new Error(
'agentLlm.design-director retry失败:kind=transport fingerprint=private-retry-fingerprint chars=256',
);
}
await new Promise<void>((resolve) => {
releaseProfessionalRetry = resolve;
});
return supervisorHarness.runtimeResult(
supervisorHarness.runtimeState({
agentId: String(args?.agentId ?? ''),
taskId: String(args?.agentId ?? ''),
sessionId: 'design-runtime-status-session',
runId: String(args?.nextRunId ?? ''),
source: 'agent-delegate-retry',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
status: 'running',
phase: 'planning',
currentTask: '在当前项目重试策划任务',
currentAction: '重新生成 Agent 工具计划',
updatedAt: 7000,
}),
);
}
if (
failProfessionalAction &&
command === 'confirm_game_creator_agent_runtime_task'
) {
throw new Error(
'agentLlm.art-director 工具确认失败:kind=transport fingerprint=private-action-fingerprint chars=384',
);
}
return supervisorHarness.invoke(command, args);
},
);
window.__TAURI__ = { core: { invoke } };
renderLauncherProjectsAt('/?launcher');
fireEvent.change(screen.getByLabelText('项目目录'), {
target: { value: projectPath },
});
fireEvent.click(screen.getByRole('button', { name: '打开' }));
expect(await screen.findByLabelText('项目总控对话')).not.toBeNull();
await waitFor(() => {
expect(professionalRuntimeReadCount).toBeGreaterThan(0);
});
expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull();
const supervisorStatusPanel = screen.getByLabelText('项目总控 Agent 状态');
supervisorStatusPanel.scrollTop = 120;
const readCountBeforeExpose = professionalRuntimeReadCount;
exposeProfessionalRuntimes = true;
const professionalList = await screen.findByLabelText(
'专业 Agent 实时状态',
{},
{ timeout: 2500 },
);
expect(professionalRuntimeReadCount).toBeGreaterThan(readCountBeforeExpose);
expect(
within(professionalList).getByText('策划 Agent 服务连接失败,请稍后重试'),
).not.toBeNull();
expect(professionalList.textContent).not.toContain('fingerprint');
expect(professionalList.textContent).not.toContain('chars=545');
expect(professionalList.textContent).not.toContain('contentChars');
expect(supervisorStatusPanel.scrollTop).toBe(0);
expect(
within(professionalList).getByText('等待确认后继续工作'),
).not.toBeNull();
expect(within(professionalList).getByText('本轮工作已完成')).not.toBeNull();
expect(
within(professionalList).queryByText('旧父 Run 的状态不得显示'),
).toBeNull();
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath,
agentId: 'code-director',
});
});
const codeAgentCard = within(professionalList)
.getByText('程序 Agent')
.closest('article')!;
const resultButton = await within(codeAgentCard).findByRole('button', {
name: '查看成果',
});
fireEvent.click(resultButton);
const resultDialog = await screen.findByRole('dialog', {
name: '程序 Agent 文本回执',
});
expect(resultDialog.textContent).toContain(
'已完成程序实现拆解,并给出可执行的模块边界。',
);
fireEvent.click(within(resultDialog).getByRole('button', { name: '关闭' }));
const documentResources = screen.getByLabelText('文档');
expect(
within(documentResources).getByText('程序 Agent 文本回执'),
).not.toBeNull();
expect(
within(documentResources).getByText('策划 Agent 文本回执'),
).not.toBeNull();
expect(
within(documentResources).getByText('美术资源计划 Agent 文本回执'),
).not.toBeNull();
expect(
within(documentResources).getByText('数值 Agent 文本回执'),
).not.toBeNull();
expect(documentResources.textContent).not.toContain(
'当前新一轮策划失败,不得覆盖旧成果。',
);
expect(
within(documentResources).getByText('历史成果 · 策划 Agent'),
).not.toBeNull();
failProfessionalConversationReads = true;
const conversationReadCountBeforeFailure =
professionalConversationReadCount;
professionalRuntimes = professionalRuntimes.map((runtime) =>
runtime.agentId === 'design-director'
? { ...runtime, updatedAt: 7100 }
: runtime,
);
await waitFor(
() => {
expect(professionalConversationReadCount).toBeGreaterThan(
conversationReadCountBeforeFailure,
);
expect(
within(screen.getByLabelText('文档')).getByText(
'策划 Agent 文本回执',
),
).not.toBeNull();
expect(
within(screen.getByLabelText('文档')).getByText(
'程序 Agent 文本回执',
),
).not.toBeNull();
},
{ timeout: 2500 },
);
failProfessionalConversationReads = false;
designConversationMode = 'updated';
const conversationReadCountBeforeUpdate = professionalConversationReadCount;
professionalRuntimes = professionalRuntimes.map((runtime) =>
runtime.agentId === 'design-director'
? { ...runtime, updatedAt: 7200 }
: runtime,
);
await waitFor(
() => {
expect(professionalConversationReadCount).toBeGreaterThan(
conversationReadCountBeforeUpdate,
);
},
{ timeout: 2500 },
);
const updatedDesignCard = await waitFor(() => {
const card = within(screen.getByLabelText('文档')).getByRole('button', {
name: /^策划 Agent 文本回执/u,
});
expect(card.getAttribute('data-resource-id')).toContain(
'agent-finalization-33333333333333333333333333333333',
);
return card;
});
fireEvent.click(updatedDesignCard);
const updatedDesignDialog = await screen.findByRole('dialog', {
name: '策划 Agent 文本回执',
});
expect(
within(updatedDesignDialog).getByText(
'更新后的策划历史成果,替换同 Agent 的旧回执。',
),
).not.toBeNull();
fireEvent.click(
within(updatedDesignDialog).getByRole('button', { name: '收起资源' }),
);
const dock = screen.getByLabelText('子 Agent 状态栏');
await waitFor(() => {
expect(
within(
within(dock).getByRole('article', { name: /策划 Agent/ }),
).getByText('失败'),
).not.toBeNull();
expect(
within(
within(dock).getByRole('article', { name: /美术 Agent/ }),
).getByText('待确认'),
).not.toBeNull();
expect(
within(
within(dock).getByRole('article', { name: /程序 Agent/ }),
).getByText('已完成'),
).not.toBeNull();
});
const artPendingAction =
within(professionalList).getByLabelText('美术 Agent待确认动作');
failProfessionalAction = true;
fireEvent.click(
within(artPendingAction).getByRole('button', { name: '确认' }),
);
expect(
await screen.findByText('专业 Agent 服务连接失败,请稍后重试'),
).not.toBeNull();
expect(screen.queryByText(/private-action-fingerprint/)).toBeNull();
failProfessionalAction = false;
fireEvent.click(
within(artPendingAction).getByRole('button', { name: '确认' }),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'art-director',
runId: 'art-runtime-status-run',
actionId: 'art-file-write-action',
note: '用户已确认专业 Agent 待执行工具动作',
},
);
});
failProfessionalRuntimeReads = true;
const readCountBeforeFailure = professionalRuntimeReadCount;
await waitFor(
() => {
expect(professionalRuntimeReadCount).toBeGreaterThan(
readCountBeforeFailure,
);
},
{ timeout: 2500 },
);
expect(
within(
within(dock).getByRole('article', { name: /策划 Agent/ }),
).getByText('失败'),
).not.toBeNull();
expect(
within(
within(dock).getByRole('article', { name: /美术 Agent/ }),
).getByText('待确认'),
).not.toBeNull();
expect(
within(
within(dock).getByRole('article', { name: /程序 Agent/ }),
).getByText('已完成'),
).not.toBeNull();
let retryButton = within(professionalList).getByRole('button', {
name: '在当前项目重试',
});
fireEvent.click(retryButton);
expect(
await within(retryButton.closest('article')!).findByText(
'策划 Agent 服务连接失败,请稍后重试',
),
).not.toBeNull();
expect(retryButton.closest('article')?.textContent).not.toContain(
'private-retry-fingerprint',
);
failProfessionalRetry = false;
retryButton = within(professionalList).getByRole('button', {
name: '在当前项目重试',
});
fireEvent.click(retryButton);
const retryingButton = within(professionalList).getByRole('button', {
name: '正在提交重试…',
});
expect(retryingButton.hasAttribute('disabled')).toBe(true);
expect(
within(retryingButton.closest('article')!).getByRole('status')
.textContent,
).toBe('正在提交重试…');
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_retry_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'design-director',
runId: 'design-runtime-status-run',
nextRunId: expect.stringMatching(/^project-professional-retry-/),
},
);
});
releaseProfessionalRetry?.();
await within(professionalList).findByText(
'重试请求已受理,正在同步新一轮状态',
);
failProfessionalRuntimeReads = false;
exposeProfessionalRuntimes = false;
const readCountBeforeAuthoritativeEmpty = professionalRuntimeReadCount;
await waitFor(
() => {
expect(professionalRuntimeReadCount).toBeGreaterThan(
readCountBeforeAuthoritativeEmpty,
);
expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull();
expect(
within(
within(dock).getByRole('article', { name: /策划 Agent/ }),
).getByText('等待中'),
).not.toBeNull();
expect(
within(screen.getByLabelText('文档')).getByText(
'策划 Agent 文本回执',
),
).not.toBeNull();
},
{ timeout: 2500 },
);
}, 10_000);
it('does not render a ready Supervisor stream twice when its assistant is already in restored history', async () => {
const projectPath = '/tmp/launcher-supervisor-finalizing';
const reply = '已经落盘的唯一项目总控回复';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-supervisor-finalizing',
);
const runId = 'supervisor-finalizing-run';
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
supervisorMessages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: reply,
agentId: 'project-supervisor',
messageId: 'launcher-supervisor-final-assistant',
updatedAt: 7000,
},
],
initialRuntime: {
runId,
status: 'running',
phase: 'finalizing',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 7000,
},
initialResponseStream: projectSupervisorResponseStream({
runId,
sequence: 8,
accumulatedText: reply,
status: 'ready',
}),
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'inspect_local_project_directory') {
return {
projectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: true,
projectName: 'launcher-supervisor-finalizing',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
return supervisorHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: supervisorHarness.listen },
};
renderLauncherProjectsAt('/?launcher');
fireEvent.change(screen.getByLabelText('项目目录'), {
target: { value: projectPath },
});
fireEvent.click(screen.getByRole('button', { name: '打开' }));
const supervisorSurface = await screen.findByLabelText('项目总控对话');
expect(await within(supervisorSurface).findByText(reply)).not.toBeNull();
expect(within(supervisorSurface).getAllByText(reply)).toHaveLength(1);
expect(
within(supervisorSurface).queryByLabelText('项目总控 Agent 实时回复'),
).toBeNull();
});
it('rejects launcher project paths with control characters before Tauri calls', () => {
const invoke = vi.fn(async () => undefined);
window.__TAURI__ = { core: { invoke } };
renderLauncherProjectsAt('/?launcher');
fireEvent.change(screen.getByLabelText('项目目录'), {
target: { value: '/tmp/bad\u0007path' },
});
fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]);
expect(screen.getByText('项目目录不能包含控制字符')).not.toBeNull();
expect(invoke).not.toHaveBeenCalled();
});
}
export function registerProjectWorkbenchNavigationTests() {
it('keeps project switching inside the single-window client flow', async () => {
const invoke = vi.fn(async () => undefined);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main');
fireEvent.click(screen.getByRole('button', { name: '切换项目' }));
expect(screen.getByText('请回到首页的项目组切换项目。')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'open_game_creator_launcher_window',
);
submitChat('/switch-project');
expect(
screen.getAllByText('请回到首页的项目组切换项目。').length,
).toBeGreaterThanOrEqual(2);
expect(invoke).not.toHaveBeenCalledWith(
'open_game_creator_launcher_window',
);
});
it('opens the current project directory from the main project window', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
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 === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'open_local_project_directory') {
return undefined;
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:/tmp/authorized-game');
const revealButtons = screen.getAllByRole('button', { name: '显示目录' });
fireEvent.click(revealButtons[revealButtons.length - 1]);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('open_local_project_directory', {
projectPath: '/tmp/authorized-game',
});
});
expect(await screen.findByText('已打开项目目录。')).not.toBeNull();
submitChat('/open-project');
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'open_local_project_directory',
),
).toHaveLength(2);
});
expect(
screen.getAllByText('已打开项目目录。').length,
).toBeGreaterThanOrEqual(2);
});
it('fills an asset registration draft from recent project files without registering immediately', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
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_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: 'assets/uploads/hero.png',
kind: 'file',
size: 4,
modifiedAt: 1700000001,
},
],
};
}
if (command === 'register_local_asset') {
throw new Error('should only fill the chat draft');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
await act(async () => {
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
});
await screen.findByText('已打开:/tmp/authorized-game');
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '文件' }));
expect(await screen.findByText(/本地项目文件:/)).not.toBeNull();
fireEvent.click(
within(screen.getByLabelText('最近项目文件')).getByRole('button', {
name: '登记资产 assets/uploads/hero.png',
}),
);
await waitFor(() =>
expect(document.activeElement).toBe(screen.getByLabelText('创作想法')),
);
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/asset-register assets/uploads/hero.png image image/png',
);
expect(
screen.queryByText('asset.register · assets/uploads/hero.png'),
).toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'register_local_asset',
expect.anything(),
);
});
}
export function registerProjectAgentStatusTests() {
it('shows runtime status and recent tasks in the main agent status list', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const runtimeTask = {
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'runtime-design-director-1',
source: 'agent-background-task',
task: '排队补齐世界观拆解',
status: 'pending',
phase: 'queued',
currentAction: '等待当前任务完成',
error: null,
updatedAt: 10,
};
const runtimeState = {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'runtime-design-director-1',
source: 'agent-background-task',
status: 'running',
phase: 'planning',
currentTask: '拆解关卡节奏',
currentGoal: '补齐第一关节奏目标',
currentAction: '整理目标和约束',
waitingOn: 'Agent 输出计划或回复',
loopIteration: 2,
maxLoopIterations: 3,
toolActionBudget: 3,
plan: ['读取项目上下文'],
planSteps: [
{
index: 0,
title: '读取项目上下文',
status: 'active',
detail: '正在整理目标和约束',
updatedAt: 11,
},
],
activePlanStepIndex: 0,
observations: ['已创建本轮 Agent Runtime run。'],
taskQueue: {
total: 2,
pending: 1,
running: 1,
completed: 0,
failed: 0,
latestRunId: 'runtime-design-director-1',
updatedAt: 10,
},
allowedTools: ['conversation.read'],
lastResponse: null,
error: null,
updatedAt: 10,
};
const completedRuntimeState = {
...runtimeState,
status: 'idle',
phase: 'completed',
currentAction: '等待下一轮输入',
waitingOn: '开发者下一轮输入',
nextStep: '等待下一轮输入',
taskQueue: {
total: 2,
pending: 1,
running: 0,
completed: 1,
failed: 0,
latestRunId: 'runtime-design-director-1',
updatedAt: 12,
},
lastResponse: '已补齐第一关节奏目标。',
updatedAt: 12,
};
const completedRuntimeTask = {
...runtimeTask,
status: 'completed',
phase: 'completed',
currentAction: '等待下一轮输入',
updatedAt: 12,
};
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [
{
state: runtimeState,
sessionPath:
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
eventPath:
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
taskPath:
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
taskQueue: runtimeState.taskQueue,
recentEvents: [],
recentTasks: [runtimeTask],
},
];
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
let runtimeUpdateHandler:
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
handler: (event: { payload: Record<string, unknown> }) => void,
) => {
if (eventName === 'game-creator-agent-progress') {
return () => {};
}
if (eventName === 'game-creator-agent-runtime-update') {
runtimeUpdateHandler = handler;
return () => {
if (runtimeUpdateHandler === handler) {
runtimeUpdateHandler = null;
}
};
}
throw new Error(`unexpected listen ${eventName}`);
},
);
window.__TAURI__ = { core: { invoke }, event: { listen } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
const agentStatusList = screen.getByLabelText('Agent 状态列表');
await waitFor(() => {
const designCard = within(agentStatusList).getByRole('button', {
name: /拆解创作方向/,
});
expect(designCard.textContent).toContain(
'Runtime:running / planning · Loop 2/3 · 整理目标和约束 · 等待 Agent 输出计划或回复 · 下一步 等待 Agent 输出计划或回复 · run runtime-design-director-1',
);
expect(designCard.textContent).toContain('当前目标:补齐第一关节奏目标');
expect(designCard.textContent).toContain('当前任务:拆解关卡节奏');
expect(designCard.textContent).toContain(
'当前计划步骤:#1 active · 读取项目上下文 · 正在整理目标和约束',
);
expect(designCard.textContent).toContain(
'任务队列:pending 1 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 2 · latest runtime-design-director-1',
);
expect(designCard.textContent).toContain(
'最近任务:pending / queued · 排队补齐世界观拆解',
);
});
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'resume_game_creator_agent_runtime_tasks',
),
).toHaveLength(1);
});
await act(async () => {
runtimeUpdateHandler?.({
payload: {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
runId: 'runtime-design-director-1',
status: 'idle',
phase: 'completed',
runtime: {
state: completedRuntimeState,
sessionPath:
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
eventPath:
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
taskPath:
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
taskQueue: completedRuntimeState.taskQueue,
recentEvents: [],
recentTasks: [completedRuntimeTask],
},
},
});
});
await waitFor(() => {
const designCard = within(agentStatusList).getByRole('button', {
name: /拆解创作方向/,
});
expect(designCard.textContent).toContain(
'Runtime:idle / completed · Loop 2/3 · 等待下一轮输入 · 等待 开发者下一轮输入 · 下一步 等待下一轮输入 · run runtime-design-director-1',
);
expect(designCard.textContent).toContain(
'任务队列:pending 1 · running 0 · waiting 0 · needsInput 0 · cancelled 0 · completed 1 · failed 0 · total 2 · latest runtime-design-director-1',
);
expect(designCard.textContent).toContain(
'最近任务:completed / completed · 排队补齐世界观拆解',
);
});
});
it('lets the developer schedule ready manifest tasks into agent runtimes', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const runtimeTask = {
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'ready-design-director-1',
source: 'agent-ready-task-scheduler',
task: '执行 Ready 任务:拆解创作方向',
status: 'running',
phase: 'planning',
currentAction: '准备读取项目上下文',
error: null,
updatedAt: 20,
};
const runtimeState = {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'ready-design-director-1',
source: 'agent-ready-task-scheduler',
status: 'running',
phase: 'planning',
currentTask: '执行 Ready 任务:拆解创作方向',
currentGoal: '把 ready 任务投递给对应 Agent',
currentAction: '准备读取项目上下文',
waitingOn: 'Agent 输出计划或回复',
loopIteration: 1,
maxLoopIterations: 3,
toolActionBudget: 3,
plan: ['读取项目上下文'],
planSteps: [],
activePlanStepIndex: null,
observations: [],
taskQueue: {
total: 1,
pending: 0,
running: 1,
completed: 0,
failed: 0,
latestRunId: 'ready-design-director-1',
updatedAt: 20,
},
allowedTools: ['conversation.read'],
lastResponse: null,
error: null,
updatedAt: 20,
};
const runtimeResult = {
state: runtimeState,
sessionPath:
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
eventPath:
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
taskPath:
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
taskQueue: runtimeState.taskQueue,
recentEvents: [],
recentTasks: [runtimeTask],
};
let scheduled = false;
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return scheduled ? [runtimeResult] : [];
}
if (command === 'schedule_game_creator_agent_ready_tasks') {
scheduled = true;
return [runtimeResult];
}
if (command === 'append_local_conversation_message') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
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');
await screen.findByText('想做什么游戏?');
const scheduleButton = await screen.findByRole('button', {
name: '调度 Ready',
});
await waitFor(() => {
expect((scheduleButton as HTMLButtonElement).disabled).toBe(false);
});
fireEvent.click(scheduleButton);
await waitFor(() => {
expect(
invoke.mock.calls.some(
([command, args]) =>
command === 'schedule_game_creator_agent_ready_tasks' &&
args?.projectPath === '/tmp/authorized-game' &&
args?.limit === 16,
),
).toBe(true);
});
expect(await screen.findAllByText('已调度 1 个 Ready 任务。')).toHaveLength(
2,
);
const agentStatusList = screen.getByLabelText('Agent 状态列表');
await waitFor(() => {
const designCard = within(agentStatusList).getByRole('button', {
name: /拆解创作方向/,
});
expect(designCard.textContent).toContain('Runtime:running / planning');
expect(designCard.textContent).toContain(
'当前目标:把 ready 任务投递给对应 Agent',
);
expect(designCard.textContent).toContain(
'最近任务:running / planning · 执行 Ready 任务:拆解创作方向',
);
});
});
it('keeps ready task scheduling out of the normal user window', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
expect(screen.queryByRole('button', { name: '调度 Ready' })).toBeNull();
expect(
invoke.mock.calls.some(
([command]) => command === 'schedule_game_creator_agent_ready_tasks',
),
).toBe(false);
});
it('ignores stale agent conversation reads after switching agents', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let releaseOldConversation: (() => void) | null = null;
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (args?.agentId === 'design-director') {
return await new Promise((resolve) => {
releaseOldConversation = () =>
resolve({
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '旧 Agent 慢速消息',
agentId: 'design-director',
updatedAt: 1,
},
],
});
});
}
return {
path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl',
agentId: args?.agentId,
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '新 Agent 历史消息',
agentId: String(args?.agentId ?? ''),
updatedAt: 2,
},
],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: `/tmp/authorized-game/memory/agents/${String(
args?.taskId ?? '',
)}.md`,
content:
args?.taskId === 'art-director'
? '新 Agent 私有记忆'
: '旧 Agent 私有记忆',
exists: true,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
const agentStatusList = screen.getByLabelText('Agent 状态列表');
const designAgentButton = within(agentStatusList).getByRole('button', {
name: /拆解创作方向/,
});
await waitFor(() => {
expect((designAgentButton as HTMLButtonElement).disabled).toBe(false);
});
fireEvent.click(designAgentButton);
await waitFor(() => {
expect(releaseOldConversation).not.toBeNull();
});
fireEvent.click(
within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }),
);
expect(await screen.findByText('新 Agent 历史消息')).not.toBeNull();
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
'新 Agent 私有记忆',
);
await act(async () => {
releaseOldConversation?.();
});
expect(screen.queryByText('旧 Agent 慢速消息')).toBeNull();
expect(screen.getByText('新 Agent 历史消息')).not.toBeNull();
expect(screen.getByLabelText('Agent 私有记忆').textContent).not.toContain(
'旧 Agent 私有记忆',
);
});
it('ignores stale agent conversation saves after switching agents', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let releaseOldSave: (() => void) | null = null;
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (args?.agentId === 'art-director') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl',
agentId: 'art-director',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '新 Agent 留存消息',
agentId: 'art-director',
updatedAt: 2,
},
],
};
}
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: `/tmp/authorized-game/memory/agents/${String(
args?.taskId ?? '',
)}.md`,
content: '',
exists: false,
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
return await new Promise((resolve) => {
releaseOldSave = () =>
resolve({
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: 1,
},
],
});
});
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
const agentStatusList = screen.getByLabelText('Agent 状态列表');
const designAgentButton = within(agentStatusList).getByRole('button', {
name: /拆解创作方向/,
});
await waitFor(() => {
expect((designAgentButton as HTMLButtonElement).disabled).toBe(false);
});
fireEvent.click(designAgentButton);
const input = await screen.findByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
await screen.findByText('正在保存用户消息');
await waitFor(() => {
expect(releaseOldSave).not.toBeNull();
});
fireEvent.click(
within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }),
);
expect(await screen.findByText('新 Agent 留存消息')).not.toBeNull();
await act(async () => {
releaseOldSave?.();
});
expect(screen.getByText('新 Agent 留存消息')).not.toBeNull();
expect(screen.queryByText('旧 Agent 保存回包')).toBeNull();
expect(screen.queryByText(/已保存 1 条/)).toBeNull();
});
it('ignores stale agent reads after closing the agent dialog', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let releaseConversation: (() => void) | null = null;
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
return await new Promise((resolve) => {
releaseConversation = () =>
resolve({
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '关闭后不该写入界面状态',
agentId: 'design-director',
updatedAt: 1,
},
],
});
});
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '关闭后不该读取私有记忆',
exists: true,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
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');
await screen.findByText('想做什么游戏?');
await waitFor(() => {
expect(
(
screen.getByRole('button', {
name: '刷新 Agent',
}) as HTMLButtonElement
).disabled,
).toBe(false);
});
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
await waitFor(() => {
expect(releaseConversation).not.toBeNull();
});
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
await act(async () => {
releaseConversation?.();
});
expect(screen.queryByText('关闭后不该写入界面状态')).toBeNull();
expect(screen.queryByText('conversation.read')).toBeNull();
expect(screen.queryByText('memory.agent.read')).toBeNull();
});
it('updates the open agent dialog when agent status is refreshed', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let runReadCount = 0;
const makeTrace = (withAgentStep: boolean) =>
({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-open-agent-refresh',
commandId: 'game.generate_draft',
status: 'running',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner',
steps: withAgentStep
? [
{
pass: 1,
agent: 'Planner',
phase: 'plan',
taskId: 'design-director',
group: 'design',
role: 'Director',
status: 'running',
inputPaths: ['memory/session.md'],
outputPaths: ['.agent/spec.md'],
summary: '刷新后的拆解方向',
toolCalls: [
{
toolId: 'llm.planner.refresh',
status: 'ok',
inputPaths: ['memory/session.md'],
outputPaths: ['.agent/spec.md'],
summary: '刷新后工具调用',
},
...Array.from({ length: 5 }, (_, index) => ({
toolId: `llm.extra.${index + 1}`,
status: 'ok',
inputPaths: [],
outputPaths: [],
summary: `额外工具调用 ${index + 1}`,
})),
],
},
]
: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: withAgentStep ? ['design-director'] : [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'continue',
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 === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_project_file') {
runReadCount += 1;
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(makeTrace(runReadCount > 1)),
};
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:/tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
expect(await screen.findByLabelText('Agent 对话')).not.toBeNull();
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'暂无最近运行证据',
);
fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' }));
expect(await screen.findByText('刷新后的拆解方向')).not.toBeNull();
await waitFor(() => {
expect(screen.getByLabelText('Agent 对话').textContent).toContain(
'刷新后的拆解方向',
);
});
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'in: memory/session.md',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'out: .agent/spec.md',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'tool: llm.planner.refresh · ok · 刷新后工具调用',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'tool: llm.planner.refresh · ok · 刷新后工具调用 · in memory/session.md · out .agent/spec.md',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'还有 1 个工具调用',
);
});
it('confirms before refreshing agents when trace read policy requires it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-agent-refresh-confirm',
commandId: 'game.generate_draft',
status: 'running',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'continue',
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('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('想做什么游戏?')).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' }));
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',
});
});
});
it('cancels agent run trace refresh policy confirmation from the panel', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
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') {
throw new Error('should wait for trace confirmation');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('想做什么游戏?')).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' }));
const traceReadCommand = await screen.findByText('agent.trace_read');
fireEvent.click(
within(
traceReadCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
expect(
await screen.findByText('run: 已取消读取 Agent trace'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.anything(),
);
});
}