测试骨架的历史切片按 limit 与锚点分页
- harness.ts 的 read_direct_project_history_slice 按生产口径从队尾取一屏、锚点条目不进窗口 - 按本屏最老一条算 firstItemId,收满一屏后再有条目才算 hasMore - project-development.suite.ts 补一条用例:26 条历史只显示尾部 20 条,点「显示更早的对话」按锚点取回前 6 条后按钮消失
This commit is contained in:
@@ -1023,10 +1023,33 @@ function createProjectSupervisorRuntimeHarness({
|
||||
return { events };
|
||||
}
|
||||
if (command === 'read_direct_project_history_slice') {
|
||||
// 生产口径:从文件尾反向取一屏可显示条目,锚点条目本身不进窗口;收满一屏之后
|
||||
// 再见一条才算 `hasMore`,`firstItemId` 是本屏最老一条的 itemId。
|
||||
const requestedLimit = Number(args?.limit ?? 20);
|
||||
const limit = Math.min(
|
||||
Math.max(Number.isFinite(requestedLimit) ? requestedLimit : 20, 1),
|
||||
200,
|
||||
);
|
||||
const beforeItemId =
|
||||
typeof args?.beforeItemId === 'string' && args.beforeItemId
|
||||
? args.beforeItemId
|
||||
: null;
|
||||
let end = directThreadHistoryItems.length;
|
||||
if (beforeItemId) {
|
||||
const anchorIndex = directThreadHistoryItems.findIndex(
|
||||
(item) => String(item.itemId ?? '') === beforeItemId,
|
||||
);
|
||||
end =
|
||||
anchorIndex >= 0 ? anchorIndex : directThreadHistoryItems.length;
|
||||
}
|
||||
const start = Math.max(0, end - limit);
|
||||
const window = directThreadHistoryItems.slice(start, end);
|
||||
const oldest = window[0];
|
||||
return {
|
||||
items: [...directThreadHistoryItems],
|
||||
hasMore: false,
|
||||
firstItemId: null,
|
||||
items: [...window],
|
||||
hasMore: start > 0,
|
||||
firstItemId:
|
||||
oldest && typeof oldest.itemId === 'string' ? oldest.itemId : null,
|
||||
};
|
||||
}
|
||||
if (command === 'start_game_creator_supervisor_runtime_task') {
|
||||
|
||||
@@ -8342,6 +8342,87 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('loads the earlier direct history page from the first-item anchor', async () => {
|
||||
// 分页锚点走的是首屏切片给出的 `firstItemId`:切片本身从队尾取,老的一屏靠锚点继续向前。
|
||||
const projectPath = '/tmp/launcher-direct-history-pagination-game';
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'launcher-direct-history-pagination-game',
|
||||
);
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
});
|
||||
// 26 条 > 首屏 20 条:首屏只显示最后 20 条,前面 6 条要靠"显示更早"翻页取回。
|
||||
supervisorHarness.setDirectThreadHistory(
|
||||
Array.from({ length: 26 }, (_, index) => {
|
||||
const label = `历史对话 ${String(index + 1).padStart(2, '0')}`;
|
||||
return {
|
||||
itemType: 'message',
|
||||
itemId: `direct-codex:turn-${label}:user`,
|
||||
role: 'user',
|
||||
text: label,
|
||||
at: 1000 + 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-pagination-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('陶泥儿项目对话');
|
||||
expect(
|
||||
await within(supervisorSurface).findByText('历史对话 26'),
|
||||
).not.toBeNull();
|
||||
// 首屏是尾部的 20 条:第 7 条起可见,更早的 6 条还没读进来。
|
||||
expect(within(supervisorSurface).getByText('历史对话 07')).not.toBeNull();
|
||||
expect(within(supervisorSurface).queryByText('历史对话 06')).toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
|
||||
projectPath,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '显示更早的对话' }));
|
||||
|
||||
// 翻页按首屏最老一条的锚点向前取;取到文件头后按钮消失。
|
||||
expect(
|
||||
await within(supervisorSurface).findByText('历史对话 01'),
|
||||
).not.toBeNull();
|
||||
expect(within(supervisorSurface).getByText('历史对话 06')).not.toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /显示更早/ })).toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_direct_project_history_slice', {
|
||||
projectPath,
|
||||
beforeItemId: 'direct-codex:turn-历史对话 07:user',
|
||||
limit: 20,
|
||||
});
|
||||
});
|
||||
|
||||
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