queue-store.client.spec.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /** Inbox projection delivery and queue-operation transport. */
  2. import { describe, expect, it } from 'vitest'
  3. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  4. import type { InboxState } from '@deepseek-ai/dsh-agent/types'
  5. import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
  6. import type { SessionControlFrame } from '@deepseek-ai/dsh-api-session-controller/types'
  7. import type { SessionId } from '@deepseek-ai/dsh-session/types'
  8. import { SessionManager } from '../src/client/sessions/manager.ts'
  9. import { FakeApiClient, fakeRemote } from './fake-api.client.ts'
  10. const SID = 'fk-q1' as SessionId
  11. const text = (value: string): ContentBlock[] => [{ type: 'text', text: value }]
  12. let nextSeq = 1
  13. function message(label: string, body: string) {
  14. return createUserMessage({
  15. content: text(body),
  16. source: { kind: 'user', rpcId: `rpc-${label}` } as never,
  17. })
  18. }
  19. function inboxFrame(value: InboxState): Extract<SessionControlFrame, { type: 'projection' }> {
  20. return {
  21. type: 'projection',
  22. sessionId: SID,
  23. key: 'inbox',
  24. seq: nextSeq++,
  25. value: value as never,
  26. }
  27. }
  28. describe('Inbox projection intake', () => {
  29. it('stores the complete Agent-owned value without adding queue state to the Session snapshot', () => {
  30. const manager = new SessionManager(fakeRemote(new FakeApiClient()))
  31. const queued = message('queued', 'later')
  32. const steering = message('steering', 'now')
  33. const value = { 'next-turn': [queued], 'next-step': [steering] }
  34. manager.handleControlFrame(inboxFrame(value))
  35. const session = manager.get(SID)
  36. expect(session.projections.faceOf('inbox').getSnapshot()).toEqual(value)
  37. expect(session.getSnapshot()).not.toHaveProperty('queue')
  38. })
  39. it('drops a projection that claims events beyond a reconnect baseline', () => {
  40. const manager = new SessionManager(fakeRemote(new FakeApiClient()))
  41. manager.handleControlFrame(inboxFrame({
  42. 'next-turn': [message('stale', 'stale')],
  43. 'next-step': [],
  44. }))
  45. manager.handleControlFrame({
  46. type: 'baseline',
  47. value: {
  48. jobs: {},
  49. projections: { [SID]: { asOfSeq: 0, values: {} } },
  50. },
  51. })
  52. expect(manager.get(SID).projections.faceOf('inbox').getSnapshot()).toBeUndefined()
  53. })
  54. it('retains only the highest-seq value received before Session materialization', () => {
  55. const manager = new SessionManager(fakeRemote(new FakeApiClient()))
  56. manager.handleControlFrame(inboxFrame({
  57. 'next-turn': [message('old', 'old')],
  58. 'next-step': [],
  59. }))
  60. const latest = {
  61. 'next-turn': [message('latest', 'latest')],
  62. 'next-step': [],
  63. }
  64. manager.handleControlFrame(inboxFrame(latest))
  65. expect(manager.get(SID).projections.faceOf('inbox').getSnapshot()).toEqual(latest)
  66. })
  67. })
  68. describe('queue operation transport', () => {
  69. it('does not mutate the Inbox projection before the Host publishes its committed value', async () => {
  70. const api = new FakeApiClient()
  71. const manager = new SessionManager(fakeRemote(api))
  72. const pending = message('pending', 'before')
  73. const initial = { 'next-turn': [pending], 'next-step': [] }
  74. manager.handleControlFrame(inboxFrame(initial))
  75. const session = manager.get(SID)
  76. await expect(session.updateQueue(pending.id, { kind: 'edit', content: text('after') }))
  77. .resolves.toEqual({ ok: true, value: { accepted: true } })
  78. expect(api.callsOf('session.updateQueue')).toEqual([{
  79. sessionId: SID,
  80. itemId: pending.id,
  81. action: { kind: 'edit', content: text('after') },
  82. }])
  83. expect(session.projections.faceOf('inbox').getSnapshot()).toBe(initial)
  84. })
  85. })