Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts
T
menghao 26cfd85d25
Project CI / Frontend tests (push) Failing after 21s
Project CI / Repository checks (push) Successful in 1m1s
Project CI / Backend tests (push) Successful in 3m0s
Project CI / Native shell tests (push) Successful in 11m36s
修复AGC的macOS临时路径兼容 (#125)
规范化配置回归测试的临时根路径
补充跨平台路径比较技术说明与踩坑记录

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/125
Co-authored-by: menghao <mh18530625731@163.com>
Co-committed-by: menghao <mh18530625731@163.com>
2026-07-31 15:28:05 +08:00

7266 lines
238 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ProjectResourceCanvasLayout,
ProjectResourceCanvasPosition,
} from '../../../../packages/shared/src/contracts/gameCreationApp';
import {
consumeInitialGameChatMessage,
latestGameChatPlayableRevision,
} from '../../src/App';
import type {
AgentRuntimeEventRecord,
AgentRuntimeState,
} from '../../src/app/types';
import {
buildGameChatProgressEvidence,
collectGameChatResultImages,
collectGameChatRuntimeEvents,
SupervisorChatOnlyView,
} from '../../src/features/project-workspace/SupervisorChatOnlyView';
import {
RESOURCE_CANVAS_CARD_WIDTH,
RESOURCE_CANVAS_COLUMN_GAP,
} from '../../src/view/project-development/resourceCanvasLayoutModel';
import {
act,
agentRuntimeUserInputRequest,
App,
cleanup,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
createProjectSupervisorRuntimeHarness,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
GameChatReleaseApp,
type GameCreationAgentRunTrace,
it,
mockRoleAgentReply,
ProjectDevelopmentView,
projectSupervisorResponseStream,
React,
readFileSync,
render,
renderAppAt,
renderLauncherProjectsAt,
resolve,
screen,
submitChat,
vi,
waitFor,
within,
} from './harness';
function gameChatRuntimeEvent({
agentId = 'project-supervisor',
taskId = agentId,
sessionId = `${agentId}-session`,
runId = 'game-chat-supervisor-run',
eventType = 'agent.progress',
status = 'running',
phase = 'execution',
summary,
detail = null,
updatedAt,
}: {
agentId?: string;
taskId?: string;
sessionId?: string;
runId?: string;
eventType?: string;
status?: string;
phase?: string;
summary: string;
detail?: string | null;
updatedAt: number;
}): AgentRuntimeEventRecord {
return {
schemaVersion: 'game-creator-runtime-event.v1',
agentId,
taskId,
sessionId,
runId,
source:
agentId === 'project-supervisor'
? 'project-supervisor'
: 'agent-delegate',
eventType,
status,
phase,
summary,
detail,
updatedAt,
};
}
function gameChatRuntimeState(
overrides: Partial<AgentRuntimeState> = {},
): AgentRuntimeState {
return {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'game-chat-supervisor-session',
runId: 'game-chat-supervisor-run',
source: 'project-supervisor',
status: 'running',
phase: 'execution',
currentTask: '生成首版游戏',
currentGoal: '完成可运行原型',
currentAction: '执行当前计划',
waitingOn: '',
nextStep: '继续执行',
plan: [],
observations: [],
allowedTools: [],
pendingToolAction: null,
lastResponse: null,
error: null,
updatedAt: 1,
...overrides,
};
}
function gameChatPreviewPlaytestRuntime({
parentRunId,
revision,
updatedAt,
}: {
parentRunId: string;
revision: number;
updatedAt: number;
}): AgentRuntimeState {
const runId = `preview-playtest-${parentRunId}-${revision}`;
const sessionId = `preview-playtest-session-${revision}`;
return gameChatRuntimeState({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId,
runId,
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId,
status: 'completed',
phase: 'completed',
currentTask: '执行浏览器试玩验证',
currentGoal: '确认当前 revision 可以试玩',
currentAction: '浏览器试玩验证已通过',
recentEvents: [
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId,
runId,
eventType: 'observation',
status: 'completed',
phase: 'tool-observation',
summary: 'preview.validate:ok · 浏览器验证已通过',
detail: JSON.stringify({
diagnosticsCount: 0,
passed: true,
playtestPassed: true,
revision,
}),
updatedAt,
}),
],
updatedAt,
});
}
function renderGameChatStatus({
runtime,
runtimeByAgentId = {},
}: {
runtime: AgentRuntimeState;
runtimeByAgentId?: Record<string, AgentRuntimeState | undefined>;
}) {
return render(
React.createElement(SupervisorChatOnlyView, {
chatAgentBusy: false,
chatInput: '',
messagesRef: React.createRef<HTMLDivElement>(),
onCancelConfirmation: vi.fn(),
onChatInputChange: vi.fn(),
onCloseRuntimeConfig: vi.fn(),
onConfirmConfirmation: vi.fn(),
onOpenRuntimeConfig: vi.fn(),
onScroll: vi.fn(),
onShowEarlierMessages: vi.fn(),
onSubmit: vi.fn(),
onToolAction: vi.fn(),
onUserInput: vi.fn(),
pendingConfirmation: null,
projectPath: '/tmp/game-chat-status',
runtime,
runtimeConfigOpen: false,
runtimeError: '',
transientReply: '',
hasConversationControls: false,
hiddenConversationCount: 0,
needsUserInput: false,
visibleMessages: [],
workspaceStatus: '已打开',
gameChatMode: true,
runtimeByAgentId,
projectReady: true,
}),
);
}
function createDeferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((nextResolve, nextReject) => {
resolve = nextResolve;
reject = nextReject;
});
return { promise, reject, resolve };
}
type GameChatPreviewFixture = {
port: number;
root: string;
url: string;
};
async function renderGameChatAutoPreviewDriver({
projectPath,
port,
readPolicy,
startPreview,
}: {
projectPath: string;
port: number;
readPolicy?: (callIndex: number) => Promise<Record<string, unknown>>;
startPreview?: (
args: Record<string, unknown>,
callIndex: number,
) => Promise<GameChatPreviewFixture>;
}) {
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const manifest = createGameCreationAppManifest(
projectPath.split(/[\\/]/u).filter(Boolean).at(-1) ?? 'game-chat-race',
'game-chat-race',
);
manifest.tasks = manifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
let parentRunId = '';
let currentPreview: GameChatPreviewFixture | null = null;
let policyCallCount = 0;
let startCallCount = 0;
const stoppedPreviews: GameChatPreviewFixture[] = [];
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: manifest.name,
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return currentPreview
? { status: 'running', ...currentPreview }
: { status: 'stopped', url: null, port: null, root: null };
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'read_project_permission_policy') {
policyCallCount += 1;
return readPolicy
? readPolicy(policyCallCount)
: {
path: '.agent/policy.json',
policy: { deniedCommands: [], confirmCommands: [] },
};
}
if (command === 'start_local_game_preview') {
startCallCount += 1;
const started = startPreview
? await startPreview(args, startCallCount)
: {
url: `http://127.0.0.1:${port}`,
port,
root: `${projectPath}/game`,
};
currentPreview = started;
return started;
}
if (command === 'stop_local_game_preview_if_matches') {
const expected = args.expectedPreview as GameChatPreviewFixture;
const matches =
currentPreview?.url === expected?.url &&
currentPreview.port === expected.port &&
currentPreview.root === expected.root;
if (matches) {
stoppedPreviews.push(expected);
currentPreview = null;
}
return matches;
}
if (command === 'start_game_creator_supervisor_runtime_task') {
parentRunId = String(args.runId ?? '');
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
const send = within(surface).getByRole('button', { name: '发送' });
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
expect((send as HTMLButtonElement).disabled).toBe(false);
});
const submit = async (prompt: string) => {
const previousSubmissionCount = invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task' ||
command === 'steer_game_creator_agent_runtime_task',
).length;
fireEvent.change(composer, { target: { value: prompt } });
fireEvent.click(send);
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task' ||
command === 'steer_game_creator_agent_runtime_task',
),
).toHaveLength(previousSubmissionCount + 1);
});
await waitFor(() => {
expect((send as HTMLButtonElement).disabled).toBe(false);
});
};
const emitValidation = async (revision: number, validatedAt: number) => {
harness.setProjectRevision(revision);
await act(async () => {
harness.emitAgentRuntime(
gameChatPreviewPlaytestRuntime({
parentRunId,
revision,
updatedAt: validatedAt,
}),
);
await Promise.resolve();
});
};
const readAuthorization = () => {
const raw = window.localStorage.getItem(
'genarrative.game-chat.auto-preview-authorization.v2',
);
return raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
};
return {
emitValidation,
get currentPreview() {
return currentPreview;
},
get parentRunId() {
return parentRunId;
},
harness,
invoke,
readAuthorization,
stoppedPreviews,
submit,
surface,
};
}
async function assertNewGameChatAuthorizationSupersedesDeferredAttempt(
deferredStage: 'policy' | 'start',
) {
const projectPath = `/tmp/game-chat-deferred-${deferredStage}`;
const deferredPolicy = createDeferred<Record<string, unknown>>();
const deferredStart = createDeferred<GameChatPreviewFixture>();
const policyResult = {
path: '.agent/policy.json',
policy: { deniedCommands: [], confirmCommands: [] },
};
const staleStartedPreview = {
url: 'http://127.0.0.1:4391',
port: 4391,
root: `${projectPath}/game`,
};
const driver = await renderGameChatAutoPreviewDriver({
projectPath,
port: 4391,
readPolicy: async (callIndex) =>
deferredStage === 'policy' && callIndex === 1
? deferredPolicy.promise
: policyResult,
startPreview: async (args, callIndex) => {
expect(args).toEqual({
projectPath,
expectedRevision: deferredStage === 'policy' ? 2 : callIndex,
});
if (deferredStage === 'start' && callIndex === 1) {
return deferredStart.promise;
}
return {
url: 'http://127.0.0.1:4391',
port: 4391,
root: `${projectPath}/game`,
};
},
});
await driver.submit('生成首个可玩版本');
await driver.emitValidation(1, 1000);
await waitFor(() => {
const reachedDeferredStage = driver.invoke.mock.calls.some(
([command]) =>
command ===
(deferredStage === 'policy'
? 'read_project_permission_policy'
: 'start_local_game_preview'),
);
expect(reachedDeferredStage).toBe(true);
});
const firstAuthorization = driver.readAuthorization();
expect(firstAuthorization?.authorizationId).toEqual(expect.any(String));
await driver.submit('同一 Run 内替换自动预览授权');
await waitFor(() => {
const nextAuthorization = driver.readAuthorization();
expect(nextAuthorization?.authorizationId).toEqual(expect.any(String));
expect(nextAuthorization?.authorizationId).not.toBe(
firstAuthorization?.authorizationId,
);
expect(nextAuthorization).toMatchObject({
afterRevision: 1,
afterValidatedAt: 1000,
projectPath,
runId: driver.parentRunId,
});
});
const replacementAuthorizationId =
driver.readAuthorization()?.authorizationId;
await act(async () => {
if (deferredStage === 'policy') {
deferredPolicy.resolve(policyResult);
await deferredPolicy.promise;
} else {
deferredStart.resolve(staleStartedPreview);
await deferredStart.promise;
}
await Promise.resolve();
await Promise.resolve();
});
if (deferredStage === 'start') {
await waitFor(() => {
expect(driver.stoppedPreviews).toEqual([staleStartedPreview]);
});
expect(driver.invoke).toHaveBeenCalledWith(
'stop_local_game_preview_if_matches',
{ projectPath, expectedPreview: staleStartedPreview },
);
} else {
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(0);
}
expect(driver.readAuthorization()?.authorizationId).toBe(
replacementAuthorizationId,
);
expect(screen.queryByLabelText('游戏运行')).toBeNull();
await driver.emitValidation(2, 2000);
expect(
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
).not.toBeNull();
const expectedStartCount = deferredStage === 'start' ? 2 : 1;
await waitFor(() => {
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(expectedStartCount);
});
expect(
driver.invoke.mock.calls
.filter(([command]) => command === 'start_local_game_preview')
.at(-1),
).toEqual(['start_local_game_preview', { projectPath, expectedRevision: 2 }]);
expect(driver.readAuthorization()).toBeNull();
}
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, moves resources by pointer, 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 resourceCards = screen.getAllByTitle('拖动调整资源位置');
const artReceiptCard = resourceCards.find((card) =>
card.textContent?.includes('美术资源计划 Agent 文本回执'),
);
expect(artReceiptCard).not.toBeUndefined();
fireEvent.pointerDown(artReceiptCard!, {
pointerId: 7,
button: 0,
clientX: 0,
clientY: 0,
});
fireEvent.pointerMove(artReceiptCard!, {
pointerId: 7,
clientX: 240,
clientY: 32,
});
fireEvent.pointerUp(artReceiptCard!, {
pointerId: 7,
clientX: 240,
clientY: 32,
});
expect(artReceiptCard?.getAttribute('style')).toContain(
'--resource-x: 240px',
);
fireEvent.click(artReceiptCard!);
fireEvent.click(artReceiptCard!);
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('persists a resource position with CAS and restores it after remount', async () => {
const projectId = 'workbench-layout-persistence';
const projectPath = '/tmp/workbench-layout-persistence';
const resourceId = 'agent-result:design-foundation:layout-result-run';
const manifest = createGameCreationAppManifest(projectId, '布局持久化测试');
const agentResults = [
{
agentId: 'design-foundation',
runId: 'layout-result-run',
label: '玩法策划 Agent',
title: '布局持久化回执',
content: '布局持久化正文',
updatedAt: 1,
},
];
let persistedLayout: ProjectResourceCanvasLayout = {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode: 'dependency',
revision: 1,
positions: [
{
resourceId,
section: 'document',
x: 12,
y: 24,
manuallyPlaced: true,
},
],
updatedAt: 100,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_canvas_layout') {
return structuredClone(persistedLayout);
}
if (command === 'update_local_project_resource_canvas_layout') {
const input = args as {
expectedRevision: number;
positions: ProjectResourceCanvasPosition[];
};
expect(input.expectedRevision).toBe(persistedLayout.revision);
persistedLayout = {
...persistedLayout,
revision: persistedLayout.revision + 1,
positions: structuredClone(input.positions),
updatedAt: persistedLayout.updatedAt + 1,
};
return {
status: 'updated',
layout: structuredClone(persistedLayout),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
function renderWorkbench() {
render(
React.createElement(ProjectDevelopmentView, {
projectName: '布局持久化测试',
projectPath,
manifest,
attachments: [],
agentResults,
supervisor: React.createElement('div', null, '项目总控'),
}),
);
}
renderWorkbench();
const card = screen.getByText('布局持久化回执').closest('button');
expect(card).not.toBeNull();
await waitFor(() => {
expect(card?.getAttribute('style')).toContain('--resource-x: 12px');
expect(card?.getAttribute('style')).toContain('--resource-y: 24px');
});
fireEvent.pointerDown(card!, {
pointerId: 11,
button: 0,
clientX: 20,
clientY: 30,
});
fireEvent.pointerMove(card!, {
pointerId: 11,
clientX: 100,
clientY: 60,
});
fireEvent.pointerUp(card!, {
pointerId: 11,
clientX: 100,
clientY: 60,
});
await waitFor(() => {
expect(persistedLayout.revision).toBe(2);
expect(persistedLayout.positions).toContainEqual({
resourceId,
section: 'document',
x: 92,
y: 54,
manuallyPlaced: true,
});
});
expect(invoke).toHaveBeenCalledWith(
'update_local_project_resource_canvas_layout',
expect.objectContaining({
projectPath,
mode: 'dependency',
expectedRevision: 1,
}),
);
cleanup();
renderWorkbench();
const restoredCard = screen.getByText('布局持久化回执').closest('button');
await waitFor(() => {
expect(restoredCard?.getAttribute('style')).toContain(
'--resource-x: 92px',
);
expect(restoredCard?.getAttribute('style')).toContain(
'--resource-y: 54px',
);
});
});
it('loads the latest layout after a CAS conflict without replaying the drag', async () => {
const projectId = 'workbench-layout-conflict';
const projectPath = '/tmp/workbench-layout-conflict';
const resourceId = 'agent-result:design-foundation:conflict-result-run';
const manifest = createGameCreationAppManifest(projectId, '布局冲突测试');
const latestLayout: ProjectResourceCanvasLayout = {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode: 'dependency',
revision: 4,
positions: [
{
resourceId,
section: 'document',
x: 400,
y: 80,
manuallyPlaced: true,
},
],
updatedAt: 400,
};
const invoke = vi.fn(async (command: string) => {
if (command === 'read_local_project_resource_canvas_layout') {
return {
...structuredClone(latestLayout),
revision: 3,
positions: [
{
...latestLayout.positions[0],
x: 20,
y: 30,
},
],
};
}
if (command === 'update_local_project_resource_canvas_layout') {
return { status: 'conflict', layout: structuredClone(latestLayout) };
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: '布局冲突测试',
projectPath,
manifest,
attachments: [],
agentResults: [
{
agentId: 'design-foundation',
runId: 'conflict-result-run',
label: '玩法策划 Agent',
title: '布局冲突回执',
content: '布局冲突正文',
updatedAt: 1,
},
],
supervisor: React.createElement('div', null, '项目总控'),
}),
);
const card = screen.getByText('布局冲突回执').closest('button');
await waitFor(() => {
expect(card?.getAttribute('style')).toContain('--resource-x: 20px');
});
fireEvent.pointerDown(card!, {
pointerId: 12,
button: 0,
clientX: 0,
clientY: 0,
});
fireEvent.pointerMove(card!, {
pointerId: 12,
clientX: 200,
clientY: 40,
});
fireEvent.pointerUp(card!, {
pointerId: 12,
clientX: 200,
clientY: 40,
});
expect(
await screen.findByText('布局已在其他窗口更新,请重新拖动'),
).not.toBeNull();
expect(card?.getAttribute('style')).toContain('--resource-x: 400px');
expect(card?.getAttribute('style')).toContain('--resource-y: 80px');
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'update_local_project_resource_canvas_layout',
),
).toHaveLength(1);
});
it('drops manual writes queued behind a CAS conflict so stale coordinates cannot overwrite the winner', async () => {
const projectId = 'workbench-layout-queued-conflict';
const projectPath = '/tmp/workbench-layout-queued-conflict';
const resourceId = 'agent-result:design-foundation:queued-conflict-run';
const manifest = createGameCreationAppManifest(
projectId,
'布局排队冲突测试',
);
const latestLayout: ProjectResourceCanvasLayout = {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode: 'dependency',
revision: 2,
positions: [
{
resourceId,
section: 'document',
x: 400,
y: 80,
manuallyPlaced: true,
},
],
updatedAt: 200,
};
let resolveFirstUpdate:
| ((result: {
status: 'conflict';
layout: ProjectResourceCanvasLayout;
}) => void)
| null = null;
let updateCalls = 0;
const invoke = vi.fn(async (command: string) => {
if (command === 'read_local_project_resource_canvas_layout') {
return {
...structuredClone(latestLayout),
revision: 1,
positions: [{ ...latestLayout.positions[0], x: 20, y: 30 }],
};
}
if (command === 'update_local_project_resource_canvas_layout') {
updateCalls += 1;
return await new Promise((resolve) => {
resolveFirstUpdate = resolve;
});
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: '布局排队冲突测试',
projectPath,
manifest,
attachments: [],
agentResults: [
{
agentId: 'design-foundation',
runId: 'queued-conflict-run',
label: '玩法策划 Agent',
title: '排队冲突回执',
content: '排队冲突正文',
updatedAt: 1,
},
],
supervisor: React.createElement('div', null, '项目总控'),
}),
);
const card = screen.getByText('排队冲突回执').closest('button');
await waitFor(() => {
expect(card?.getAttribute('style')).toContain('--resource-x: 20px');
});
for (const [pointerId, startX, endX] of [
[31, 0, 100],
[32, 100, 220],
] as const) {
fireEvent.pointerDown(card!, {
pointerId,
button: 0,
clientX: startX,
clientY: 0,
});
fireEvent.pointerMove(card!, {
pointerId,
clientX: endX,
clientY: 20,
});
fireEvent.pointerUp(card!, {
pointerId,
clientX: endX,
clientY: 20,
});
}
expect(updateCalls).toBe(1);
await act(async () => {
resolveFirstUpdate?.({
status: 'conflict',
layout: structuredClone(latestLayout),
});
await Promise.resolve();
});
expect(
await screen.findByText('布局已在其他窗口更新,请重新拖动'),
).not.toBeNull();
await waitFor(() => {
expect(card?.getAttribute('style')).toContain('--resource-x: 400px');
expect(card?.getAttribute('style')).toContain('--resource-y: 80px');
});
expect(updateCalls).toBe(1);
});
it('maps manifest asset kinds into stable type layout ordering', async () => {
const projectId = 'workbench-layout-asset-subtypes';
const manifest = createGameCreationAppManifest(
projectId,
'资源子类型排序测试',
);
manifest.assets.push(
{
id: 'ui-prototype-first-by-label',
kind: 'ui-prototype',
mediaType: 'image/png',
localPath: 'assets/a-ui-prototype.png',
source: { kind: 'generated', taskId: 'design-foundation' },
},
{
id: 'art-spritesheet-last-by-label',
kind: 'art-spritesheet',
mediaType: 'image/png',
localPath: 'assets/z-art-spritesheet.png',
source: { kind: 'generated', taskId: 'art-asset-plan' },
},
);
const updates: Array<{
mode: 'dependency' | 'type';
positions: ProjectResourceCanvasPosition[];
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
const mode = args?.mode as 'dependency' | 'type';
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
if (command === 'update_local_project_resource_canvas_layout') {
const positions = structuredClone(
args?.positions as ProjectResourceCanvasPosition[],
);
updates.push({ mode, positions });
return {
status: 'updated',
layout: {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode,
revision: 1,
positions,
updatedAt: 1,
},
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: '资源子类型排序测试',
projectPath: '/tmp/workbench-layout-asset-subtypes',
manifest,
attachments: [],
agentResults: [],
supervisor: React.createElement('div', null, '项目总控'),
}),
);
await waitFor(() =>
expect(updates.some(({ mode }) => mode === 'dependency')).toBe(true),
);
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
await waitFor(() =>
expect(updates.some(({ mode }) => mode === 'type')).toBe(true),
);
const typePositions = updates.find(
({ mode }) => mode === 'type',
)?.positions;
expect(typePositions).toEqual(
expect.arrayContaining([
expect.objectContaining({
resourceId: 'asset:art-spritesheet-last-by-label',
x: 0,
y: 0,
}),
expect.objectContaining({
resourceId: 'asset:ui-prototype-first-by-label',
x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,
y: 0,
}),
]),
);
});
it('keeps newly reconciled resources visible when their automatic layout save fails', async () => {
const projectId = 'workbench-layout-save-failure';
const manifest = createGameCreationAppManifest(projectId, '布局失败测试');
const invoke = vi.fn(async (command: string) => {
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode: 'dependency',
revision: 2,
positions: [],
updatedAt: 200,
};
}
if (command === 'update_local_project_resource_canvas_layout') {
throw new Error('disk full');
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: '布局失败测试',
projectPath: '/tmp/workbench-layout-save-failure',
manifest,
attachments: [],
agentResults: [
{
agentId: 'design-foundation',
runId: 'save-failure-run',
label: '玩法策划 Agent',
title: '自动排版失败回执',
content: '自动排版失败正文',
updatedAt: 1,
},
],
supervisor: React.createElement('div', null, '项目总控'),
}),
);
expect(
await screen.findByText('布局保存失败,已保留当前会话布局'),
).not.toBeNull();
expect(
screen.getByText('自动排版失败回执').closest('button'),
).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.mock.calls.filter(
([command]) => command === 'read_local_project_image_preview',
),
).toHaveLength(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('keeps the Project Supervisor welcome and empty runtime surface before the first message', async () => {
const projectPath = '/tmp/launcher-empty-supervisor-game';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-empty-supervisor-game',
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
initialSessionExists: false,
});
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-empty-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('项目总控对话');
const messageList =
within(supervisorSurface).getByLabelText('项目总控消息');
expect(
await within(messageList).findByText('想做什么游戏?'),
).not.toBeNull();
expect(
within(supervisorSurface).getByLabelText('项目总控 Agent 状态'),
).not.toBeNull();
expect(
within(supervisorSurface).getByText('项目总控 Agent · 尚未开始'),
).not.toBeNull();
expect(
within(supervisorSurface).getByText('告诉陶泥儿你想做什么游戏'),
).not.toBeNull();
expect(
within(supervisorSurface).getByRole('button', { name: '发送' }),
).toHaveProperty('disabled', false);
});
it('hydrates a persisted needs-reconciliation Supervisor runtime without an active Session index', async () => {
const projectPath = '/tmp/launcher-reconciliation-supervisor-game';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-reconciliation-supervisor-game',
);
const reconciliationRuntime = {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'persisted-supervisor-session',
runId: 'persisted-reconciliation-run',
source: 'project-supervisor',
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
currentTask: '帮我生成一个贪吃蛇',
currentGoal: '完成贪吃蛇原型',
currentAction: '等待核对 Provider 回复交接',
waitingOn: '人工核对',
nextStep: '核对后继续或取消',
plan: [],
observations: [],
allowedTools: [],
pendingToolAction: null,
lastResponse: null,
error: 'tool-plan-unknown',
updatedAt: 7000,
};
const cancelledQueue = {
total: 2,
pending: 1,
running: 0,
waitingForConfirmation: 0,
waitingForUserInput: 0,
cancelled: 1,
completed: 0,
failed: 0,
latestRunId: 'persisted-reconciliation-run',
updatedAt: 8000,
};
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
initialSessionExists: false,
initialRuntime: reconciliationRuntime,
runtimeMapLoader: async () => [reconciliationRuntime],
});
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-reconciliation-supervisor-game',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'cancel_game_creator_agent_runtime_task') {
const state = supervisorHarness.runtimeState({
...reconciliationRuntime,
status: 'cancelled',
phase: 'cancelled',
currentAction: '待核对的旧任务已结束',
waitingOn: '队列中的下一个任务',
error: null,
taskQueue: cancelledQueue,
updatedAt: 8000,
});
return {
...supervisorHarness.runtimeResult(state),
taskQueue: cancelledQueue,
};
}
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('项目总控 Agent · 失败'),
).not.toBeNull();
expect(
within(supervisorSurface).getByText('当前阶段:待核对'),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_game_creator_agent_runtimes', {
projectPath,
});
const reconcileButton = within(supervisorSurface).getByRole('button', {
name: '已核对,结束旧任务',
});
fireEvent.click(reconcileButton);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'cancel_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
runId: 'persisted-reconciliation-run',
},
);
});
expect(invoke).not.toHaveBeenCalledWith(
'confirm_retry_game_creator_agent_runtime_task',
expect.anything(),
);
expect(
await within(supervisorSurface).findByText(
'旧任务已结束,队列中还有 1 个待处理任务,队列将继续处理',
),
).not.toBeNull();
expect(
within(supervisorSurface).queryByRole('button', {
name: '在当前项目重试总控',
}),
).toBeNull();
});
it('allows retrying a cancelled reconciled Supervisor only after its queue is empty', async () => {
const projectPath = '/tmp/launcher-reconciliation-empty-queue';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-reconciliation-empty-queue',
);
const runId = 'reconciliation-empty-queue-run';
const reconciliationRuntime = {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'reconciliation-empty-queue-session',
runId,
source: 'project-supervisor',
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
currentTask: '生成贪吃蛇原型',
currentGoal: '完成可玩原型',
currentAction: '等待核对 Provider 回复交接',
waitingOn: '人工核对',
nextStep: '核对后结束旧任务',
plan: [],
observations: [],
allowedTools: [],
pendingToolAction: null,
lastResponse: null,
error: 'tool-plan-unknown',
updatedAt: 7000,
};
const emptyQueue = {
total: 1,
pending: 0,
running: 0,
waitingForConfirmation: 0,
waitingForUserInput: 0,
cancelled: 1,
completed: 0,
failed: 0,
latestRunId: runId,
updatedAt: 8000,
};
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
sessionId: 'reconciliation-empty-queue-session',
initialSessionExists: false,
initialRuntime: reconciliationRuntime,
runtimeMapLoader: async () => [reconciliationRuntime],
});
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-reconciliation-empty-queue',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'cancel_game_creator_agent_runtime_task') {
const state = supervisorHarness.runtimeState({
...reconciliationRuntime,
status: 'cancelled',
phase: 'cancelled',
currentAction: '待核对的旧任务已结束',
waitingOn: '',
error: null,
taskQueue: emptyQueue,
updatedAt: 8000,
});
return {
...supervisorHarness.runtimeResult(state),
taskQueue: emptyQueue,
};
}
if (command === 'confirm_retry_game_creator_agent_runtime_task') {
const nextRunId = String(args?.nextRunId ?? '');
const state = supervisorHarness.runtimeState({
...reconciliationRuntime,
runId: nextRunId,
status: 'running',
phase: 'planning',
currentAction: '重新生成项目总控计划',
waitingOn: 'Agent 输出计划或回复',
error: null,
updatedAt: 9000,
});
return {
...supervisorHarness.runtimeResult(state),
acceptedRunId: nextRunId,
};
}
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('项目总控对话');
fireEvent.click(
await within(supervisorSurface).findByRole('button', {
name: '已核对,结束旧任务',
}),
);
expect(
await within(supervisorSurface).findByText(
'旧任务已结束,当前队列为空,可重新启动项目总控',
),
).not.toBeNull();
const retryButton = await within(supervisorSurface).findByRole('button', {
name: '在当前项目重试总控',
});
fireEvent.click(retryButton);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_retry_game_creator_agent_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
runId,
nextRunId: expect.stringMatching(/^project-supervisor-retry-/),
},
);
});
const cancelCallIndex = invoke.mock.calls.findIndex(
([command]) => command === 'cancel_game_creator_agent_runtime_task',
);
const retryCallIndex = invoke.mock.calls.findIndex(
([command]) =>
command === 'confirm_retry_game_creator_agent_runtime_task',
);
expect(cancelCallIndex).toBeGreaterThanOrEqual(0);
expect(retryCallIndex).toBeGreaterThan(cancelCallIndex);
});
it('starts the game-chat surface as project-free chat with project and settings controls', () => {
render(
React.createElement(App, {
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = screen.getByLabelText('游戏创作聊天');
expect(within(surface).getByText('未选择项目')).not.toBeNull();
expect(within(surface).getByLabelText('最新状态')).not.toBeNull();
expect(within(surface).getByText('请选择项目目录')).not.toBeNull();
expect(
within(surface).getByRole('button', { name: '选择项目路径' }),
).not.toBeNull();
expect(
within(surface).getByRole('button', { name: '设置' }),
).not.toBeNull();
expect(
(
within(surface).getByLabelText(
'项目总控对话内容',
) as HTMLTextAreaElement
).disabled,
).toBe(true);
expect(screen.queryByLabelText('游戏运行')).toBeNull();
expect(document.querySelector('iframe')).toBeNull();
expect(document.querySelector('.game-chat-shell')?.className).toBe(
'game-chat-shell',
);
});
it('opens the dedicated game-chat release without platform authentication', async () => {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockRejectedValue(new Error('platform api-server unavailable'));
render(React.createElement(GameChatReleaseApp));
await act(async () => {
await Promise.resolve();
});
expect(screen.getByLabelText('游戏创作聊天')).not.toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});
it('submits one initial game-chat message per page across StrictMode remounts and terminal updates', async () => {
const projectPath = '/tmp/game-chat-initial-message-latch';
const prompt = '继续完成当前贪吃蛇';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const manifest = createGameCreationAppManifest(
'game-chat-initial-message-latch',
'初始消息闩锁',
);
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: '初始消息闩锁',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
const renderInitialMessageApp = (message: string) =>
React.createElement(
React.StrictMode,
null,
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
initialSupervisorMessage: message,
}),
);
const rendered = render(renderInitialMessageApp(prompt));
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const firstRunId = String(
invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
)?.[1]?.runId ?? '',
);
expect(firstRunId).not.toBe('');
for (const [index, status] of [
'cancelled',
'failed',
'completed',
].entries()) {
act(() => {
harness.emitRuntime(
harness.runtimeState({
runId: firstRunId,
status,
phase: status,
updatedAt: 4000 + index,
}),
);
});
rendered.rerender(renderInitialMessageApp(`${prompt}-${status}`));
}
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
const initCallCount = invoke.mock.calls.filter(
([command]) => command === 'init_local_game_project',
).length;
rendered.unmount();
render(renderInitialMessageApp(prompt));
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'init_local_game_project',
).length,
).toBeGreaterThan(initCallCount);
expect(
(screen.getByLabelText('项目总控对话内容') as HTMLTextAreaElement)
.disabled,
).toBe(false);
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
it('removes the initial game-chat message from the launch URL before a page reload', () => {
const searchParams = new URLSearchParams(
'game-chat=&projectPath=%2Ftmp%2Ftcs&initialMessage=%E7%BB%A7%E7%BB%AD%E8%B4%AA%E5%90%83%E8%9B%87',
);
let replacedUrl = '';
expect(
consumeInitialGameChatMessage(
searchParams,
{ pathname: '/index.html', hash: '#game-chat' },
(url) => {
replacedUrl = url;
},
),
).toBe('继续贪吃蛇');
expect(searchParams.has('initialMessage')).toBe(false);
expect(replacedUrl).not.toContain('initialMessage');
expect(replacedUrl).toContain('projectPath=%2Ftmp%2Ftcs');
expect(
consumeInitialGameChatMessage(
searchParams,
{ pathname: '/index.html', hash: '#game-chat' },
() => {
throw new Error('重载后不应再次改写 URL');
},
),
).toBe('');
});
it('does not deliver a launch message when the opened game-chat project changed', async () => {
const launchProjectPath = '/tmp/game-chat-initial-project';
const openedProjectPath = '/tmp/game-chat-replacement-project';
const prompt = '这条消息只能投递给启动项目';
const harness = createProjectSupervisorRuntimeHarness({
projectPath: openedProjectPath,
});
const manifest = createGameCreationAppManifest(
'game-chat-replacement-project',
'替换项目',
);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'inspect_local_project_directory') {
return {
projectPath: launchProjectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: true,
projectName: '启动项目',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'init_local_game_project') {
return {
projectPath: openedProjectPath,
manifestPath: `${openedProjectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: launchProjectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
initialSupervisorMessage: prompt,
}),
);
await waitFor(() => {
expect(
(screen.getByLabelText('项目总控对话内容') as HTMLTextAreaElement)
.disabled,
).toBe(false);
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
projectPath: openedProjectPath,
});
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(0);
expect(screen.queryByText(prompt)).toBeNull();
});
it('keeps the non-empty folder confirmation when game-chat initializes a picked project', async () => {
const projectPath = '/tmp/game-chat-non-empty';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'pick_local_project_directory') {
return projectPath;
}
if (command === 'inspect_local_project_directory') {
return {
projectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: false,
projectName: null,
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'is_local_project_directory_non_empty') {
return true;
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
fireEvent.click(screen.getByRole('button', { name: '选择项目路径' }));
const dialog = await screen.findByRole('dialog', {
name: '文件夹不是空的',
});
expect(within(dialog).getByText(projectPath)).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'init_local_game_project',
expect.anything(),
);
fireEvent.click(within(dialog).getByRole('button', { name: '继续新建' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('init_local_game_project', {
projectPath,
projectId: 'local-project-draft',
name: 'game-chat-non-empty',
});
});
expect(await screen.findByText('game-chat-non-empty')).not.toBeNull();
});
it('embeds only a running loopback preview in the game-chat surface with the shared sandbox', async () => {
const projectPath = '/tmp/game-chat-loopback';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
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: 'game-chat-loopback',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'running',
url: 'http://127.0.0.1:4173',
port: 4173,
root: `${projectPath}/game`,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const preview = await screen.findByLabelText('游戏运行');
const iframe = within(preview).getByTitle(
'game-chat-loopback 游戏运行画面',
);
expect(iframe.getAttribute('src')).toBe('http://127.0.0.1:4173/');
expect(iframe.getAttribute('sandbox')).toBe(
'allow-scripts allow-same-origin allow-forms allow-pointer-lock',
);
expect(iframe.getAttribute('allow')).toBe('autoplay; fullscreen; gamepad');
expect(
document.querySelector('.game-chat-shell.has-preview'),
).not.toBeNull();
});
it('routes game-chat /preview through the built-in confirmation flow instead of starting an autonomous run', async () => {
const projectPath = '/tmp/game-chat-manual-preview';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
let previewStarted = false;
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: 'game-chat-manual-preview',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return previewStarted
? {
status: 'running',
url: 'http://127.0.0.1:4310',
port: 4310,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['preview.start'],
},
};
}
if (command === 'start_local_game_preview') {
previewStarted = true;
return {
status: 'running',
url: 'http://127.0.0.1:4310',
port: 4310,
root: `${projectPath}/game`,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, { target: { value: '/preview' } });
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
const commandConfirmation =
await within(surface).findByLabelText('项目总控 Agent 待确认命令');
expect(
within(commandConfirmation).getByText('preview.start'),
).not.toBeNull();
expect(
invoke.mock.calls.some(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toBe(false);
fireEvent.click(
within(commandConfirmation).getByRole('button', { name: '确认' }),
);
const policyConfirmation =
await within(surface).findByLabelText('项目总控 Agent 待确认操作');
fireEvent.click(
within(policyConfirmation).getByRole('button', { name: '确认' }),
);
expect(await screen.findByLabelText('游戏运行')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath,
});
expect(
invoke.mock.calls.some(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toBe(false);
});
it('removes the game-chat game region after the registered preview stops', async () => {
const projectPath = '/tmp/game-chat-stopped-preview';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
let previewStatusReads = 0;
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: 'game-chat-stopped-preview',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
previewStatusReads += 1;
return previewStatusReads === 1
? {
status: 'running',
url: 'http://127.0.0.1:4174',
port: 4174,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
expect(await screen.findByLabelText('游戏运行')).not.toBeNull();
await waitFor(
() => {
expect(screen.queryByLabelText('游戏运行')).toBeNull();
expect(
document.querySelector('.game-chat-shell.has-preview'),
).toBeNull();
},
{ timeout: 2500 },
);
});
it('does not render a game region for a running remote preview URL', async () => {
const projectPath = '/tmp/game-chat-remote-preview';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
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: 'game-chat-remote-preview',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'running',
url: 'https://preview.example.com/game',
port: 443,
root: `${projectPath}/game`,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', {
projectPath,
});
});
expect(await screen.findByLabelText('游戏创作聊天')).not.toBeNull();
expect(screen.queryByLabelText('游戏运行')).toBeNull();
expect(document.querySelector('iframe')).toBeNull();
expect(document.querySelector('.game-chat-shell.has-preview')).toBeNull();
});
it('filters game-chat events to the active parent run, sorts and deduplicates them', () => {
const duplicate = gameChatRuntimeEvent({
runId: 'active-parent-run',
summary: '总控事件 2',
updatedAt: 20,
});
const supervisor = gameChatRuntimeState({
runId: 'active-parent-run',
recentEvents: [
gameChatRuntimeEvent({
runId: 'active-parent-run',
summary: '总控事件 1',
updatedAt: 10,
}),
gameChatRuntimeEvent({
runId: 'previous-parent-run',
summary: '总控旧 run 事件',
updatedAt: 50,
}),
duplicate,
duplicate,
],
});
const currentChild = gameChatRuntimeState({
agentId: 'design-foundation',
taskId: 'design-foundation',
sessionId: 'design-session',
runId: 'design-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: 'active-parent-run',
updatedAt: 30,
recentEvents: [
gameChatRuntimeEvent({
agentId: 'design-foundation',
runId: 'design-run',
summary: '当前子 Agent 事件',
updatedAt: 30,
}),
gameChatRuntimeEvent({
agentId: 'design-foundation',
runId: 'previous-design-run',
summary: '子 Agent 旧 run 事件',
updatedAt: 60,
}),
],
});
const staleChild = gameChatRuntimeState({
agentId: 'art-asset-plan',
taskId: 'art-asset-plan',
sessionId: 'art-session',
runId: 'art-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: 'stale-parent-run',
updatedAt: 40,
recentEvents: [
gameChatRuntimeEvent({
agentId: 'art-asset-plan',
runId: 'art-run',
summary: '过期父 run 事件',
updatedAt: 40,
}),
],
});
expect(supervisor.recentEvents?.map((event) => event.summary)).toEqual([
'总控事件 1',
'总控旧 run 事件',
'总控事件 2',
'总控事件 2',
]);
const events = collectGameChatRuntimeEvents(supervisor, {
'design-foundation': currentChild,
'art-asset-plan': staleChild,
});
expect(events.map((item) => item.event.summary)).toEqual([
'当前子 Agent 事件',
'总控事件 2',
'总控事件 1',
]);
expect(events.map((item) => item.agentLabel)).toEqual([
'玩法策划 Agent',
'项目总控 Agent',
'项目总控 Agent',
]);
});
it('keeps an unstructured image inspection neutral instead of presenting tool success as visual approval', () => {
const runId = 'game-chat-unstructured-image-inspection';
const runtime = gameChatRuntimeState({
runId,
recentEvents: [
gameChatRuntimeEvent({
runId,
eventType: 'observation',
status: 'ok',
phase: 'tool-observation',
summary: 'image.inspect:ok · 视觉检查已完成,共分析 2 张图片',
detail: JSON.stringify({
inspectionKind: null,
validationProfile: null,
passed: null,
checks: null,
issues: null,
}),
updatedAt: 25,
}),
],
});
const progress = buildGameChatProgressEvidence(runtime, {}, null);
expect(progress?.evidence).toEqual([
expect.objectContaining({
label: '视觉分析完成',
tone: 'work',
}),
]);
});
it('presents an unstructured image inspection tool failure from its production summary', () => {
const runId = 'game-chat-unstructured-image-inspection-failure';
const runtime = gameChatRuntimeState({
runId,
recentEvents: [
gameChatRuntimeEvent({
runId,
eventType: 'observation',
status: 'running',
phase: 'tool-observation',
summary: 'image.inspect:failed · 视觉模型调用失败',
detail: null,
updatedAt: 26,
}),
],
});
const progress = buildGameChatProgressEvidence(runtime, {}, null);
expect(progress?.evidence).toEqual([
expect.objectContaining({
label: '视觉分析未完成',
tone: 'fail',
}),
]);
});
it('uses structured image inspection verdicts for pass and fail presentation', () => {
for (const [passed, label, tone] of [
[true, '视觉检查通过', 'pass'],
[false, '视觉检查未通过', 'fail'],
] as const) {
const runId = `game-chat-structured-image-inspection-${passed}`;
const runtime = gameChatRuntimeState({
runId,
recentEvents: [
gameChatRuntimeEvent({
runId,
eventType: 'observation',
status: passed ? 'ok' : 'failed',
phase: 'tool-observation',
summary: `image.inspect:${passed ? 'ok' : 'failed'} · UI 原型视觉检查${passed ? '已通过' : '未通过'}`,
detail: JSON.stringify({ passed }),
updatedAt: passed ? 26 : 27,
}),
],
});
expect(
buildGameChatProgressEvidence(runtime, {}, null)?.evidence,
).toEqual([expect.objectContaining({ label, tone })]);
}
});
it('requires an explicit playtest pass before presenting preview validation as passed', () => {
const runId = 'game-chat-preview-without-playtest-pass';
const runtime = gameChatRuntimeState({
runId,
recentEvents: [
gameChatRuntimeEvent({
runId,
eventType: 'observation',
status: 'ok',
phase: 'tool-observation',
summary: 'preview.validate:ok · 浏览器验证已完成',
detail: JSON.stringify({
diagnostics: [],
diagnosticsCount: 0,
passed: true,
revision: 8,
}),
updatedAt: 26,
}),
],
});
const progress = buildGameChatProgressEvidence(runtime, {}, null);
expect(progress?.evidence).toEqual([
expect.objectContaining({
label: '试玩未通过',
tone: 'fail',
}),
]);
});
it('does not infer a preview pass from an ok summary when structured evidence is missing', () => {
const runId = 'game-chat-preview-without-structured-evidence';
const runtime = gameChatRuntimeState({
runId,
recentEvents: [
gameChatRuntimeEvent({
runId,
eventType: 'observation',
status: 'ok',
phase: 'tool-observation',
summary: 'preview.validate:ok · 浏览器验证已完成',
detail: null,
updatedAt: 27,
}),
],
});
const progress = buildGameChatProgressEvidence(runtime, {}, null);
expect(progress?.evidence).toEqual([
expect.objectContaining({
label: '试玩未通过',
tone: 'fail',
}),
]);
});
it('derives a playable revision only from a passed preview validation with a positive integer revision', () => {
const runId = 'game-chat-playable-revision-run';
const invalidPlaytest = gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 1,
updatedAt: 3000,
});
const invalidEvidence = [
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: invalidPlaytest.sessionId,
runId: invalidPlaytest.runId,
eventType: 'observation',
summary: 'preview.validate:failed',
detail: JSON.stringify({
passed: true,
playtestPassed: true,
revision: 8,
}),
updatedAt: 8000,
}),
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: invalidPlaytest.sessionId,
runId: invalidPlaytest.runId,
eventType: 'observation',
summary: 'preview.validate:ok · 验证未通过',
detail: JSON.stringify({
passed: false,
playtestPassed: false,
revision: 7,
}),
updatedAt: 7000,
}),
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: invalidPlaytest.sessionId,
runId: invalidPlaytest.runId,
eventType: 'observation',
summary: 'preview.validate:ok · 缺少 revision',
detail: JSON.stringify({ passed: true, playtestPassed: true }),
updatedAt: 6000,
}),
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: invalidPlaytest.sessionId,
runId: invalidPlaytest.runId,
eventType: 'observation',
summary: 'preview.validate:ok · revision 非正整数',
detail: JSON.stringify({
passed: true,
playtestPassed: true,
revision: 0,
}),
updatedAt: 5000,
}),
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: invalidPlaytest.sessionId,
runId: invalidPlaytest.runId,
eventType: 'observation',
summary: 'preview.validate:ok · revision 不是整数',
detail: JSON.stringify({
passed: true,
playtestPassed: true,
revision: 1.5,
}),
updatedAt: 4000,
}),
];
invalidPlaytest.recentEvents = invalidEvidence;
const runtimeWithOnlyInvalidEvidence = gameChatRuntimeState({
runId,
});
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest': invalidPlaytest,
}),
).toBeNull();
const previewPlaytest = gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 9,
updatedAt: 9000,
});
previewPlaytest.recentEvents?.push(
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: previewPlaytest.sessionId,
runId: previewPlaytest.runId,
eventType: 'observation',
status: 'completed',
phase: 'tool-observation',
summary: 'preview.validate:ok · revision 10 首次验证',
detail: JSON.stringify({
diagnosticsCount: 0,
passed: true,
playtestPassed: true,
revision: 10,
}),
updatedAt: 8500,
}),
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: previewPlaytest.sessionId,
runId: previewPlaytest.runId,
eventType: 'observation',
status: 'completed',
phase: 'tool-observation',
summary: 'preview.validate:ok · revision 10 最新验证',
detail: JSON.stringify({
diagnosticsCount: 0,
passed: true,
playtestPassed: true,
revision: 10,
}),
updatedAt: 8800,
}),
);
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest': previewPlaytest,
}),
).toEqual({ runId, revision: 10, validatedAt: 8800 });
const staleParentPlaytest = gameChatPreviewPlaytestRuntime({
parentRunId: 'different-parent-run',
revision: 11,
updatedAt: 11000,
});
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest': staleParentPlaytest,
}),
).toBeNull();
const wrongSourcePlaytest = {
...gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 12,
updatedAt: 12000,
}),
source: 'agent-delegate' as const,
};
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest': wrongSourcePlaytest,
}),
).toBeNull();
const passedRevision = gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 20,
updatedAt: 20000,
});
const higherFailedRevision = gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 21,
updatedAt: 21000,
});
higherFailedRevision.recentEvents = [
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: higherFailedRevision.sessionId,
runId: higherFailedRevision.runId,
eventType: 'observation',
status: 'failed',
phase: 'tool-observation',
summary: 'preview.validate:failed · revision 21',
detail: JSON.stringify({
passed: false,
playtestPassed: false,
revision: 21,
}),
updatedAt: 21000,
}),
];
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest-passed': passedRevision,
'preview-playtest-higher-failed': higherFailedRevision,
}),
).toBeNull();
const sameRevisionLaterFailed = gameChatPreviewPlaytestRuntime({
parentRunId: runId,
revision: 22,
updatedAt: 22000,
});
sameRevisionLaterFailed.recentEvents?.push(
gameChatRuntimeEvent({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: sameRevisionLaterFailed.sessionId,
runId: sameRevisionLaterFailed.runId,
eventType: 'observation',
status: 'failed',
phase: 'tool-observation',
summary: 'preview.validate:failed · revision 22 后续回归',
detail: JSON.stringify({
passed: false,
playtestPassed: false,
revision: 22,
}),
updatedAt: 22100,
}),
);
expect(
latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, {
'preview-playtest': sameRevisionLaterFailed,
}),
).toBeNull();
});
it('shows four latest game-chat events by default and expands to the capped latest twenty', () => {
const runtime = gameChatRuntimeState({
runId: 'game-chat-events-run',
recentEvents: Array.from({ length: 23 }, (_, index) =>
gameChatRuntimeEvent({
runId: 'game-chat-events-run',
summary: `状态事件 ${index + 1}`,
updatedAt: index + 1,
}),
),
});
expect(runtime.recentEvents).toHaveLength(23);
renderGameChatStatus({ runtime });
const statusCard = screen.getByLabelText('最新状态');
expect(statusCard.querySelectorAll(':scope > div > small')).toHaveLength(4);
expect(statusCard.textContent).toContain('状态事件 23');
expect(statusCard.textContent).not.toContain('状态事件 19');
const expand = within(statusCard).getByRole('button', {
name: '展开 20 条',
});
expect(expand.getAttribute('aria-expanded')).toBe('false');
fireEvent.click(expand);
expect(statusCard.querySelectorAll(':scope > div > small')).toHaveLength(
20,
);
const expandedEventTexts = Array.from(
statusCard.querySelectorAll(':scope > div > small'),
(element) => element.textContent,
);
expect(
expandedEventTexts.some((text) => text?.includes('状态事件 4')),
).toBe(true);
expect(
expandedEventTexts.some((text) => text?.includes('状态事件 3')),
).toBe(false);
expect(
within(statusCard)
.getByRole('button', { name: '收起' })
.getAttribute('aria-expanded'),
).toBe('true');
});
it('updates one runtime-owned game-chat Supervisor progress broadcast in place without conversation writes', async () => {
const projectPath = '/tmp/game-chat-progress-broadcast';
const supervisorRunId = 'game-chat-progress-run';
const previewFailure = gameChatRuntimeEvent({
runId: supervisorRunId,
eventType: 'observation',
status: 'failed',
phase: 'tool-observation',
summary: 'preview.validate:failed',
detail: JSON.stringify({
diagnostics: [
{ message: '角色仍会穿过右侧墙体' },
'失败后重开按钮没有响应',
],
diagnosticsCount: 2,
passed: false,
playtestPassed: false,
revision: 7,
}),
updatedAt: 7100,
});
const initialSupervisorOverrides = {
runId: supervisorRunId,
runProfile: 'autonomous-game-build' as const,
status: 'running',
phase: 'execution',
loopIteration: 4,
currentTask: '完成首版可运行原型',
currentAction: '核对首版试玩诊断',
planRevision: 1,
planSteps: [
{ index: 0, title: '完成基础玩法', status: 'completed' },
{ index: 1, title: '核对首版试玩诊断', status: 'in_progress' },
{ index: 2, title: '修复并复测', status: 'pending' },
],
activePlanStepIndex: 1,
recentEvents: [previewFailure],
updatedAt: 7200,
};
let professionalRuntimes: Array<Record<string, unknown>> = [];
const harness = createProjectSupervisorRuntimeHarness({
projectPath,
initialRuntime: initialSupervisorOverrides,
runtimeMapLoader: async () => professionalRuntimes,
});
const initialSupervisor = harness.runtimeState(
initialSupervisorOverrides,
) as AgentRuntimeState;
const codePrototypeRuntime = harness.runtimeState({
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'code-prototype-progress-session',
runId: 'code-prototype-progress-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'code-prototype-progress-delegation',
status: 'running',
phase: 'execution',
loopIteration: 2,
currentTask: '修复首版程序问题',
currentAction: '修复角色碰撞与重开逻辑',
planSteps: [
{
index: 0,
title: '修复角色碰撞与重开逻辑',
status: 'in_progress',
},
],
activePlanStepIndex: 0,
updatedAt: 7300,
}) as AgentRuntimeState;
professionalRuntimes = [codePrototypeRuntime];
harness.setRuntimeReader(async () => ({
...harness.runtimeResult(initialSupervisor),
recentEvents: initialSupervisor.recentEvents,
}));
const manifest = createGameCreationAppManifest(
'game-chat-progress-broadcast',
'game-chat-progress-broadcast',
);
manifest.tasks = manifest.tasks.map((task, index) => {
if (index < 3) {
return { ...task, status: 'completed' as const };
}
if (task.id === 'code-prototype') {
return { ...task, status: 'running' as const };
}
return task;
});
let runtimeUpdateHandler:
| ((event: {
payload: {
projectPath: string;
agentId: string;
runId: string;
status: string;
phase: string;
runtime: Record<string, unknown>;
};
}) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
handler: (event: {
payload: {
projectPath: string;
agentId: string;
runId: string;
status: string;
phase: string;
runtime: Record<string, unknown>;
};
}) => void,
) => {
if (eventName === 'game-creator-agent-runtime-update') {
runtimeUpdateHandler = handler;
}
return harness.listen(eventName, handler);
},
);
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: 'game-chat-progress-broadcast',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'init_local_game_project') {
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const progress = await screen.findByLabelText('Supervisor 进度播报');
expect(screen.getAllByLabelText('Supervisor 进度播报')).toHaveLength(1);
expect(progress.getAttribute('data-runtime-owned')).toBe('true');
expect(progress.getAttribute('data-run-id')).toBe(supervisorRunId);
expect(
within(progress).getByText('Supervisor 进度播报 · 第 4 轮'),
).not.toBeNull();
expect(
within(progress).getByText(
`任务图 3/${manifest.tasks.length} · 进行中 1 · 计划 1/3`,
),
).not.toBeNull();
expect(within(progress).getByText('核对首版试玩诊断')).not.toBeNull();
expect(within(progress).getByText('活跃专业 Agent')).not.toBeNull();
expect(
within(progress).getByText(
'程序原型 Agent · 第 2 轮 · 修复角色碰撞与重开逻辑',
),
).not.toBeNull();
expect(within(progress).getByText('试玩未通过')).not.toBeNull();
expect(
within(progress).getByText(
'revision 7 · 诊断 2 项 · 角色仍会穿过右侧墙体 · 失败后重开按钮没有响应',
),
).not.toBeNull();
expect(
invoke.mock.calls.filter(
([command]) => command === 'append_local_conversation_message',
),
).toHaveLength(0);
const delegateDecision = gameChatRuntimeEvent({
runId: supervisorRunId,
eventType: 'action',
phase: 'tool-action',
summary: '调用工具 agent.delegate',
detail: 'code-prototype 根据试玩诊断返工碰撞与重开逻辑',
updatedAt: 7400,
});
const updatedSupervisor = harness.runtimeState({
...initialSupervisorOverrides,
loopIteration: 5,
planRevision: 2,
planSteps: [
{ index: 0, title: '完成基础玩法', status: 'completed' },
{ index: 1, title: '核对首版试玩诊断', status: 'completed' },
{ index: 2, title: '安排程序 Agent 返工', status: 'in_progress' },
],
activePlanStepIndex: 2,
recentEvents: [previewFailure, delegateDecision],
updatedAt: 7500,
}) as AgentRuntimeState;
const updateHandler = runtimeUpdateHandler;
if (!updateHandler) {
throw new Error('missing game creator runtime update listener');
}
act(() => {
updateHandler({
payload: {
projectPath,
agentId: 'project-supervisor',
runId: supervisorRunId,
status: 'running',
phase: 'execution',
runtime: {
...harness.runtimeResult(updatedSupervisor),
recentEvents: updatedSupervisor.recentEvents,
},
},
});
});
await waitFor(() => {
expect(
within(progress).getByText('Supervisor 进度播报 · 第 5 轮'),
).not.toBeNull();
expect(
within(progress).getByText(
`任务图 3/${manifest.tasks.length} · 进行中 1 · 计划 2/3`,
),
).not.toBeNull();
expect(within(progress).getByText('安排程序 Agent 返工')).not.toBeNull();
expect(within(progress).getByText('返工决定')).not.toBeNull();
expect(
within(progress).getByText(
'code-prototype 根据试玩诊断返工碰撞与重开逻辑',
),
).not.toBeNull();
});
expect(screen.getByLabelText('Supervisor 进度播报')).toBe(progress);
expect(screen.getAllByLabelText('Supervisor 进度播报')).toHaveLength(1);
expect(
invoke.mock.calls.filter(
([command]) => command === 'append_local_conversation_message',
),
).toHaveLength(0);
});
it('renders registered image outcomes in one runtime-owned game-chat Supervisor card', async () => {
const projectPath = '/tmp/game-chat-result-images';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const manifest = createGameCreationAppManifest(
'game-chat-result-images',
'game-chat-result-images',
);
manifest.assets.push(
{
id: 'art-spritesheet',
kind: 'art-spritesheet',
mediaType: 'image/png',
localPath: 'assets/art-spritesheet.png',
source: { kind: 'canvas', taskId: 'art-asset-plan' },
},
{
id: 'ui-prototype',
kind: 'ui-prototype',
mediaType: 'image/webp',
localPath: 'assets/ui-prototype.webp',
source: { kind: 'canvas', taskId: 'design-foundation' },
},
{
id: 'audio-manifest',
kind: 'audio',
mediaType: 'audio/mpeg',
localPath: 'assets/theme.mp3',
source: { kind: 'generated' },
},
{
id: 'invalid-image-path',
kind: 'reference',
mediaType: 'image/png',
localPath: 'assets/../outside.png',
source: { kind: 'generated' },
},
);
expect(collectGameChatResultImages(manifest)).toEqual([
{
key: 'art-spritesheet:assets/art-spritesheet.png',
label: '美术素材图集',
mediaType: 'image/png',
path: 'assets/art-spritesheet.png',
},
{
key: 'ui-prototype:assets/ui-prototype.webp',
label: '界面原型',
mediaType: 'image/webp',
path: 'assets/ui-prototype.webp',
},
]);
const pngDataUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
const webpDataUrl = 'data:image/webp;base64,UklGRgAAAABXRUJQVlA4';
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: 'game-chat-result-images',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'init_local_game_project') {
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'read_local_project_image_preview') {
const relativePath = String(args?.relativePath ?? '');
const mediaType = relativePath.endsWith('.webp')
? 'image/webp'
: 'image/png';
return {
path: relativePath,
mediaType,
byteLen: 12,
dataUrl: mediaType === 'image/webp' ? webpDataUrl : pngDataUrl,
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const imageCard = await screen.findByLabelText('Supervisor 成果图片');
expect(imageCard.getAttribute('data-runtime-owned')).toBe('true');
expect(screen.getAllByLabelText('Supervisor 成果图片')).toHaveLength(1);
expect(within(imageCard).getByText('2/2')).not.toBeNull();
expect(
within(imageCard)
.getByRole('img', {
name: '美术素材图集 · assets/art-spritesheet.png',
})
.getAttribute('src'),
).toBe(pngDataUrl);
expect(
within(imageCard)
.getByRole('img', {
name: '界面原型 · assets/ui-prototype.webp',
})
.getAttribute('src'),
).toBe(webpDataUrl);
expect(
invoke.mock.calls.filter(
([command]) => command === 'read_local_project_image_preview',
),
).toEqual([
[
'read_local_project_image_preview',
{
projectPath,
relativePath: 'assets/art-spritesheet.png',
},
],
[
'read_local_project_image_preview',
{
projectPath,
relativePath: 'assets/ui-prototype.webp',
},
],
]);
fireEvent.click(
within(imageCard).getByRole('button', {
name: '查看 美术素材图集',
}),
);
const viewer = screen.getByRole('dialog', {
name: '美术素材图集 图片查看器',
});
const scale = within(viewer).getByLabelText('图片缩放比例');
expect(scale.textContent).toBe('100%');
fireEvent.click(within(viewer).getByRole('button', { name: '放大图片' }));
expect(scale.textContent).toBe('125%');
const stage = within(viewer).getByLabelText('可缩放拖拽图片区域');
fireEvent.wheel(stage, { deltaY: -1 });
expect(scale.textContent).toBe('150%');
const dispatchPointer = (
type: 'pointerdown' | 'pointermove' | 'pointerup',
clientX: number,
clientY: number,
) => {
const event = new Event(type, { bubbles: true });
Object.defineProperties(event, {
button: { value: 0 },
clientX: { value: clientX },
clientY: { value: clientY },
pointerId: { value: 1 },
});
fireEvent(stage, event);
};
dispatchPointer('pointerdown', 100, 80);
dispatchPointer('pointermove', 136, 104);
dispatchPointer('pointerup', 136, 104);
const viewerImage = within(viewer).getByRole('img', {
name: '美术素材图集 · assets/art-spritesheet.png',
});
expect(viewerImage.getAttribute('style')).toContain(
'translate(36px, 24px) scale(1.5)',
);
fireEvent.click(within(viewer).getByRole('button', { name: '复位图片' }));
expect(scale.textContent).toBe('100%');
expect(viewerImage.getAttribute('style')).toContain(
'translate(0px, 0px) scale(1)',
);
fireEvent.keyDown(window, { key: 'Escape' });
expect(
screen.queryByRole('dialog', {
name: '美术素材图集 图片查看器',
}),
).toBeNull();
expect(
invoke.mock.calls.filter(
([command]) => command === 'append_local_conversation_message',
),
).toHaveLength(0);
});
it('persists one terminal game-chat stage record and keeps it in the next round', async () => {
const projectPath = '/tmp/game-chat-stage-record';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const manifest = createGameCreationAppManifest(
'game-chat-stage-record',
'game-chat-stage-record',
);
manifest.tasks = manifest.tasks.map((task, index) =>
index < 3 ? { ...task, status: 'completed' as const } : task,
);
manifest.assets.push({
id: 'stage-art',
kind: 'art-spritesheet',
mediaType: 'image/png',
localPath: 'assets/art-spritesheet.png',
source: { kind: 'canvas', taskId: 'art-asset-plan' },
});
let runtimeUpdateHandler:
| ((event: {
payload: {
projectPath: string;
agentId: string;
runId: string;
status: string;
phase: string;
runtime: Record<string, unknown>;
};
}) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
handler: (event: {
payload: {
projectPath: string;
agentId: string;
runId: string;
status: string;
phase: string;
runtime: Record<string, unknown>;
};
}) => void,
) => {
if (eventName === 'game-creator-agent-runtime-update') {
runtimeUpdateHandler = handler;
}
return harness.listen(eventName, handler);
},
);
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: 'game-chat-stage-record',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'init_local_game_project') {
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'read_local_project_image_preview') {
return {
path: String(args?.relativePath ?? ''),
mediaType: 'image/png',
byteLen: 12,
dataUrl:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, { target: { value: '完成这一轮并保留证据' } });
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(
invoke.mock.calls.some(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toBe(true);
});
const startCall = invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const firstRunId = String(startCall?.[1]?.runId ?? '');
expect(firstRunId).not.toBe('');
const previewPassed = gameChatRuntimeEvent({
runId: firstRunId,
eventType: 'observation',
summary: 'preview.validate:ok · 浏览器验证已通过',
detail: JSON.stringify({
diagnosticsCount: 0,
passed: true,
playtestPassed: true,
revision: 9,
}),
updatedAt: 9100,
});
const rework = gameChatRuntimeEvent({
runId: firstRunId,
eventType: 'action',
summary: '调用工具 agent.delegate',
detail: '根据上一版试玩诊断安排程序 Agent 完成返工',
updatedAt: 9000,
});
const terminalRuntime = harness.runtimeState({
runId: firstRunId,
runProfile: 'autonomous-game-build',
status: 'idle',
phase: 'completed',
loopIteration: 6,
currentAction: '等待下一轮输入',
planSteps: [
{ index: 0, title: '完成返工', status: 'completed' },
{ index: 1, title: '通过试玩', status: 'completed' },
],
activePlanStepIndex: null,
recentEvents: [rework, previewPassed],
updatedAt: 9200,
});
harness.appendSupervisorMessage({
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '第一轮已经完成。',
agentId: 'project-supervisor',
messageId: 'stage-record-final-assistant',
updatedAt: 9200,
});
const updateHandler = runtimeUpdateHandler;
if (!updateHandler) {
throw new Error('missing game creator runtime update listener');
}
act(() => {
updateHandler({
payload: {
projectPath,
agentId: 'project-supervisor',
runId: firstRunId,
status: 'idle',
phase: 'completed',
runtime: {
...harness.runtimeResult(terminalRuntime),
recentEvents: [rework, previewPassed],
},
},
});
});
const stageRecord = await screen.findByText(
/【Supervisor 阶段记录】[\s\S]*第 6 轮 · 本轮已完成/,
);
expect(stageRecord.className).toContain('game-chat-stage-record');
expect(stageRecord.textContent).toContain('试玩通过:revision 9');
expect(stageRecord.textContent).toContain(
'返工决定:根据上一版试玩诊断安排程序 Agent 完成返工',
);
expect(stageRecord.textContent).toContain(
'成果图片:美术素材图集(assets/art-spritesheet.png)',
);
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command, args]) =>
command === 'append_local_conversation_message' &&
args?.agentId === null &&
String(
(args?.message as { content?: string } | undefined)?.content ??
'',
).startsWith('【Supervisor 阶段记录】'),
),
).toHaveLength(1);
});
fireEvent.change(composer, { target: { value: '开始下一轮' } });
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(2);
});
expect(screen.getAllByText(/【Supervisor 阶段记录】/)).toHaveLength(1);
expect(screen.getByText(/试玩通过:revision 9/)).not.toBeNull();
});
it('starts and displays the first playable game-chat preview exactly once', async () => {
const projectPath = '/tmp/game-chat-auto-preview';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const pendingManifest = createGameCreationAppManifest(
'game-chat-auto-preview',
'game-chat-auto-preview',
);
const completedManifest = createGameCreationAppManifest(
'game-chat-auto-preview',
'game-chat-auto-preview',
);
completedManifest.tasks = completedManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
let playableVersionReady = false;
let previewStarted = false;
let acceptedRunId = '';
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: 'game-chat-auto-preview',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return previewStarted
? {
status: 'running',
url: 'http://127.0.0.1:4311',
port: 4311,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return playableVersionReady ? completedManifest : pendingManifest;
}
if (command === 'start_local_game_preview') {
expect(args).toEqual({ projectPath, expectedRevision: 1 });
previewStarted = true;
return {
url: 'http://127.0.0.1:4311',
port: 4311,
root: `${projectPath}/game`,
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
acceptedRunId = String(args?.runId ?? '');
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, {
target: { value: '生成第一版可玩原型' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
{
projectPath,
sessionId: harness.sessionId,
task: '生成第一版可玩原型',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
},
);
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(0);
expect(screen.queryByLabelText('游戏运行')).toBeNull();
expect(within(surface).getByText('预览未启动')).not.toBeNull();
playableVersionReady = true;
harness.setProjectRevision(1);
const firstPlayableRevision = gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunId,
revision: 1,
updatedAt: 3900,
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunId,
runProfile: 'autonomous-game-build',
status: 'running',
phase: 'verification',
currentTask: '验证首个可玩版本',
currentGoal: '交付首个可玩版本',
currentAction: '可玩版本已经通过验证',
updatedAt: 4000,
}),
);
harness.emitAgentRuntime(firstPlayableRevision);
await Promise.resolve();
});
expect(
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
).not.toBeNull();
const previewFrame = screen.getByTitle(
'game-chat-auto-preview 游戏运行画面',
) as HTMLIFrameElement;
expect(previewFrame.src).toMatch(/^http:\/\/127\.0\.0\.1:4311\//);
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath,
event: 'permission.confirm',
commandId: 'preview.start',
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
it('refreshes a running game-chat iframe after a later run completes without restarting the preview', async () => {
const projectPath = '/tmp/game-chat-preview-revision';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const playableManifest = createGameCreationAppManifest(
'game-chat-preview-revision',
'game-chat-preview-revision',
);
playableManifest.tasks = playableManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
const acceptedRunIds: string[] = [];
let previewStarted = false;
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: 'game-chat-preview-revision',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return previewStarted
? {
status: 'running',
url: 'http://127.0.0.1:4314',
port: 4314,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return playableManifest;
}
if (command === 'start_local_game_preview') {
expect(args).toEqual({ projectPath, expectedRevision: 1 });
previewStarted = true;
return {
url: 'http://127.0.0.1:4314',
port: 4314,
root: `${projectPath}/game`,
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
acceptedRunIds.push(String(args?.runId ?? ''));
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, {
target: { value: '生成第一版可玩原型' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(acceptedRunIds).toHaveLength(1);
});
harness.setProjectRevision(1);
const firstPreviewPassed = gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunIds[0],
revision: 1,
updatedAt: 3900,
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunIds[0],
runProfile: 'autonomous-game-build',
status: 'running',
phase: 'verification',
currentTask: '验证第一版可玩原型',
currentGoal: '交付第一版可玩原型',
currentAction: '第一版已经通过浏览器验证',
updatedAt: 4000,
}),
);
harness.emitAgentRuntime(firstPreviewPassed);
await Promise.resolve();
});
const firstFrame = (await screen.findByTitle(
'game-chat-preview-revision 游戏运行画面',
{},
{ timeout: 3000 },
)) as HTMLIFrameElement;
const firstFrameUrl = firstFrame.src;
expect(acceptedRunIds).toHaveLength(1);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunIds[0],
runProfile: 'autonomous-game-build',
status: 'completed',
phase: 'completed',
currentTask: '第一版已经完成',
currentGoal: '第一版已经完成',
currentAction: '完成交付',
lastResponse: '第一版已经完成',
updatedAt: 5000,
}),
);
await Promise.resolve();
});
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
const previewStatusCallsBeforeRevision = invoke.mock.calls.filter(
([command]) => command === 'get_local_game_preview_status',
).length;
fireEvent.change(composer, {
target: { value: '更新第二版玩法和平衡性' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(acceptedRunIds).toHaveLength(2);
expect(
invoke.mock.calls.filter(
([command]) => command === 'get_local_game_preview_status',
).length,
).toBeGreaterThan(previewStatusCallsBeforeRevision);
});
expect(screen.getByTitle('game-chat-preview-revision 游戏运行画面')).toBe(
firstFrame,
);
expect(firstFrame.src).toBe(firstFrameUrl);
harness.setProjectRevision(2);
const secondPreviewPassed = gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunIds[1],
revision: 2,
updatedAt: 7900,
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunIds[1],
runProfile: 'autonomous-game-build',
status: 'completed',
phase: 'completed',
currentTask: '第二版已经完成',
currentGoal: '第二版已经完成',
currentAction: '完成第二版验证',
lastResponse: '第二版已经完成',
updatedAt: 8000,
}),
);
harness.emitAgentRuntime(secondPreviewPassed);
await Promise.resolve();
});
await waitFor(
() => {
const refreshedFrame = screen.getByTitle(
'game-chat-preview-revision 游戏运行画面',
) as HTMLIFrameElement;
expect(
refreshedFrame !== firstFrame || refreshedFrame.src !== firstFrameUrl,
).toBe(true);
},
{ timeout: 3000 },
);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(2);
}, 10000);
it('keeps a same-run steer authorization until a newer revision refreshes the running iframe', async () => {
const projectPath = '/tmp/game-chat-same-run-steer-preview';
const driver = await renderGameChatAutoPreviewDriver({
projectPath,
port: 4381,
startPreview: async (args) => {
expect(args).toEqual({ projectPath, expectedRevision: 1 });
return {
url: 'http://127.0.0.1:4381',
port: 4381,
root: `${projectPath}/game`,
};
},
});
await driver.submit('生成首个可玩版本');
await driver.emitValidation(1, 1000);
const firstFrame = (await screen.findByTitle(
'game-chat-same-run-steer-preview 游戏运行画面',
{},
{ timeout: 3000 },
)) as HTMLIFrameElement;
const firstFrameUrl = firstFrame.src;
await waitFor(() => {
expect(driver.readAuthorization()).toBeNull();
});
await driver.submit('保持同一 Run 并继续调整当前版本');
await waitFor(() => {
expect(driver.readAuthorization()).toMatchObject({
afterRevision: 1,
afterValidatedAt: 1000,
projectPath,
runId: driver.parentRunId,
authorizationId: expect.any(String),
});
});
const steerAuthorizationId = driver.readAuthorization()?.authorizationId;
const previewStatusReads = driver.invoke.mock.calls.filter(
([command]) => command === 'get_local_game_preview_status',
).length;
await waitFor(() => {
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'get_local_game_preview_status',
).length,
).toBeGreaterThan(previewStatusReads);
}, { timeout: 3000 });
expect(driver.readAuthorization()?.authorizationId).toBe(
steerAuthorizationId,
);
expect(
screen.getByTitle('game-chat-same-run-steer-preview 游戏运行画面'),
).toBe(firstFrame);
expect(firstFrame.src).toBe(firstFrameUrl);
await driver.emitValidation(2, 2000);
await waitFor(
() => {
const refreshedFrame = screen.getByTitle(
'game-chat-same-run-steer-preview 游戏运行画面',
) as HTMLIFrameElement;
expect(
refreshedFrame !== firstFrame || refreshedFrame.src !== firstFrameUrl,
).toBe(true);
expect(driver.readAuthorization()).toBeNull();
},
{ timeout: 3000 },
);
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'steer_game_creator_agent_runtime_task',
),
).toHaveLength(1);
}, 10000);
it('waits for new validation evidence after a revision-drift start failure before retrying', async () => {
const projectPath = '/tmp/game-chat-preview-revision-drift';
let setProjectRevisionOnDrift: ((revision: number) => void) | null = null;
const driver = await renderGameChatAutoPreviewDriver({
projectPath,
port: 4382,
startPreview: async (args) => {
const expectedRevision = Number(args.expectedRevision);
if (expectedRevision === 1) {
setProjectRevisionOnDrift?.(2);
throw new Error(
'本地游戏项目已在验证后发生变化:expectedRevision=1 · currentRevision=2',
);
}
expect(expectedRevision).toBe(2);
return {
url: 'http://127.0.0.1:4382',
port: 4382,
root: `${projectPath}/game`,
};
},
});
setProjectRevisionOnDrift = driver.harness.setProjectRevision;
await driver.submit('生成后验证 revision 漂移恢复');
await driver.emitValidation(1, 1000);
await waitFor(() => {
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
expect(driver.readAuthorization()).not.toBeNull();
});
await act(async () => {
driver.harness.emitAgentRuntime(
gameChatPreviewPlaytestRuntime({
parentRunId: driver.parentRunId,
revision: 1,
updatedAt: 1000,
}),
);
await Promise.resolve();
await Promise.resolve();
});
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
expect(driver.readAuthorization()).not.toBeNull();
await driver.emitValidation(2, 2000);
expect(
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
).not.toBeNull();
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(2);
expect(
driver.invoke.mock.calls
.filter(([command]) => command === 'start_local_game_preview')
.at(-1),
).toEqual([
'start_local_game_preview',
{ projectPath, expectedRevision: 2 },
]);
expect(driver.readAuthorization()).toBeNull();
}, 10000);
it('does not let a deferred policy result clear a replacement authorization', async () => {
await assertNewGameChatAuthorizationSupersedesDeferredAttempt('policy');
}, 10000);
it('atomically stops a stale deferred start without clearing its replacement authorization', async () => {
await assertNewGameChatAuthorizationSupersedesDeferredAttempt('start');
}, 10000);
it('allows a new authorization and later validation to retry the same revision after a terminal attempt', async () => {
const projectPath = '/tmp/game-chat-preview-same-revision-retry';
const driver = await renderGameChatAutoPreviewDriver({
projectPath,
port: 4383,
startPreview: async (args, callIndex) => {
expect(args).toEqual({ projectPath, expectedRevision: 1 });
if (callIndex === 1) {
throw new Error('模拟 preview.start 非瞬时失败');
}
return {
url: 'http://127.0.0.1:4383',
port: 4383,
root: `${projectPath}/game`,
};
},
});
await driver.submit('生成同 revision 首次验证');
await driver.emitValidation(1, 1000);
await waitFor(() => {
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
expect(driver.readAuthorization()).toBeNull();
});
await driver.submit('同一 Run 新授权后重新验证同一 revision');
await waitFor(() => {
expect(driver.readAuthorization()).toMatchObject({
afterRevision: 1,
afterValidatedAt: 1000,
authorizationId: expect.any(String),
});
});
await driver.emitValidation(1, 2000);
expect(
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
).not.toBeNull();
expect(
driver.invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(2);
expect(driver.readAuthorization()).toBeNull();
}, 10000);
it('retries the authorized game-chat preview after a transient project write lock', async () => {
const projectPath = '/tmp/game-chat-auto-preview-lock-retry';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const completedManifest = createGameCreationAppManifest(
'game-chat-auto-preview-lock-retry',
'game-chat-auto-preview-lock-retry',
);
completedManifest.tasks = completedManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
let previewStarted = false;
let startAttempts = 0;
let acceptedRunId = '';
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: 'game-chat-auto-preview-lock-retry',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return previewStarted
? {
status: 'running',
url: 'http://127.0.0.1:4313',
port: 4313,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return completedManifest;
}
if (command === 'start_local_game_preview') {
expect(args).toEqual({ projectPath, expectedRevision: 3 });
startAttempts += 1;
if (startAttempts === 1) {
throw new Error(
`项目正在被其他写操作占用:${projectPath}/.agent/project.lock`,
);
}
previewStarted = true;
return {
url: 'http://127.0.0.1:4313',
port: 4313,
root: `${projectPath}/game`,
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
acceptedRunId = String(args?.runId ?? '');
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, {
target: { value: '生成后自动预览并处理瞬时写锁' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(acceptedRunId).toMatch(/^project-supervisor-task-/);
});
harness.setProjectRevision(3);
const playableRevision = gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunId,
revision: 3,
updatedAt: 3900,
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunId,
runProfile: 'autonomous-game-build',
status: 'running',
phase: 'verification',
updatedAt: 4000,
}),
);
harness.emitAgentRuntime(playableRevision);
await Promise.resolve();
});
expect(
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
).not.toBeNull();
expect(startAttempts).toBe(2);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(2);
});
it('adopts the accepted game-chat run when start initially returns the previous idle state', async () => {
const projectPath = '/tmp/game-chat-accepted-run';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const completedManifest = createGameCreationAppManifest(
'game-chat-accepted-run',
'game-chat-accepted-run',
);
completedManifest.tasks = completedManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
const previousIdleRuntime = harness.runtimeState({
runId: 'game-chat-previous-idle',
status: 'idle',
phase: 'idle',
updatedAt: 1000,
});
let acceptedRunId = '';
let postStartRuntimeReads = 0;
let allowCompletion = false;
let previewStarted = false;
const acceptedRunEvent = gameChatRuntimeEvent({
runId: 'placeholder',
summary: '新受理 Run 正在执行',
updatedAt: 4000,
});
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: 'game-chat-accepted-run',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return previewStarted
? {
status: 'running',
url: 'http://127.0.0.1:4312',
port: 4312,
root: `${projectPath}/game`,
}
: {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return completedManifest;
}
if (command === 'start_local_game_preview') {
expect(args).toEqual({ projectPath, expectedRevision: 4 });
previewStarted = true;
return {
url: 'http://127.0.0.1:4312',
port: 4312,
root: `${projectPath}/game`,
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
acceptedRunId = String(args?.runId ?? '');
return {
...harness.runtimeResult(previousIdleRuntime),
acceptedRunId,
};
}
if (command === 'read_game_creator_agent_runtime' && acceptedRunId) {
postStartRuntimeReads += 1;
const state = harness.runtimeState({
runId: acceptedRunId,
runProfile: 'autonomous-game-build',
status: allowCompletion ? 'completed' : 'running',
phase: allowCompletion ? 'completed' : 'execution',
currentTask: '生成 accepted run 首版游戏',
currentGoal: '完成 accepted run 可玩原型',
currentAction: allowCompletion ? '完成交付' : '执行当前计划',
waitingOn: allowCompletion ? '' : '专业任务收束',
nextStep: allowCompletion ? '' : '继续执行',
lastResponse: allowCompletion ? 'accepted run 已完成' : null,
updatedAt: allowCompletion ? 5000 : 4000,
});
return {
...harness.runtimeResult(state),
recentEvents: [
{
...acceptedRunEvent,
runId: acceptedRunId,
status: allowCompletion ? 'completed' : 'running',
phase: allowCompletion ? 'completed' : 'execution',
summary: allowCompletion
? '新受理 Run 已完成'
: acceptedRunEvent.summary,
updatedAt: allowCompletion ? 5000 : 4000,
},
],
};
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, {
target: { value: '生成并接管真实受理 Run' },
});
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(acceptedRunId).toMatch(/^project-supervisor-task-/);
expect(postStartRuntimeReads).toBeGreaterThan(0);
expect(
within(screen.getByLabelText('最新状态')).getByText(
/新受理 Run 正在执行/,
),
).not.toBeNull();
});
expect(screen.queryByText('等待新的运行事件')).toBeNull();
harness.setProjectRevision(4);
await act(async () => {
harness.emitAgentRuntime(
gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunId,
revision: 4,
updatedAt: 4100,
}),
);
await Promise.resolve();
});
expect(await screen.findByLabelText('游戏运行')).not.toBeNull();
await waitFor(() => {
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
});
const startCallIndex = invoke.mock.calls.findIndex(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const acceptedReadCallIndex = invoke.mock.calls.findIndex(
([command], index) =>
index > startCallIndex && command === 'read_game_creator_agent_runtime',
);
const manifestCallIndex = invoke.mock.calls.findIndex(
([command], index) =>
index > startCallIndex && command === 'get_local_game_manifest',
);
expect(acceptedReadCallIndex).toBeGreaterThan(startCallIndex);
expect(manifestCallIndex).toBeGreaterThan(acceptedReadCallIndex);
allowCompletion = true;
await waitFor(
() => {
expect(
within(screen.getByLabelText('最新状态')).getByText('本轮已完成'),
).not.toBeNull();
expect(
within(screen.getByLabelText('最新状态')).getByText(
/新受理 Run 已完成/,
),
).not.toBeNull();
},
{ timeout: 2500 },
);
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(1);
});
it('honors an explicit preview.start deny after a playable revision is verified', async () => {
const projectPath = '/tmp/game-chat-preview-denied';
const harness = createProjectSupervisorRuntimeHarness({ projectPath });
const completedManifest = createGameCreationAppManifest(
'game-chat-preview-denied',
'game-chat-preview-denied',
);
completedManifest.tasks = completedManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
let acceptedRunId = '';
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: 'game-chat-preview-denied',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_preview_status') {
return {
status: 'stopped',
url: null,
port: null,
root: null,
};
}
if (command === 'get_local_game_manifest') {
return completedManifest;
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: ['preview.start'],
confirmCommands: [],
},
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
acceptedRunId = String(args?.runId ?? '');
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
render(
React.createElement(App, {
initialProjectPath: projectPath,
projectSupervisorOnly: true,
gameChatOnly: true,
}),
);
const surface = await screen.findByLabelText('游戏创作聊天');
const composer = within(surface).getByLabelText('项目总控对话内容');
await waitFor(() => {
expect((composer as HTMLTextAreaElement).disabled).toBe(false);
});
fireEvent.change(composer, { target: { value: '生成但不要自动启动' } });
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
await waitFor(() => {
expect(acceptedRunId).toMatch(/^project-supervisor-task-/);
});
harness.setProjectRevision(5);
const deniedPlayableRevision = gameChatPreviewPlaytestRuntime({
parentRunId: acceptedRunId,
revision: 5,
updatedAt: 3900,
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: acceptedRunId,
runProfile: 'autonomous-game-build',
status: 'running',
phase: 'verification',
updatedAt: 4000,
}),
);
harness.emitAgentRuntime(deniedPlayableRevision);
await Promise.resolve();
});
expect(
await within(surface).findByText('项目权限策略拒绝执行:preview.start'),
).not.toBeNull();
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_local_game_preview',
),
).toHaveLength(0);
expect(screen.queryByLabelText('游戏运行')).toBeNull();
});
it('keeps game-chat full-width before preview and switches to desktop 2:1 or mobile vertical layout', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toMatch(
/\.game-chat-shell\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s,
);
expect(styles).toMatch(
/\.game-chat-shell\.has-preview\s*\{[^}]*grid-template-columns:\s*minmax\(0, 2fr\) minmax\(320px, 1fr\)/s,
);
expect(styles).toMatch(
/@media \(max-width: 760px\)[\s\S]*?\.game-chat-shell\.has-preview\s*\{[^}]*grid-template-rows:\s*minmax\(220px, 42dvh\) minmax\(0, 1fr\)[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/,
);
});
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(),
);
});
}