补空对话首轮的工具调用块用例
- 新增用例:历史为空、消息列表里只有没有 messageId 的默认问候时,当前回合的工具块在回合进行中就直接渲染在消息列表末尾 - 同一用例断言回合结束后块只有一份、并回到该回合 assistant 消息之前(末尾兜底不留重复块) - 断言块内每行为按钮、行数 = 工具数、data-duration-ms 与行耗时文案
This commit is contained in:
@@ -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`),所以这里直接挂面板本体,
|
||||
|
||||
Reference in New Issue
Block a user