167b11d52e
固定首批程序实现与质量只读职责,并在计划执行前阻断质量 Agent 项目写入 要求非只读专业 Agent 完成本人修改和对应 revision 验证后再交付 补齐 Provider 瞬态重试、最终回复和 Swarm CLI 终态收敛 升级 Provider action batch v3 并兼容 v2/v1 持久恢复 扩展确定性与真实外部 Provider 塔防验收、前端状态和项目文档
3133 lines
109 KiB
TypeScript
3133 lines
109 KiB
TypeScript
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(),
|
||
);
|
||
});
|
||
}
|