edges.spec.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  2. import { mkdtemp, rm } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
  6. import { SessionId } from '@deepseek-ai/dsh-session'
  7. import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
  8. describe('acp bridge — demux & config edges', () => {
  9. let storageDir: string
  10. let harness: BridgeHarness | undefined
  11. beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-edge-')) })
  12. afterEach(async () => {
  13. if (harness) await harness.dispose()
  14. harness = undefined
  15. await rm(storageDir, { recursive: true, force: true })
  16. })
  17. it('ignores events from an agent the bridge does not own (strict id demux)', async () => {
  18. // A second agent created directly on the registry (NOT via the bridge) runs
  19. // a turn. Its session events must NOT produce ACP updates and
  20. // must not settle anything — the bridge demuxes strictly by its own id.
  21. harness = await makeBridgeHarness({ storageDir, script: [textResponse('foreign')] })
  22. await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
  23. await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
  24. await vi.waitFor(() => {
  25. expect(harness!.updates.some(update => update.sessionUpdate === 'available_commands_update')).toBe(true)
  26. })
  27. const before = harness.updates.length
  28. const { agent: foreign } = await harness.ctx.agents.create({ sessionId: SessionId('foreign-session'), agentOptions: { provider: 'mock', model: 'mock' } })
  29. foreign.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } })
  30. await foreign.whenIdle()
  31. await new Promise(r => setTimeout(r, 10))
  32. // No update was emitted for the foreign agent's stream.
  33. expect(harness.updates.length).toBe(before)
  34. })
  35. it('survives a session/update that the client rejects (best-effort notify)', async () => {
  36. harness = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] })
  37. await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
  38. const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
  39. // Make the client reject every update — the bridge's notify() must swallow
  40. // the rejection and the prompt must still settle normally.
  41. harness.onSessionUpdateError = () => { throw new Error('client update rejected') }
  42. const res = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
  43. expect(res.stopReason).toBe('end_turn')
  44. })
  45. it('accepts session/new with additionalDirectories empty', async () => {
  46. // Exercises the defined-but-empty additionalDirectories branch (length 0 → allowed).
  47. harness = await makeBridgeHarness({ storageDir })
  48. await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
  49. const a = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [], additionalDirectories: [] })
  50. expect(a.sessionId).toBeTruthy()
  51. })
  52. it('rejects non-empty mcpServers until MCP wiring is implemented', async () => {
  53. harness = await makeBridgeHarness({ storageDir })
  54. await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
  55. await expect(harness.client.newSession({
  56. cwd: process.cwd(),
  57. mcpServers: [{ name: 'fs', command: 'npx', args: ['server'], env: [] }],
  58. })).rejects.toThrow(/mcpServers/)
  59. })
  60. })