From e382d23892d1060cbf367225eeff81ba86481f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 17 Sep 2026 16:49:08 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E8=B7=A8=E9=A1=B5=E5=90=8C=E5=9B=9E?= =?UTF-8?q?=E5=90=88=E7=9A=84=E5=8E=86=E5=8F=B2=E5=88=86=E9=A1=B5=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E7=94=A8=E4=BE=8B=20=E7=94=A8=2051=20=E6=9D=A1?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=A4=8D=E7=8E=B0=E7=94=A8=E6=88=B7=E6=8A=A5?= =?UTF-8?q?=E7=9A=84=E7=8E=B0=E8=B1=A1=EF=BC=9A20=20=E6=9D=A1=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=9D=A1=E7=9B=AE=E6=95=B4=E5=B1=8F=E8=90=BD=E8=BF=9B?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E4=B8=AA=E6=8A=98=E5=8F=A0=E7=9A=84=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=BF=87=E7=A8=8B=20=E6=96=AD=E8=A8=80=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E7=82=B9=E5=87=BB=E8=BF=9E=E6=8B=89=E4=B8=A4=E9=A1=B5?= =?UTF-8?q?=E6=8B=BF=E5=88=B0=E6=96=B0=E7=94=A8=E6=88=B7=E6=B0=94=E6=B3=A1?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=8C=89=20process-31=20/=20process-11=20?= =?UTF-8?q?=E4=B8=A4=E6=AC=A1=E9=94=9A=E7=82=B9=E6=8E=A8=E8=BF=9B=20?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E8=BF=9E=E6=8B=89=E6=8F=92=E5=85=A5=E6=97=A7?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=90=8E=E8=A7=86=E5=8F=A3=E4=BB=8D=E5=81=9C?= =?UTF-8?q?=E5=9C=A8=E5=8E=9F=E5=A4=84=EF=BC=8C=E4=B8=8D=E8=A2=AB=E6=8B=89?= =?UTF-8?q?=E5=9B=9E=E5=88=97=E8=A1=A8=E5=BA=95=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appSurface/project-development.suite.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 6aa05e4bc..638d0b31f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -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) => { + 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,否则整个回合会卡在队列里。