fix(acp): 对齐 ACP session ID 与全局会话状态

在 newSession/resumeSession/loadSession 中调用 switchSession,
确保 transcript 持久化、analytics 与 cost tracking 使用 ACP session ID,
而非内部默认 session ID。

- newSession 生成 sessionId 后立即对齐全局状态
- resumeSession 命中 fingerprint 缓存路径也对齐
- loadSession 在 sessionIdExists() 检查前对齐(lookup 依赖 getSessionId)
- 补充 5 个测试覆盖上述路径,以及 prompt 不触发额外 switchSession
This commit is contained in:
xuzhongpeng.xzp 2026-05-12 19:03:27 +08:00 committed by James Feng
parent 188bcea5ba
commit 6950401c06
2 changed files with 1469 additions and 70 deletions

View File

@ -4,57 +4,76 @@ import {
test, test,
mock, mock,
beforeEach, beforeEach,
afterEach,
afterAll, afterAll,
spyOn, spyOn,
} from 'bun:test' } from 'bun:test'
// ── Mock infrastructure ────────────────────────────────────────── // ── Mock infrastructure ──────────────────────────────────────────
// bun:test mock.module is process-global: it leaks to sibling test files // bun:test mock.module is process-global: it leaks to sibling test files
// in the same worker. safeMockModule snapshots real exports before mocking // in the same worker. Preserve real exports before partial module mocking
// so afterAll can restore them, preventing cross-file pollution. // so afterAll can restore them, preventing cross-file pollution.
const _restores: (() => void)[] = [] const _restores: (() => void)[] = []
const originalCwd = process.cwd()
const originalAcpPermissionMode = process.env.ACP_PERMISSION_MODE
const originalAcpAllowBypass =
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS
function safeMockModule(tsPath: string, overrides: Record<string, unknown>) { function mockModulePreservingExports(
tsPath: string,
overrides: Record<string, unknown>,
) {
const jsPath = tsPath.replace(/\.ts$/, '.js') const jsPath = tsPath.replace(/\.ts$/, '.js')
const real = require(tsPath) const snapshot = { ...(require(tsPath) as Record<string, unknown>) }
const snapshot = { ...real }
mock.module(jsPath, () => ({ ...snapshot, ...overrides })) mock.module(jsPath, () => ({ ...snapshot, ...overrides }))
_restores.push(() => mock.module(jsPath, () => snapshot)) _restores.push(() => mock.module(jsPath, () => snapshot))
} }
afterAll(() => {
for (let i = _restores.length - 1; i >= 0; i--) {
_restores[i]()
}
_restores.length = 0
restoreEnv('ACP_PERMISSION_MODE', originalAcpPermissionMode)
restoreEnv('CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS', originalAcpAllowBypass)
})
// ── Module mocks (must precede any import of the module under test) ── // ── Module mocks (must precede any import of the module under test) ──
const mockSetModel = mock(() => {}) const mockSetModel = mock(() => {})
const mockSubmitMessage = mock(async function* (_input: string) {})
// Fully synthetic — no real module to snapshot, so plain mock.module suffices. mockModulePreservingExports('../../../QueryEngine.ts', {
mock.module('../../../QueryEngine.js', () => ({
QueryEngine: class MockQueryEngine { QueryEngine: class MockQueryEngine {
submitMessage = mock(async function* () {}) submitMessage = mockSubmitMessage
interrupt = mock(() => {}) interrupt = mock(() => {})
resetAbortController = mock(() => {}) resetAbortController = mock(() => {})
getAbortSignal = mock(() => new AbortController().signal) getAbortSignal = mock(() => new AbortController().signal)
setModel = mockSetModel setModel = mockSetModel
}, },
})) })
safeMockModule('../../../tools.ts', { mockModulePreservingExports('../../../tools.ts', {
getTools: mock(() => []), getTools: mock(() => []),
}) })
safeMockModule('../../../Tool.ts', { mockModulePreservingExports('../../../Tool.ts', {
toolMatchesName: mock(() => false), toolMatchesName: mock(() => false),
findToolByName: mock(() => undefined), findToolByName: mock(() => undefined),
filterToolProgressMessages: mock(() => []), filterToolProgressMessages: mock(() => []),
buildTool: mock((def: any) => def), buildTool: mock((def: any) => def),
}) })
safeMockModule('../../../utils/config.ts', { mockModulePreservingExports('../../../utils/config.ts', {
enableConfigs: mock(() => {}), enableConfigs: mock(() => {}),
}) })
safeMockModule('../../../bootstrap/state.ts', { const mockSwitchSession = mock(() => {})
mockModulePreservingExports('../../../bootstrap/state.ts', {
setOriginalCwd: mock(() => {}), setOriginalCwd: mock(() => {}),
switchSession: mockSwitchSession,
addSlowOperation: mock(() => {}), addSlowOperation: mock(() => {}),
}) })
@ -75,24 +94,16 @@ const mockGetDefaultAppState = mock(() => ({
mainLoopModelForSession: null, mainLoopModelForSession: null,
})) }))
safeMockModule('../../../state/AppStateStore.ts', { mockModulePreservingExports('../../../state/AppStateStore.ts', {
getDefaultAppState: mockGetDefaultAppState, getDefaultAppState: mockGetDefaultAppState,
}) })
// Single export, fully synthetic — no real module to snapshot. mockModulePreservingExports('../utils.ts', {
mock.module('../permissions.js', () => ({
createAcpCanUseTool: mock(() =>
mock(async () => ({ behavior: 'allow', updatedInput: {} })),
),
}))
safeMockModule('../utils.ts', {
resolvePermissionMode: mock(() => 'default'),
computeSessionFingerprint: mock(() => '{}'), computeSessionFingerprint: mock(() => '{}'),
sanitizeTitle: mock((s: string) => s), sanitizeTitle: mock((s: string) => s),
}) })
safeMockModule('../bridge.ts', { mockModulePreservingExports('../bridge.ts', {
forwardSessionUpdates: mock(async () => ({ forwardSessionUpdates: mock(async () => ({
stopReason: 'end_turn' as const, stopReason: 'end_turn' as const,
})), })),
@ -105,33 +116,38 @@ safeMockModule('../bridge.ts', {
})), })),
}) })
safeMockModule('../../../utils/listSessionsImpl.ts', { mockModulePreservingExports('../../../utils/listSessionsImpl.ts', {
listSessionsImpl: mock(async () => []), listSessionsImpl: mock(async () => []),
}) })
const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6') const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6')
safeMockModule('../../../utils/model/model.ts', { mockModulePreservingExports('../../../utils/model/model.ts', {
getMainLoopModel: mockGetMainLoopModel, getMainLoopModel: mockGetMainLoopModel,
}) })
safeMockModule('../../../utils/model/modelOptions.ts', { mockModulePreservingExports('../../../utils/model/modelOptions.ts', {
getModelOptions: mock(() => []), getModelOptions: mock(() => []),
}) })
const mockApplySafeEnvVars = mock(() => {}) const mockApplySafeEnvVars = mock(() => {})
safeMockModule('../../../utils/managedEnv.ts', { mockModulePreservingExports('../../../utils/managedEnv.ts', {
applySafeConfigEnvironmentVariables: mockApplySafeEnvVars, applySafeConfigEnvironmentVariables: mockApplySafeEnvVars,
}) })
const mockGetSettings = mock(() => ({}))
mockModulePreservingExports('../../../utils/settings/settings.ts', {
getSettings_DEPRECATED: mockGetSettings,
})
const mockDeserializeMessages = mock((msgs: unknown[]) => msgs) const mockDeserializeMessages = mock((msgs: unknown[]) => msgs)
safeMockModule('../../../utils/conversationRecovery.ts', { mockModulePreservingExports('../../../utils/conversationRecovery.ts', {
deserializeMessages: mockDeserializeMessages, deserializeMessages: mockDeserializeMessages,
}) })
const mockGetLastSessionLog = mock(async () => null) const mockGetLastSessionLog = mock(async () => null)
const mockSessionIdExists = mock(() => false) const mockSessionIdExists = mock(() => false)
safeMockModule('../../../utils/sessionStorage.ts', { mockModulePreservingExports('../../../utils/sessionStorage.ts', {
getLastSessionLog: mockGetLastSessionLog, getLastSessionLog: mockGetLastSessionLog,
sessionIdExists: mockSessionIdExists, sessionIdExists: mockSessionIdExists,
}) })
@ -161,7 +177,7 @@ const mockGetCommands = mock(async () => [
}, },
]) ])
safeMockModule('../../../commands.ts', { mockModulePreservingExports('../../../commands.ts', {
getCommands: mockGetCommands, getCommands: mockGetCommands,
}) })
@ -181,16 +197,49 @@ function makeConn() {
} as any } as any
} }
function removeBypassMode(session: any) {
session.modes = {
...session.modes,
availableModes: session.modes.availableModes.filter(
(mode: any) => mode.id !== 'bypassPermissions',
),
}
session.appState.toolPermissionContext = {
...session.appState.toolPermissionContext,
isBypassPermissionsModeAvailable: false,
}
}
function restoreEnv(name: string, value: string | undefined) {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
// ── Tests ───────────────────────────────────────────────────────── // ── Tests ─────────────────────────────────────────────────────────
describe('AcpAgent', () => { describe('AcpAgent', () => {
afterAll(() => {
for (const restore of _restores) restore()
})
beforeEach(() => { beforeEach(() => {
delete process.env.ACP_PERMISSION_MODE
delete process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS
mockSetModel.mockClear() mockSetModel.mockClear()
mockSwitchSession.mockClear()
mockSubmitMessage.mockReset()
mockSubmitMessage.mockImplementation(async function* (_input: string) {})
mockGetMainLoopModel.mockClear() mockGetMainLoopModel.mockClear()
mockGetDefaultAppState.mockClear() mockGetDefaultAppState.mockClear()
mockGetSettings.mockReset()
mockGetSettings.mockImplementation(() => ({}))
;(forwardSessionUpdates as ReturnType<typeof mock>).mockReset()
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementation(
async () => ({ stopReason: 'end_turn' as const }),
)
})
afterEach(() => {
process.chdir(originalCwd)
}) })
describe('initialize', () => { describe('initialize', () => {
@ -255,6 +304,13 @@ describe('AcpAgent', () => {
expect(r1.sessionId).not.toBe(r2.sessionId) expect(r1.sessionId).not.toBe(r2.sessionId)
}) })
test('does not leave process cwd changed after session creation', async () => {
const cwdBeforeSession = process.cwd()
const agent = new AcpAgent(makeConn())
await agent.newSession({ cwd: '/tmp' } as any)
expect(process.cwd()).toBe(cwdBeforeSession)
})
test('calls getDefaultAppState to build session appState', async () => { test('calls getDefaultAppState to build session appState', async () => {
const agent = new AcpAgent(makeConn()) const agent = new AcpAgent(makeConn())
await agent.newSession({ cwd: '/tmp' } as any) await agent.newSession({ cwd: '/tmp' } as any)
@ -290,6 +346,105 @@ describe('AcpAgent', () => {
const res = await agent.newSession({ cwd: '/tmp' } as any) const res = await agent.newSession({ cwd: '/tmp' } as any)
expect(res.sessionId).toBeDefined() expect(res.sessionId).toBeDefined()
}) })
test('uses settings permissions.defaultMode when _meta does not provide a mode', async () => {
mockGetSettings.mockImplementationOnce(() => ({
permissions: { defaultMode: 'acceptEdits' },
}))
const agent = new AcpAgent(makeConn())
const res = await agent.newSession({ cwd: '/tmp' } as any)
expect(res.modes?.currentModeId).toBe('acceptEdits')
})
test('uses _meta.permissionMode before settings permissions.defaultMode', async () => {
mockGetSettings.mockImplementationOnce(() => ({
permissions: { defaultMode: 'acceptEdits' },
}))
const agent = new AcpAgent(makeConn())
const res = await agent.newSession({
cwd: '/tmp',
_meta: { permissionMode: 'plan' },
} as any)
expect(res.modes?.currentModeId).toBe('plan')
})
test('rejects _meta.permissionMode bypass without a local ACP bypass gate', async () => {
mockGetSettings.mockImplementationOnce(() => ({
permissions: { defaultMode: 'acceptEdits' },
}))
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
() => {},
)
const agent = new AcpAgent(makeConn())
try {
await expect(
agent.newSession({
cwd: '/tmp',
_meta: { permissionMode: 'bypassPermissions' },
} as any),
).rejects.toThrow('Mode not available: bypassPermissions')
expect(consoleErrorSpy).not.toHaveBeenCalled()
} finally {
consoleErrorSpy.mockRestore()
}
})
test('honors _meta.permissionMode bypass with a local ACP bypass gate', async () => {
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
const agent = new AcpAgent(makeConn())
const res = await agent.newSession({
cwd: '/tmp',
_meta: { permissionMode: 'bypassPermissions' },
} as any)
expect(res.modes?.currentModeId).toBe('bypassPermissions')
expect(res.modes?.availableModes.map((mode: any) => mode.id)).toContain(
'bypassPermissions',
)
})
test('falls back to default when settings permissions.defaultMode is invalid', async () => {
mockGetSettings.mockImplementationOnce(() => ({
permissions: { defaultMode: 'invalid-mode' },
}))
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
() => {},
)
const agent = new AcpAgent(makeConn())
try {
const res = await agent.newSession({ cwd: '/tmp' } as any)
expect(res.modes?.currentModeId).toBe('default')
expect(consoleErrorSpy).toHaveBeenCalled()
} finally {
consoleErrorSpy.mockRestore()
}
})
test('rejects invalid _meta.permissionMode without falling back to settings', async () => {
mockGetSettings.mockImplementationOnce(() => ({
permissions: { defaultMode: 'acceptEdits' },
}))
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
() => {},
)
const agent = new AcpAgent(makeConn())
try {
await expect(
agent.newSession({
cwd: '/tmp',
_meta: { permissionMode: 'invalid-mode' },
} as any),
).rejects.toThrow('Invalid _meta.permissionMode: invalid-mode')
expect(consoleErrorSpy).not.toHaveBeenCalled()
} finally {
consoleErrorSpy.mockRestore()
}
})
}) })
describe('prompt', () => { describe('prompt', () => {
@ -375,7 +530,7 @@ describe('AcpAgent', () => {
expect(res2.stopReason).toBe('end_turn') expect(res2.stopReason).toBe('end_turn')
}) })
test('returns end_turn on unexpected error', async () => { test('propagates unexpected prompt errors', async () => {
const agent = new AcpAgent(makeConn()) const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
;( ;(
@ -383,16 +538,13 @@ describe('AcpAgent', () => {
).mockImplementationOnce(async () => { ).mockImplementationOnce(async () => {
throw new Error('unexpected') throw new Error('unexpected')
}) })
const errorSpy = spyOn(console, 'error').mockImplementation(() => {})
try { await expect(
const res = await agent.prompt({ agent.prompt({
sessionId, sessionId,
prompt: [{ type: 'text', text: 'hello' }], prompt: [{ type: 'text', text: 'hello' }],
} as any) } as any),
expect(res.stopReason).toBe('end_turn') ).rejects.toThrow('unexpected')
} finally {
errorSpy.mockRestore()
}
}) })
test('returns usage from forwardSessionUpdates', async () => { test('returns usage from forwardSessionUpdates', async () => {
@ -676,15 +828,28 @@ describe('AcpAgent', () => {
).rejects.toThrow('Session not found') ).rejects.toThrow('Session not found')
}) })
test('availableModes includes bypassPermissions when not root', async () => { test('availableModes excludes bypassPermissions without a local ACP bypass gate', async () => {
const agent = new AcpAgent(makeConn()) const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
const session = agent.sessions.get(sessionId) const session = agent.sessions.get(sessionId)
const modeIds = session?.modes.availableModes.map((m: any) => m.id) const modeIds = session?.modes.availableModes.map((m: any) => m.id)
expect(modeIds).toContain('bypassPermissions') expect(modeIds).not.toContain('bypassPermissions')
}) })
test('can switch to bypassPermissions mode', async () => { test('rejects bypassPermissions without a local ACP bypass gate', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
await expect(
agent.setSessionMode({ sessionId, modeId: 'bypassPermissions' } as any),
).rejects.toThrow('Mode not available')
const session = agent.sessions.get(sessionId)
expect(session?.modes.currentModeId).toBe('default')
expect(session?.appState.toolPermissionContext.mode).toBe('default')
})
test('can switch to bypassPermissions mode with a local ACP bypass gate', async () => {
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
const agent = new AcpAgent(makeConn()) const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
await agent.setSessionMode({ await agent.setSessionMode({
@ -697,6 +862,21 @@ describe('AcpAgent', () => {
'bypassPermissions', 'bypassPermissions',
) )
}) })
test('rejects bypassPermissions when the session does not expose it', async () => {
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
const session = agent.sessions.get(sessionId)
removeBypassMode(session)
await expect(
agent.setSessionMode({ sessionId, modeId: 'bypassPermissions' } as any),
).rejects.toThrow('Mode not available')
expect(session?.modes.currentModeId).toBe('default')
expect(session?.appState.toolPermissionContext.mode).toBe('default')
})
}) })
describe('setSessionConfigOption', () => { describe('setSessionConfigOption', () => {
@ -723,6 +903,24 @@ describe('AcpAgent', () => {
} as any), } as any),
).rejects.toThrow('Invalid value') ).rejects.toThrow('Invalid value')
}) })
test('rejects unavailable mode config values', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
const session = agent.sessions.get(sessionId)
removeBypassMode(session)
await expect(
agent.setSessionConfigOption({
sessionId,
configId: 'mode',
value: 'bypassPermissions',
} as any),
).rejects.toThrow('Mode not available')
expect(session?.modes.currentModeId).toBe('default')
expect(session?.appState.toolPermissionContext.mode).toBe('default')
})
}) })
describe('prompt queueing', () => { describe('prompt queueing', () => {
@ -758,6 +956,100 @@ describe('AcpAgent', () => {
expect(r2.stopReason).toBe('end_turn') expect(r2.stopReason).toBe('end_turn')
}) })
test('drains 1000 queued prompts in FIFO order without sorting the pending map', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
let resolveFirst!: () => void
;(
forwardSessionUpdates as ReturnType<typeof mock>
).mockImplementationOnce(
() =>
new Promise<{ stopReason: string }>(resolve => {
resolveFirst = () => resolve({ stopReason: 'end_turn' })
}),
)
const first = agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'first' }],
} as any)
const queued = Array.from({ length: 1000 }, (_, index) =>
agent.prompt({
sessionId,
prompt: [{ type: 'text', text: `queued-${index}` }],
} as any),
)
resolveFirst()
const results = await Promise.all([first, ...queued])
expect(results.every(result => result.stopReason === 'end_turn')).toBe(
true,
)
expect(mockSubmitMessage.mock.calls.map(call => call[0])).toEqual([
'first',
...Array.from({ length: 1000 }, (_, index) => `queued-${index}`),
])
})
test('keeps promptRunning true while handing off to the next queued prompt', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
let resolveFirst!: () => void
let resolveSecond!: () => void
;(
forwardSessionUpdates as ReturnType<typeof mock>
).mockImplementationOnce(
() =>
new Promise<{ stopReason: string }>(resolve => {
resolveFirst = () => resolve({ stopReason: 'end_turn' })
}),
)
;(
forwardSessionUpdates as ReturnType<typeof mock>
).mockImplementationOnce(
() =>
new Promise<{ stopReason: string }>(resolve => {
resolveSecond = () => resolve({ stopReason: 'end_turn' })
}),
)
const p1 = agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'first' }],
} as any)
const p2 = agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'second' }],
} as any)
const p3 = p1.then(() =>
agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'third' }],
} as any),
)
resolveFirst()
await p1
const session = agent.sessions.get(sessionId)
expect(session?.promptRunning).toBe(true)
expect(mockSubmitMessage.mock.calls.map(call => call[0])).toEqual([
'first',
'second',
])
resolveSecond()
await Promise.all([p2, p3])
expect(mockSubmitMessage.mock.calls.map(call => call[0])).toEqual([
'first',
'second',
'third',
])
})
test('queued prompts return cancelled when session is cancelled', async () => { test('queued prompts return cancelled when session is cancelled', async () => {
const agent = new AcpAgent(makeConn()) const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
@ -787,6 +1079,46 @@ describe('AcpAgent', () => {
expect(r1.stopReason).toBe('cancelled') expect(r1.stopReason).toBe('cancelled')
expect(r2.stopReason).toBe('cancelled') expect(r2.stopReason).toBe('cancelled')
}) })
test('queued prompt does not clear active prompt cancellation', async () => {
const agent = new AcpAgent(makeConn())
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
let resolveFirst!: () => void
;(
forwardSessionUpdates as ReturnType<typeof mock>
).mockImplementationOnce(
() =>
new Promise<{ stopReason: string }>(resolve => {
resolveFirst = () => resolve({ stopReason: 'end_turn' })
}),
)
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
{ stopReason: 'end_turn' },
)
const p1 = agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'first' }],
} as any)
await agent.cancel({ sessionId } as any)
const p2 = agent.prompt({
sessionId,
prompt: [{ type: 'text', text: 'second' }],
} as any)
resolveFirst()
const [r1, r2] = await Promise.all([p1, p2])
expect(r1.stopReason).toBe('cancelled')
expect(r2.stopReason).toBe('end_turn')
expect(mockSubmitMessage.mock.calls.map(call => call[0])).toEqual([
'first',
'second',
])
})
}) })
describe('commands', () => { describe('commands', () => {
@ -829,4 +1161,66 @@ describe('AcpAgent', () => {
expect(commit.input).toEqual({ hint: '[message]' }) 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)
})
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)
})
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)
})
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)
})
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()
})
})
}) })

File diff suppressed because it is too large Load Diff