补空对话首轮的工具调用块用例

- 新增用例:历史为空、消息列表里只有没有 messageId 的默认问候时,当前回合的工具块在回合进行中就直接渲染在消息列表末尾
- 同一用例断言回合结束后块只有一份、并回到该回合 assistant 消息之前(末尾兜底不留重复块)
- 断言块内每行为按钮、行数 = 工具数、data-duration-ms 与行耗时文案
This commit is contained in:
2026-09-15 17:58:37 +08:00
parent 89e842d077
commit 1b40822421
@@ -8554,6 +8554,207 @@ export function registerProjectSupervisorSurfaceTests() {
).not.toBeNull();
});
it('renders the running turn tool-call group in a fresh chat that has no anchored assistant message', async () => {
// 空对话首轮:历史为空,消息列表里只有 App 落的默认问候(`想做什么游戏?`,没有 messageId)。
// 这种回合没有任何「可锚」的 assistant 消息,块必须兜底落在消息列表末尾,
// 而不是等到回合结束、assistant 消息带上 messageId 之后才出现。
const projectPath = '/tmp/launcher-tool-call-fresh-turn-game';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'launcher-tool-call-fresh-turn-game',
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
});
let directTurnUpdateHandler:
| ((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-direct-turn-update') {
directTurnUpdateHandler = handler;
return () => {
if (directTurnUpdateHandler === handler) {
directTurnUpdateHandler = null;
}
};
}
return supervisorHarness.listen(eventName, handler);
},
);
const directReply = createDeferred<string>();
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'get_design_agent_runtime_mode') return null;
if (command === 'inspect_local_project_directory') {
return {
projectPath,
exists: true,
isDirectory: true,
isGameCreatorProject: true,
projectName: 'launcher-tool-call-fresh-turn-game',
recentRunStatus: null,
recentRunStopReason: null,
};
}
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'chat_with_game_creator_direct_codex') {
return directReply.promise;
}
if (command === 'read_direct_tool_calls') {
return [];
}
if (command === 'read_direct_project_conversation') {
// 真正的空对话:没有任何历史消息,App 会落一条没有 messageId 的默认问候。
return {
path: `${projectPath}/.agent/conversations/project.jsonl`,
agentId: null,
sessionId: null,
messages: [],
};
}
if (command === 'get_local_game_preview_status') {
return { status: 'stopped', url: null, port: null, root: null };
}
return supervisorHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen },
};
renderLauncherProjectsAt('/?launcher');
pickProjectFromLauncher(projectPath);
const supervisorSurface = await screen.findByLabelText('陶泥儿项目对话');
const messageList = supervisorSurface.querySelector(
'.project-supervisor-message-list',
);
expect(messageList).not.toBeNull();
expect(
within(supervisorSurface).getByText('想做什么游戏?'),
).not.toBeNull();
expect(
within(supervisorSurface).queryAllByTestId('agent-tool-call-group'),
).toHaveLength(0);
await setComposerText(
screen.getByLabelText('陶泥儿对话内容'),
'做一个跑酷游戏',
);
fireEvent.submit(
screen
.getByLabelText('陶泥儿对话内容')
.closest('form') as HTMLFormElement,
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'chat_with_game_creator_direct_codex',
expect.objectContaining({ projectPath }),
);
});
const clientTurnId = String(
(
invoke.mock.calls.find(
([command]) => command === 'chat_with_game_creator_direct_codex',
)?.[1] as Record<string, unknown> | undefined
)?.clientTurnId ?? '',
);
expect(clientTurnId).not.toBe('');
await act(async () => {
directTurnUpdateHandler?.({
payload: {
projectPath,
turnId: clientTurnId,
sequence: 1,
status: 'running',
activity: 'command-exec',
updatedAt: 1400,
toolCalls: [
{
schemaVersion: 'agc-tool-call.v1',
id: 'fresh-turn-command',
kind: 'command',
title: '执行命令',
summary: 'npm run build',
status: 'running',
detail: { command: 'npm run build' },
startedAt: 1000,
updatedAt: 1400,
},
],
},
});
});
// 回合进行中:块已经可见,且落在消息列表末尾(默认问候之后),不依赖任何带 messageId 的 assistant 消息。
const runningGroups = within(supervisorSurface).getAllByTestId(
'agent-tool-call-group',
);
expect(runningGroups).toHaveLength(1);
const runningGroup = runningGroups[0] as HTMLElement;
expect(runningGroup.getAttribute('data-status')).toBe('running');
expect(runningGroup.getAttribute('data-duration-ms')).toBe('400');
const runningHead = within(runningGroup).getByTestId(
'agent-tool-call-group-head',
);
expect(runningHead.getAttribute('aria-expanded')).toBe('false');
expect(runningHead.textContent).toContain('已执行 1 个命令');
const runningChildren = Array.from((messageList as HTMLElement).children);
const greetingIndex = runningChildren.findIndex((node) =>
node.textContent?.includes('想做什么游戏?'),
);
expect(greetingIndex).toBeGreaterThanOrEqual(0);
expect(
runningChildren.findIndex((node) => node === runningGroup),
).toBeGreaterThan(greetingIndex);
fireEvent.click(runningHead);
const runningRows = within(runningGroup).getAllByTestId(
'agent-tool-call-row',
);
expect(runningRows).toHaveLength(1);
expect(runningRows[0]?.getAttribute('data-kind')).toBe('command');
expect(runningRows[0]?.getAttribute('data-duration-ms')).toBe('400');
expect(
within(runningRows[0] as HTMLElement).getByText('0.4s'),
).not.toBeNull();
// 回合结束:assistant 消息落盘后块回到它之前,且**只有一份**(末尾兜底不留下重复块)。
await act(async () => {
directReply.resolve('DIRECT_REPLY:空对话首轮');
});
await waitFor(() => {
expect(
within(supervisorSurface).getByText('DIRECT_REPLY:空对话首轮'),
).not.toBeNull();
});
await waitFor(() => {
expect(
within(supervisorSurface).getAllByTestId('agent-tool-call-group'),
).toHaveLength(1);
});
const settledGroup = within(supervisorSurface).getAllByTestId(
'agent-tool-call-group',
)[0] as HTMLElement;
const settledChildren = Array.from((messageList as HTMLElement).children);
const settledAssistantIndex = settledChildren.findIndex(
(node) =>
node.classList.contains('message--assistant') &&
node.textContent?.includes('DIRECT_REPLY:空对话首轮'),
);
expect(settledAssistantIndex).toBeGreaterThanOrEqual(0);
expect(settledChildren.findIndex((node) => node === settledGroup)).toBe(
settledAssistantIndex - 1,
);
});
it('renders the Codex empty state and opens the panel settings overlay in a fresh direct chat', async () => {
// 空态只在 direct-codex 面板、且没有任何消息/流式回复/待确认时出现。真实项目打开时
// App 总会先放一条默认问候(`createDefaultChatMessages`),所以这里直接挂面板本体,