补跨页同回合的历史分页回归用例
用 51 条历史复现用户报的现象:20 条工具条目整屏落进同一个折叠的执行过程 断言一次点击连拉两页拿到新用户气泡,并按 process-31 / process-11 两次锚点推进 断言连拉插入旧内容后视口仍停在原处,不被拉回列表底部
This commit is contained in:
@@ -8423,6 +8423,114 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps pulling earlier pages while the page only deepens the rendered turn', async () => {
|
||||
// 用户报的现象:一屏 20 条全是同一个回合的工具卡片,落在折叠的「执行过程」里,
|
||||
// 点一次「显示更早」看不到任何变化。连拉必须越过这一屏,直到出现新的用户气泡。
|
||||
const projectPath = '/tmp/launcher-direct-history-cross-page-game';
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'launcher-direct-history-cross-page-game',
|
||||
);
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
});
|
||||
// 51 条:用户气泡 + 50 条工具条目。首屏取尾 20 条(process-31..process-50),
|
||||
// 第一次翻页再取 20 条(process-11..process-30),都还在同一个回合里。
|
||||
supervisorHarness.setDirectThreadHistory([
|
||||
{
|
||||
itemType: 'message',
|
||||
itemId: 'direct-codex:turn-更早:user',
|
||||
role: 'user',
|
||||
text: '更早的用户提问',
|
||||
at: 1000,
|
||||
},
|
||||
...Array.from({ length: 50 }, (_, index) => ({
|
||||
itemType: 'function_call',
|
||||
itemId: `process-${index + 1}`,
|
||||
name: 'exec_command',
|
||||
arguments: `echo ${index + 1}`,
|
||||
at: 1100 + index,
|
||||
})),
|
||||
]);
|
||||
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-direct-history-cross-page-game',
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
};
|
||||
}
|
||||
if (command === 'get_local_game_manifest') {
|
||||
return manifest;
|
||||
}
|
||||
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: supervisorHarness.listen },
|
||||
};
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
pickProjectFromLauncher(projectPath);
|
||||
|
||||
const supervisorSurface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
|
||||
projectPath,
|
||||
limit: 20,
|
||||
}),
|
||||
);
|
||||
// 首屏只有这个回合的执行过程:用户气泡还在更早的页里。
|
||||
expect(within(supervisorSurface).queryByText('更早的用户提问')).toBeNull();
|
||||
|
||||
// 视口停在列表中部(既不在顶部触发自动翻页,也不在底部跟随最新)。
|
||||
const messageList = screen.getByLabelText('陶泥儿消息') as HTMLDivElement;
|
||||
let messageScrollHeight = 600;
|
||||
Object.defineProperty(messageList, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => messageScrollHeight,
|
||||
});
|
||||
Object.defineProperty(messageList, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => 100,
|
||||
});
|
||||
messageList.scrollTop = 100;
|
||||
fireEvent.scroll(messageList);
|
||||
// 插入后列表会变长:跟随最新的话视口会被拉到这个高度。
|
||||
messageScrollHeight = 2000;
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '显示更早的对话' }));
|
||||
|
||||
// 一次点击连拉两页:第一页仍然只有执行过程,第二页才带回用户气泡。
|
||||
expect(
|
||||
await within(supervisorSurface).findByText('更早的用户提问'),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
|
||||
projectPath,
|
||||
beforeItemId: 'process-31',
|
||||
limit: 20,
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
|
||||
projectPath,
|
||||
beforeItemId: 'process-11',
|
||||
limit: 20,
|
||||
});
|
||||
// 取到文件头后按钮收起。
|
||||
expect(screen.queryByRole('button', { name: /显示更早/ })).toBeNull();
|
||||
// 连拉插入的旧内容不得把视口弹到列表底部。
|
||||
expect(messageList.scrollTop).toBe(100);
|
||||
});
|
||||
|
||||
it('drains a notify that lands before the subscribe receipt so the running turn is not stranded', async () => {
|
||||
// 真实竞态:`subscribe` 在 Rust 侧已经注册好 subscriber 并开始通知,但前端还没拿到
|
||||
// subscriptionId。此时的通知不能白丢——拿到回执后必须补一次 consume,否则整个回合会卡在队列里。
|
||||
|
||||
Reference in New Issue
Block a user