controller.host.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { Context } from '@deepseek-ai/cordis'
  2. import AgentRegistry from '@deepseek-ai/dsh-agent'
  3. import type { Agent } from '@deepseek-ai/dsh-agent'
  4. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  5. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  6. import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
  7. import { describe, expect, it, vi } from 'vitest'
  8. import type { SessionInteractionId } from '../src/types.ts'
  9. import { createSessionTestController } from './test-remote.ts'
  10. const defaults = {
  11. defaultModelSelection: () => ({ provider: 'fixture', model: 'fixture-model' }),
  12. cwd: '/tmp',
  13. }
  14. describe('SessionController facade', () => {
  15. it('owns Host service methods and publishes Agent lifecycle projections', async () => {
  16. const ctx = new Context()
  17. await ctx.plugin(SessionStore)
  18. await ctx.plugin(AgentRegistry)
  19. const sessionId = SessionId('controller-session')
  20. const header: SessionHeader = {
  21. version: 0,
  22. id: sessionId,
  23. createdAt: 1,
  24. cwd: '/workspace',
  25. }
  26. const events: SessionEvent[] = []
  27. const inspect = vi.fn(() => Promise.resolve({ meta: header, events }))
  28. ctx.provide('sessionPersistence', {
  29. list: () => Promise.resolve([header]),
  30. inspect,
  31. } as never)
  32. const controller = createSessionTestController(ctx, defaults)
  33. const status = vi.fn()
  34. const failure = vi.fn()
  35. const activity = vi.fn()
  36. ctx.on('api-session/status', status)
  37. ctx.on('api-session/error', failure)
  38. ctx.on('api-session/activity', activity)
  39. await expect(controller.inspect(sessionId)).resolves.toEqual({ meta: header, events })
  40. expect(inspect).toHaveBeenCalledOnce()
  41. const session = ctx.sessions.create(sessionId, { meta: header })
  42. const agent = {
  43. id: sessionId,
  44. session,
  45. status: 'idle',
  46. ctx,
  47. } as Agent
  48. ctx.agents.register(agent)
  49. await expect(controller.resolveAgent(sessionId)).resolves.toEqual({ agent })
  50. await expect(controller.inspect(sessionId)).resolves.toEqual({ meta: header, events })
  51. expect(inspect).toHaveBeenCalledOnce()
  52. ctx.emit('agent/status', { agent, status: 'running' })
  53. ctx.emit('agent/error', { agent, turn: 1, step: 0, error: new Error('fixture failure') })
  54. session.append('user/message', createUserMessage({
  55. content: [{ type: 'text', text: 'hello' }],
  56. source: { kind: 'user' },
  57. }), { surfaceOp: 'append' })
  58. expect(status).toHaveBeenCalledWith(sessionId, true)
  59. expect(failure).toHaveBeenCalledWith(sessionId, expect.stringContaining('fixture failure'))
  60. expect(activity).toHaveBeenCalledWith(sessionId, expect.any(Number))
  61. const abort = new AbortController()
  62. const iterator = controller.follow({
  63. address: { kind: 'session', sessionId },
  64. }, abort.signal)[Symbol.asyncIterator]()
  65. await expect(iterator.next()).resolves.toMatchObject({
  66. done: false,
  67. value: { type: 'opened', cursor: 0 },
  68. })
  69. abort.abort()
  70. await expect(iterator.next()).resolves.toEqual({ done: true, value: undefined })
  71. expect(controller.respond({
  72. interactionId: 'missing' as SessionInteractionId,
  73. result: { ok: false, error: { code: 'cancelled', message: 'cancelled', details: {} } },
  74. })).toEqual({ accepted: false, reason: 'not-pending' })
  75. })
  76. })