mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
在 newSession/resumeSession/loadSession 中调用 switchSession, 确保 transcript 持久化、analytics 与 cost tracking 使用 ACP session ID, 而非内部默认 session ID。 - newSession 生成 sessionId 后立即对齐全局状态 - resumeSession 命中 fingerprint 缓存路径也对齐 - loadSession 在 sessionIdExists() 检查前对齐(lookup 依赖 getSessionId) - 补充 5 个测试覆盖上述路径,以及 prompt 不触发额外 switchSession
1227 lines
41 KiB
TypeScript
1227 lines
41 KiB
TypeScript
import {
|
|
describe,
|
|
expect,
|
|
test,
|
|
mock,
|
|
beforeEach,
|
|
afterEach,
|
|
afterAll,
|
|
spyOn,
|
|
} from 'bun:test'
|
|
|
|
// ── Mock infrastructure ──────────────────────────────────────────
|
|
// bun:test mock.module is process-global: it leaks to sibling test files
|
|
// in the same worker. Preserve real exports before partial module mocking
|
|
// so afterAll can restore them, preventing cross-file pollution.
|
|
|
|
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 mockModulePreservingExports(
|
|
tsPath: string,
|
|
overrides: Record<string, unknown>,
|
|
) {
|
|
const jsPath = tsPath.replace(/\.ts$/, '.js')
|
|
const snapshot = { ...(require(tsPath) as Record<string, unknown>) }
|
|
mock.module(jsPath, () => ({ ...snapshot, ...overrides }))
|
|
_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) ──
|
|
|
|
const mockSetModel = mock(() => {})
|
|
const mockSubmitMessage = mock(async function* (_input: string) {})
|
|
|
|
mockModulePreservingExports('../../../QueryEngine.ts', {
|
|
QueryEngine: class MockQueryEngine {
|
|
submitMessage = mockSubmitMessage
|
|
interrupt = mock(() => {})
|
|
resetAbortController = mock(() => {})
|
|
getAbortSignal = mock(() => new AbortController().signal)
|
|
setModel = mockSetModel
|
|
},
|
|
})
|
|
|
|
mockModulePreservingExports('../../../tools.ts', {
|
|
getTools: mock(() => []),
|
|
})
|
|
|
|
mockModulePreservingExports('../../../Tool.ts', {
|
|
toolMatchesName: mock(() => false),
|
|
findToolByName: mock(() => undefined),
|
|
filterToolProgressMessages: mock(() => []),
|
|
buildTool: mock((def: any) => def),
|
|
})
|
|
|
|
mockModulePreservingExports('../../../utils/config.ts', {
|
|
enableConfigs: mock(() => {}),
|
|
})
|
|
|
|
const mockSwitchSession = mock(() => {})
|
|
|
|
mockModulePreservingExports('../../../bootstrap/state.ts', {
|
|
setOriginalCwd: mock(() => {}),
|
|
switchSession: mockSwitchSession,
|
|
addSlowOperation: mock(() => {}),
|
|
})
|
|
|
|
const mockGetDefaultAppState = mock(() => ({
|
|
toolPermissionContext: {
|
|
mode: 'default',
|
|
additionalWorkingDirectories: new Map(),
|
|
alwaysAllowRules: { user: [], project: [], local: [] },
|
|
alwaysDenyRules: { user: [], project: [], local: [] },
|
|
alwaysAskRules: { user: [], project: [], local: [] },
|
|
isBypassPermissionsModeAvailable: true,
|
|
},
|
|
fastMode: false,
|
|
settings: {},
|
|
tasks: {},
|
|
verbose: false,
|
|
mainLoopModel: null,
|
|
mainLoopModelForSession: null,
|
|
}))
|
|
|
|
mockModulePreservingExports('../../../state/AppStateStore.ts', {
|
|
getDefaultAppState: mockGetDefaultAppState,
|
|
})
|
|
|
|
mockModulePreservingExports('../utils.ts', {
|
|
computeSessionFingerprint: mock(() => '{}'),
|
|
sanitizeTitle: mock((s: string) => s),
|
|
})
|
|
|
|
mockModulePreservingExports('../bridge.ts', {
|
|
forwardSessionUpdates: mock(async () => ({
|
|
stopReason: 'end_turn' as const,
|
|
})),
|
|
replayHistoryMessages: mock(async () => {}),
|
|
toolInfoFromToolUse: mock(() => ({
|
|
title: 'Test',
|
|
kind: 'other',
|
|
content: [],
|
|
locations: [],
|
|
})),
|
|
})
|
|
|
|
mockModulePreservingExports('../../../utils/listSessionsImpl.ts', {
|
|
listSessionsImpl: mock(async () => []),
|
|
})
|
|
|
|
const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6')
|
|
|
|
mockModulePreservingExports('../../../utils/model/model.ts', {
|
|
getMainLoopModel: mockGetMainLoopModel,
|
|
})
|
|
|
|
mockModulePreservingExports('../../../utils/model/modelOptions.ts', {
|
|
getModelOptions: mock(() => []),
|
|
})
|
|
|
|
const mockApplySafeEnvVars = mock(() => {})
|
|
mockModulePreservingExports('../../../utils/managedEnv.ts', {
|
|
applySafeConfigEnvironmentVariables: mockApplySafeEnvVars,
|
|
})
|
|
|
|
const mockGetSettings = mock(() => ({}))
|
|
mockModulePreservingExports('../../../utils/settings/settings.ts', {
|
|
getSettings_DEPRECATED: mockGetSettings,
|
|
})
|
|
|
|
const mockDeserializeMessages = mock((msgs: unknown[]) => msgs)
|
|
mockModulePreservingExports('../../../utils/conversationRecovery.ts', {
|
|
deserializeMessages: mockDeserializeMessages,
|
|
})
|
|
|
|
const mockGetLastSessionLog = mock(async () => null)
|
|
const mockSessionIdExists = mock(() => false)
|
|
mockModulePreservingExports('../../../utils/sessionStorage.ts', {
|
|
getLastSessionLog: mockGetLastSessionLog,
|
|
sessionIdExists: mockSessionIdExists,
|
|
})
|
|
|
|
const mockGetCommands = mock(async () => [
|
|
{
|
|
name: 'commit',
|
|
description: 'Create a git commit',
|
|
type: 'prompt',
|
|
userInvocable: true,
|
|
isHidden: false,
|
|
argumentHint: '[message]',
|
|
},
|
|
{
|
|
name: 'compact',
|
|
description: 'Compact conversation',
|
|
type: 'local',
|
|
userInvocable: true,
|
|
isHidden: false,
|
|
},
|
|
{
|
|
name: 'hidden-skill',
|
|
description: 'Hidden skill',
|
|
type: 'prompt',
|
|
userInvocable: false,
|
|
isHidden: true,
|
|
},
|
|
])
|
|
|
|
mockModulePreservingExports('../../../commands.ts', {
|
|
getCommands: mockGetCommands,
|
|
})
|
|
|
|
// ── Import after mocks ────────────────────────────────────────────
|
|
|
|
const { AcpAgent } = await import('../agent.js')
|
|
const { forwardSessionUpdates } = await import('../bridge.js')
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
|
|
function makeConn() {
|
|
return {
|
|
sessionUpdate: mock(async () => {}),
|
|
requestPermission: mock(async () => ({
|
|
outcome: { outcome: 'cancelled' },
|
|
})),
|
|
} 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 ─────────────────────────────────────────────────────────
|
|
|
|
describe('AcpAgent', () => {
|
|
beforeEach(() => {
|
|
delete process.env.ACP_PERMISSION_MODE
|
|
delete process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS
|
|
mockSetModel.mockClear()
|
|
mockSwitchSession.mockClear()
|
|
mockSubmitMessage.mockReset()
|
|
mockSubmitMessage.mockImplementation(async function* (_input: string) {})
|
|
mockGetMainLoopModel.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', () => {
|
|
test('returns protocol version and agent info', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.initialize({} as any)
|
|
expect(res.protocolVersion).toBeDefined()
|
|
expect(res.agentInfo?.name).toBe('claude-code')
|
|
expect(typeof res.agentInfo?.version).toBe('string')
|
|
})
|
|
|
|
test('advertises image and embeddedContext capability', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.initialize({} as any)
|
|
expect(res.agentCapabilities?.promptCapabilities?.image).toBe(true)
|
|
expect(res.agentCapabilities?.promptCapabilities?.embeddedContext).toBe(
|
|
true,
|
|
)
|
|
})
|
|
|
|
test('loadSession capability is true', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.initialize({} as any)
|
|
expect(res.agentCapabilities?.loadSession).toBe(true)
|
|
})
|
|
|
|
test('session capabilities include fork, list, resume, close', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.initialize({} as any)
|
|
expect(res.agentCapabilities?.sessionCapabilities).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('authenticate', () => {
|
|
test('returns empty object (no auth required)', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.authenticate({} as any)
|
|
expect(res).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe('newSession', () => {
|
|
test('returns a sessionId string', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(typeof res.sessionId).toBe('string')
|
|
expect(res.sessionId.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('returns modes and models', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(res.modes).toBeDefined()
|
|
expect(res.models).toBeDefined()
|
|
expect(res.configOptions).toBeDefined()
|
|
})
|
|
|
|
test('each call returns a unique sessionId', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const r1 = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const r2 = await agent.newSession({ cwd: '/tmp' } as any)
|
|
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 () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(mockGetDefaultAppState).toHaveBeenCalled()
|
|
})
|
|
|
|
test('calls getMainLoopModel to resolve current model', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(mockGetMainLoopModel).toHaveBeenCalled()
|
|
expect(res.models?.currentModelId).toBe('claude-sonnet-4-6')
|
|
})
|
|
|
|
test('calls queryEngine.setModel with resolved model', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(mockSetModel).toHaveBeenCalledWith('claude-sonnet-4-6')
|
|
})
|
|
|
|
test('respects model alias resolution via getMainLoopModel', async () => {
|
|
mockGetMainLoopModel.mockReturnValueOnce('glm-5.1')
|
|
const agent = new AcpAgent(makeConn())
|
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
|
expect(res.models?.currentModelId).toBe('glm-5.1')
|
|
expect(mockSetModel).toHaveBeenCalledWith('glm-5.1')
|
|
})
|
|
|
|
test('stores clientCapabilities from initialize', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await agent.initialize({
|
|
clientCapabilities: { _meta: { terminal_output: true } },
|
|
} as any)
|
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
|
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', () => {
|
|
test('throws when session not found', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await expect(
|
|
agent.prompt({ sessionId: 'nonexistent', prompt: [] } as any),
|
|
).rejects.toThrow('nonexistent')
|
|
})
|
|
|
|
test('returns end_turn for empty prompt text', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const res = await agent.prompt({ sessionId, prompt: [] } as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
test('returns end_turn for whitespace-only prompt', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: ' ' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
test('calls forwardSessionUpdates for valid prompt', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{ stopReason: 'end_turn' },
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
test('cancel before prompt does not block next prompt', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await agent.cancel({ sessionId } as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{ stopReason: 'end_turn' },
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
test('cancel during prompt returns cancelled', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
let resolveStream!: () => void
|
|
;(
|
|
forwardSessionUpdates as ReturnType<typeof mock>
|
|
).mockImplementationOnce(
|
|
() =>
|
|
new Promise<{ stopReason: string }>(resolve => {
|
|
resolveStream = () => resolve({ stopReason: 'cancelled' })
|
|
}),
|
|
)
|
|
const promptPromise = agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
await agent.cancel({ sessionId } as any)
|
|
resolveStream()
|
|
const res = await promptPromise
|
|
expect(res.stopReason).toBe('cancelled')
|
|
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{ stopReason: 'end_turn' },
|
|
)
|
|
const res2 = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'world' }],
|
|
} as any)
|
|
expect(res2.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
test('propagates unexpected prompt errors', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(
|
|
forwardSessionUpdates as ReturnType<typeof mock>
|
|
).mockImplementationOnce(async () => {
|
|
throw new Error('unexpected')
|
|
})
|
|
|
|
await expect(
|
|
agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any),
|
|
).rejects.toThrow('unexpected')
|
|
})
|
|
|
|
test('returns usage from forwardSessionUpdates', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{
|
|
stopReason: 'end_turn',
|
|
usage: {
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
cachedReadTokens: 10,
|
|
cachedWriteTokens: 5,
|
|
},
|
|
},
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.usage).toBeDefined()
|
|
expect(res.usage!.inputTokens).toBe(100)
|
|
expect(res.usage!.outputTokens).toBe(50)
|
|
expect(res.usage!.totalTokens).toBe(165)
|
|
})
|
|
})
|
|
|
|
describe('cancel', () => {
|
|
test('does not throw for unknown session', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await expect(
|
|
agent.cancel({ sessionId: 'ghost' } as any),
|
|
).resolves.toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('closeSession', () => {
|
|
test('throws for unknown session', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await expect(
|
|
agent.unstable_closeSession({ sessionId: 'ghost' } as any),
|
|
).rejects.toThrow('Session not found')
|
|
})
|
|
|
|
test('removes session after close', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await agent.unstable_closeSession({ sessionId } as any)
|
|
expect(agent.sessions.has(sessionId)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('setSessionModel', () => {
|
|
test('updates model on queryEngine', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
mockSetModel.mockClear()
|
|
await agent.unstable_setSessionModel({
|
|
sessionId,
|
|
modelId: 'glm-5.1',
|
|
} as any)
|
|
expect(mockSetModel).toHaveBeenCalledWith('glm-5.1')
|
|
})
|
|
|
|
test('passes alias modelId to queryEngine as-is for later resolution', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
mockSetModel.mockClear()
|
|
await agent.unstable_setSessionModel({
|
|
sessionId,
|
|
modelId: 'sonnet[1m]',
|
|
} as any)
|
|
expect(mockSetModel).toHaveBeenCalledWith('sonnet[1m]')
|
|
})
|
|
})
|
|
|
|
describe('entry.ts initialization contract', () => {
|
|
test('entry.ts imports applySafeConfigEnvironmentVariables from managedEnv', async () => {
|
|
const entrySource = await Bun.file(
|
|
new URL('../entry.ts', import.meta.url),
|
|
).text()
|
|
expect(entrySource).toContain('applySafeConfigEnvironmentVariables')
|
|
expect(entrySource).toContain('enableConfigs')
|
|
|
|
const enableIdx = entrySource.indexOf('enableConfigs()')
|
|
const applyIdx = entrySource.indexOf(
|
|
'applySafeConfigEnvironmentVariables()',
|
|
)
|
|
expect(enableIdx).toBeGreaterThan(-1)
|
|
expect(applyIdx).toBeGreaterThan(-1)
|
|
expect(enableIdx).toBeLessThan(applyIdx)
|
|
})
|
|
})
|
|
|
|
describe('prompt usage tracking', () => {
|
|
test('returns totalTokens as sum of all token types', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{
|
|
stopReason: 'end_turn',
|
|
usage: {
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
cachedReadTokens: 10,
|
|
cachedWriteTokens: 5,
|
|
},
|
|
},
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.usage).toBeDefined()
|
|
expect(res.usage!.totalTokens).toBe(165)
|
|
})
|
|
|
|
test('returns undefined usage when forwardSessionUpdates returns none', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{
|
|
stopReason: 'end_turn',
|
|
},
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.usage).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('prompt error handling', () => {
|
|
test('returns cancelled when session was cancelled during prompt', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(
|
|
forwardSessionUpdates as ReturnType<typeof mock>
|
|
).mockImplementationOnce(async () => {
|
|
const session = agent.sessions.get(sessionId)
|
|
if (session) session.cancelled = true
|
|
return { stopReason: 'end_turn' }
|
|
})
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('cancelled')
|
|
})
|
|
|
|
test('returns cancelled on cancel after error', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
;(
|
|
forwardSessionUpdates as ReturnType<typeof mock>
|
|
).mockImplementationOnce(async () => {
|
|
const session = agent.sessions.get(sessionId)
|
|
if (session) session.cancelled = true
|
|
throw new Error('unexpected')
|
|
})
|
|
const res = await agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'hello' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('cancelled')
|
|
})
|
|
})
|
|
|
|
describe('resumeSession', () => {
|
|
test('creates new session with the requested sessionId when not in memory', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const requestedId = 'e73e9b66-9637-4477-b512-af45357b1dcb'
|
|
const res = await agent.unstable_resumeSession({
|
|
sessionId: requestedId,
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
expect(agent.sessions.has(requestedId)).toBe(true)
|
|
expect(res.modes).toBeDefined()
|
|
expect(res.models).toBeDefined()
|
|
})
|
|
|
|
test('reuses existing session when sessionId matches and fingerprint unchanged', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const res1 = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const sid = res1.sessionId
|
|
const originalSession = agent.sessions.get(sid)
|
|
const res2 = await agent.unstable_resumeSession({
|
|
sessionId: sid,
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
expect(agent.sessions.get(sid)).toBe(originalSession)
|
|
})
|
|
|
|
test('can prompt after resumeSession with previously unknown sessionId', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const sid = 'restored-session-id-1234'
|
|
await agent.unstable_resumeSession({
|
|
sessionId: sid,
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{ stopReason: 'end_turn' },
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId: sid,
|
|
prompt: [{ type: 'text', text: 'hello after restore' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
})
|
|
|
|
describe('loadSession', () => {
|
|
test('creates new session with the requested sessionId', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const requestedId = 'aaaa-bbbb-cccc'
|
|
await agent.loadSession({
|
|
sessionId: requestedId,
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
expect(agent.sessions.has(requestedId)).toBe(true)
|
|
})
|
|
|
|
test('can prompt after loadSession', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const sid = 'loaded-session-id'
|
|
await agent.loadSession({
|
|
sessionId: sid,
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce(
|
|
{ stopReason: 'end_turn' },
|
|
)
|
|
const res = await agent.prompt({
|
|
sessionId: sid,
|
|
prompt: [{ type: 'text', text: 'hello after load' }],
|
|
} as any)
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
})
|
|
|
|
describe('forkSession', () => {
|
|
test('returns a different sessionId from any existing', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const original = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const forked = await agent.unstable_forkSession({
|
|
cwd: '/tmp',
|
|
mcpServers: [],
|
|
} as any)
|
|
expect(forked.sessionId).not.toBe(original.sessionId)
|
|
expect(agent.sessions.has(forked.sessionId)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('setSessionMode', () => {
|
|
test('updates current mode on the session', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await agent.setSessionMode({ sessionId, modeId: 'auto' } as any)
|
|
const session = agent.sessions.get(sessionId)
|
|
expect(session?.modes.currentModeId).toBe('auto')
|
|
})
|
|
|
|
test('throws for invalid mode', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await expect(
|
|
agent.setSessionMode({ sessionId, modeId: 'invalid_mode' } as any),
|
|
).rejects.toThrow('Invalid mode')
|
|
})
|
|
|
|
test('throws for unknown session', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
await expect(
|
|
agent.setSessionMode({ sessionId: 'ghost', modeId: 'auto' } as any),
|
|
).rejects.toThrow('Session not found')
|
|
})
|
|
|
|
test('availableModes excludes bypassPermissions without a local ACP bypass gate', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
const session = agent.sessions.get(sessionId)
|
|
const modeIds = session?.modes.availableModes.map((m: any) => m.id)
|
|
expect(modeIds).not.toContain('bypassPermissions')
|
|
})
|
|
|
|
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 { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await agent.setSessionMode({
|
|
sessionId,
|
|
modeId: 'bypassPermissions',
|
|
} as any)
|
|
const session = agent.sessions.get(sessionId)
|
|
expect(session?.modes.currentModeId).toBe('bypassPermissions')
|
|
expect(session?.appState.toolPermissionContext.mode).toBe(
|
|
'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', () => {
|
|
test('throws for unknown config option', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await expect(
|
|
agent.setSessionConfigOption({
|
|
sessionId,
|
|
configId: 'nonexistent',
|
|
value: 'x',
|
|
} as any),
|
|
).rejects.toThrow('Unknown config option')
|
|
})
|
|
|
|
test('throws for non-string value', async () => {
|
|
const agent = new AcpAgent(makeConn())
|
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
await expect(
|
|
agent.setSessionConfigOption({
|
|
sessionId,
|
|
configId: 'mode',
|
|
value: 42,
|
|
} as any),
|
|
).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', () => {
|
|
test('queued prompts execute in order after current prompt finishes', 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)
|
|
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('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 () => {
|
|
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 p1 = agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'first' }],
|
|
} as any)
|
|
const p2 = agent.prompt({
|
|
sessionId,
|
|
prompt: [{ type: 'text', text: 'second' }],
|
|
} as any)
|
|
|
|
await agent.cancel({ sessionId } as any)
|
|
resolveFirst()
|
|
const [r1, r2] = await Promise.all([p1, p2])
|
|
expect(r1.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', () => {
|
|
test('sends filtered prompt-type commands to client', async () => {
|
|
const conn = makeConn()
|
|
const agent = new AcpAgent(conn)
|
|
await agent.newSession({ cwd: '/tmp' } as any)
|
|
|
|
await new Promise(r => setTimeout(r, 10))
|
|
|
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
|
const cmdUpdate = calls.find((c: any[]) => {
|
|
const update = c[0]?.update
|
|
return update?.sessionUpdate === 'available_commands_update'
|
|
})
|
|
expect(cmdUpdate).toBeDefined()
|
|
|
|
const cmds = (cmdUpdate as any[])[0].update.availableCommands
|
|
const names = cmds.map((c: any) => c.name)
|
|
expect(names).toContain('commit')
|
|
expect(names).not.toContain('compact')
|
|
expect(names).not.toContain('hidden-skill')
|
|
})
|
|
|
|
test('maps argumentHint to input.hint', async () => {
|
|
const conn = makeConn()
|
|
const agent = new AcpAgent(conn)
|
|
await agent.newSession({ cwd: '/tmp' } as any)
|
|
|
|
await new Promise(r => setTimeout(r, 10))
|
|
|
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
|
const cmdUpdate = calls.find((c: any[]) => {
|
|
const update = c[0]?.update
|
|
return update?.sessionUpdate === 'available_commands_update'
|
|
})
|
|
const commit = (cmdUpdate as any[])[0].update.availableCommands.find(
|
|
(c: any) => c.name === 'commit',
|
|
)
|
|
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()
|
|
})
|
|
})
|
|
})
|