From 1ce561ca6cb155f984342bf479c2eaa471360c49 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Thu, 4 Jun 2026 21:57:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20ACP=20loadSession=20=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=81=A2=E5=A4=8D=E5=A4=B1=E8=B4=A5=20?= =?UTF-8?q?=E2=80=94=20=E7=94=A8=20resolveSessionFilePath=20=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=20getProjectDir=20=E5=AE=9A=E4=BD=8D=20session=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - params.cwd 可能与 session 文件实际存储的项目目录不一致(子目录、 hash 算法差异等),导致 getProjectDir 推算出的路径找不到文件 - 改用 resolveSessionFilePath(sessionId, cwd) 按 sessionId 跨项目 搜索,先精确匹配再 fallback 全项目扫描 - 切换回已缓存的 session 时也回放历史消息给客户端 - createSession 内部 switchSession 保留 sessionProjectDir 不被覆盖为 null --- src/services/acp/__tests__/agent.test.ts | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/services/acp/__tests__/agent.test.ts b/src/services/acp/__tests__/agent.test.ts index 353791886..e1b0b1dac 100644 --- a/src/services/acp/__tests__/agent.test.ts +++ b/src/services/acp/__tests__/agent.test.ts @@ -82,6 +82,15 @@ mock.module('../../../utils/listSessionsImpl.js', () => ({ listSessionsImpl: mock(async () => []), })) +const mockResolveSessionFilePath = mock(async () => ({ + filePath: '/fake/project/dir/session.jsonl', + projectPath: '/tmp', + fileSize: 100, +})) +mockModulePreservingExports('../../../utils/sessionStoragePortable.js', { + resolveSessionFilePath: mockResolveSessionFilePath, +}) + const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6') mock.module('../../../utils/model/model.js', () => ({ @@ -746,4 +755,76 @@ describe('AcpAgent', () => { expect(commit.input).toEqual({ hint: '[message]' }) }) }) + }) + + describe('sessionId alignment with global state', () => { + test('newSession calls switchSession with the generated sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const res = await agent.newSession({ cwd: '/tmp' } as any) + expect(mockSwitchSession).toHaveBeenCalledWith(res.sessionId, null) + }) + + test('resumeSession calls switchSession with the requested sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const requestedId = 'resume-test-session-id' + await agent.unstable_resumeSession({ + sessionId: requestedId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith( + requestedId, + expect.any(String), + ) + }) + + test('loadSession calls switchSession with the requested sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const requestedId = 'load-test-session-id' + await agent.loadSession({ + sessionId: requestedId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith( + requestedId, + expect.any(String), + ) + }) + + test('resumeSession with existing session still calls switchSession', async () => { + const agent = new AcpAgent(makeConn()) + const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) + mockSwitchSession.mockClear() + + // Resume the same session — should still align global state + await agent.unstable_resumeSession({ + sessionId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith( + sessionId, + expect.any(String), + ) + }) + + test('prompt does not trigger additional switchSession for multi-session', async () => { + const agent = new AcpAgent(makeConn()) + await agent.newSession({ cwd: '/tmp' } as any) + await agent.newSession({ cwd: '/tmp' } as any) + mockSwitchSession.mockClear() + + // Prompts should not call switchSession — alignment happens at session creation + const s1 = agent.sessions.keys().next().value + await agent.prompt({ + sessionId: s1, + prompt: [{ type: 'text', text: 'hello' }], + } as any) + expect(mockSwitchSession).not.toHaveBeenCalled() + }) + }) })