request-log.spec.ts 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. /**
  2. * recordRequestHeader unit tests: exactly one of four things per request —
  3. * an 'initial' snapshot (log has no header yet), a 'resume' snapshot (fresh
  4. * loop instance over a log that has one), nothing (header unchanged), a
  5. * round-tripping delta, or a 'fallback' snapshot when the delta encoding
  6. * cannot express the change (pure tool reordering).
  7. */
  8. import { describe, expect, it } from 'vitest'
  9. import { Session, SessionId, canonicalHeader } from '@deepseek-ai/dsh-session'
  10. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  11. import type { ToolSchema } from '@deepseek-ai/dsh-llm'
  12. import { createTransmissionLog, recordRequestHeader } from '../src/request-log.ts'
  13. function tool(name: string, description = 'd'): ToolSchema {
  14. return { name, description, parameters: { type: 'object' } }
  15. }
  16. function openSession(id: string): Session {
  17. const session = new Session(SessionId(id))
  18. session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
  19. return session
  20. }
  21. function headerEvents(session: Session): SessionEvent[] {
  22. return session.events.filter(e => e.type === 'request/header' || e.type === 'request/header-delta')
  23. }
  24. describe('recordRequestHeader', () => {
  25. it("anchors a new conversation with an 'initial' snapshot, then logs nothing while unchanged", () => {
  26. const session = openSession('rl-initial')
  27. const state = createTransmissionLog()
  28. const header = canonicalHeader({ config: { model: 'm' }, system: 's', tools: [tool('t')] })
  29. recordRequestHeader(session, state, header)
  30. const [first] = headerEvents(session)
  31. expect(first?.type === 'request/header' && first.data.reason).toBe('initial')
  32. recordRequestHeader(session, state, header)
  33. expect(headerEvents(session)).toHaveLength(1)
  34. })
  35. it("anchors a fresh loop instance over an anchored log with a 'resume' snapshot, even unchanged", () => {
  36. const session = openSession('rl-resume')
  37. const header = canonicalHeader({ config: { model: 'm' }, system: 's' })
  38. recordRequestHeader(session, createTransmissionLog(), header)
  39. // A second instance (process restart / fork): the boundary itself is a
  40. // recorded fact — snapshot appended even though the header is identical.
  41. recordRequestHeader(session, createTransmissionLog(), header)
  42. const events = headerEvents(session)
  43. expect(events).toHaveLength(2)
  44. expect(events[1]?.type === 'request/header' && events[1].data.reason).toBe('resume')
  45. })
  46. it('logs a round-tripping delta for a mid-run change, and the fold reproduces the header', () => {
  47. const session = openSession('rl-delta')
  48. const state = createTransmissionLog()
  49. const first = canonicalHeader({ config: { model: 'm' }, system: 'a\nb', tools: [tool('t')] })
  50. recordRequestHeader(session, state, first)
  51. const second = canonicalHeader({ config: { model: 'm' }, system: 'a\nc', tools: [tool('t'), tool('u')] })
  52. recordRequestHeader(session, state, second)
  53. const events = headerEvents(session)
  54. expect(events).toHaveLength(2)
  55. expect(events[1]?.type).toBe('request/header-delta')
  56. expect(session.requestHeader()).toEqual(second)
  57. })
  58. it("records a change the delta cannot express (pure reordering) as a 'fallback' snapshot", () => {
  59. const session = openSession('rl-fallback')
  60. const state = createTransmissionLog()
  61. const first = canonicalHeader({ config: { model: 'm' }, tools: [tool('a'), tool('b')] })
  62. recordRequestHeader(session, state, first)
  63. const reordered = canonicalHeader({ config: { model: 'm' }, tools: [tool('b'), tool('a')] })
  64. recordRequestHeader(session, state, reordered)
  65. const events = headerEvents(session)
  66. expect(events).toHaveLength(2)
  67. expect(events[1]?.type === 'request/header' && events[1].data.reason).toBe('fallback')
  68. // The fold still lands on the exact header — deltas are an encoding
  69. // optimization, never a correctness dependency.
  70. expect(session.requestHeader()).toEqual(reordered)
  71. })
  72. })