service-orchestration.spec.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // @vitest-environment jsdom
  2. import { Context } from 'cordis'
  3. import { describe, expect, it, vi } from 'vitest'
  4. import { scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
  5. import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
  6. import { ConversationService } from '@deepseek-ai/dsh-client-ui-conversation/client'
  7. const sid = (id: string) => id as SessionId
  8. const SCOPE_TAG: symbol = (() => {
  9. const reads: (string | symbol)[] = []
  10. const proxy = new Proxy(new Context(), {
  11. get(target, property, receiver): unknown {
  12. reads.push(property)
  13. return Reflect.get(target, property, receiver)
  14. },
  15. })
  16. void scopeOf(proxy)
  17. return reads.find((value): value is symbol => typeof value === 'symbol')!
  18. })()
  19. async function bench(withSessions = true) {
  20. const ctx = new Context()
  21. const prompt = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
  22. const cancel = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
  23. const loadOlder = vi.fn(() => Promise.resolve())
  24. const sessions = {
  25. binding: (sessionId: SessionId) => ({
  26. sessionId, session: { prompt, cancel, loadOlder },
  27. }),
  28. scopeOf,
  29. } as unknown as SessionsService
  30. if (withSessions) ctx.provide('sessions', sessions)
  31. await ctx.plugin(ConversationService).await()
  32. const root = ctx.get('conversation') as ConversationService
  33. const scoped = ctx.plugin(() => {}).ctx.extend({ [SCOPE_TAG]: sid('s1') }).get('conversation') as ConversationService
  34. return { root, scoped, prompt, cancel, loadOlder }
  35. }
  36. describe('ConversationService', () => {
  37. it('routes operations through the public Session binding', async () => {
  38. const b = await bench()
  39. await b.scoped.send('hello', 'steer')
  40. await b.scoped.cancel()
  41. await b.scoped.loadOlder()
  42. expect(b.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'steer')
  43. expect(b.cancel).toHaveBeenCalledOnce()
  44. expect(b.loadOlder).toHaveBeenCalledOnce()
  45. })
  46. it('folds Session business failures into callback rejections', async () => {
  47. const b = await bench()
  48. b.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'busy', details: {} } } as never)
  49. await expect(b.scoped.send('x', 'queue')).rejects.toThrow('conversation.send failed: agent-busy: busy')
  50. b.cancel.mockResolvedValueOnce({ ok: false, error: { code: 'internal', message: 'nope', details: {} } } as never)
  51. await expect(b.scoped.cancel()).rejects.toThrow('conversation.cancel failed: internal: nope')
  52. })
  53. it('fails loudly from the root scope or without SessionsService', async () => {
  54. const b = await bench()
  55. await expect(b.root.send('x', 'queue')).rejects.toThrow(/requires a session scope/)
  56. const missing = await bench(false)
  57. await expect(missing.root.send('x', 'queue')).rejects.toThrow(/sessions service unavailable/)
  58. })
  59. })