request-log.spec.ts 3.8 KB

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