request-header.spec.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /** Request-header canonicalization, equality, snapshot folding, and format rejection. */
  2. import { describe, expect, it } from 'vitest'
  3. import { Session, SessionId, canonicalHeader, foldRequestHeader, headerEquals } from '@deepseek-ai/dsh-session'
  4. import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session'
  5. import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
  6. import type { ToolSchema } from '@deepseek-ai/dsh-llm'
  7. const CONFIG = { provider: 'mock', model: 'm' }
  8. function tool(name: string, description = 'd'): ToolSchema {
  9. return { name, description, parameters: { type: 'object' } }
  10. }
  11. describe('canonicalHeader', () => {
  12. it('normalizes empty optional fields to absence and preserves populated fields', () => {
  13. expect(canonicalHeader({
  14. config: CONFIG,
  15. adapterDefaults: {},
  16. system: '',
  17. tools: [],
  18. })).toEqual({ config: CONFIG })
  19. const full = canonicalHeader({
  20. config: { ...CONFIG, maxTokens: 256_000 },
  21. adapterDefaults: { maxTokens: true },
  22. system: 's',
  23. tools: [tool('a')],
  24. })
  25. expect(full).toEqual({
  26. config: { ...CONFIG, maxTokens: 256_000 },
  27. adapterDefaults: { maxTokens: true },
  28. system: 's',
  29. tools: [tool('a')],
  30. })
  31. })
  32. })
  33. describe('headerEquals', () => {
  34. const base = canonicalHeader({ config: CONFIG, system: 's', tools: [tool('a')] })
  35. it('compares every canonical field and preserves tool order', () => {
  36. expect(headerEquals(base, structuredClone(base))).toBe(true)
  37. expect(headerEquals(base, { ...base, config: { provider: 'mock', model: 'other' } })).toBe(false)
  38. expect(headerEquals(base, {
  39. ...base,
  40. config: { ...base.config, reasoningEffort: ReasoningEffortId('high') },
  41. })).toBe(false)
  42. expect(headerEquals(
  43. { ...base, config: { ...base.config, maxTokens: 256_000 } },
  44. {
  45. ...base,
  46. config: { ...base.config, maxTokens: 256_000 },
  47. adapterDefaults: { maxTokens: true },
  48. },
  49. )).toBe(false)
  50. expect(headerEquals(base, { ...base, system: 'other' })).toBe(false)
  51. expect(headerEquals(base, { ...base, tools: [] })).toBe(false)
  52. expect(headerEquals(base, { ...base, tools: [tool('a', 'changed')] })).toBe(false)
  53. expect(headerEquals({ config: CONFIG, tools: [tool('a'), tool('b')] }, { config: CONFIG, tools: [tool('b'), tool('a')] })).toBe(false)
  54. })
  55. it('treats absent and empty tool arrays as equivalent canonical absence', () => {
  56. expect(headerEquals({ config: CONFIG }, { config: CONFIG, tools: [] })).toBe(true)
  57. })
  58. })
  59. describe('foldRequestHeader', () => {
  60. it('returns the supplied baseline when no snapshot follows', () => {
  61. const from: EpochHeader = { config: CONFIG, system: 'baseline' }
  62. const unrelated: SessionEvent[] = [
  63. { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
  64. ]
  65. expect(foldRequestHeader(unrelated)).toBeUndefined()
  66. expect(foldRequestHeader(unrelated, from)).toBe(from)
  67. })
  68. it('takes the latest full snapshot and skips unrelated events', () => {
  69. const session = Session.create(SessionId('fold'))
  70. session.append('turn/start', { turn: 1 })
  71. session.append('request/header', { header: { config: CONFIG, system: 'first' }, reason: 'initial' })
  72. session.append('user/message', createUserMessage({
  73. content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
  74. }), { surfaceOp: 'append' })
  75. session.append('request/header', { header: { config: { provider: 'mock', model: 'other' }, tools: [] }, reason: 'change' })
  76. expect(foldRequestHeader(session.events)).toEqual({ config: { provider: 'mock', model: 'other' } })
  77. })
  78. })
  79. describe('legacy request-header format', () => {
  80. it('rejects request/header-delta in seeds and untyped appends', () => {
  81. const legacy = [{
  82. type: 'request/header-delta', seq: 0, time: 1, data: { config: CONFIG },
  83. }] as unknown as SessionEvent[]
  84. expect(() => Session.create(SessionId('legacy'), legacy)).toThrow(/unsupported legacy request\/header-delta/)
  85. const session = Session.create(SessionId('legacy-append-delta'))
  86. const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
  87. expect(() => appendLegacy('request/header-delta', { config: CONFIG }))
  88. .toThrow(/unsupported legacy request\/header-delta/)
  89. expect(session.events).toHaveLength(0)
  90. })
  91. it('rejects the removed fallback reason in seeds and untyped appends', () => {
  92. const legacy = [{
  93. type: 'request/header', seq: 0, time: 1, data: { header: { config: CONFIG }, reason: 'fallback' },
  94. }] as unknown as SessionEvent[]
  95. expect(() => Session.create(SessionId('legacy-seed-reason'), legacy))
  96. .toThrow('unsupported legacy request/header reason "fallback"')
  97. const session = Session.create(SessionId('legacy-append-reason'))
  98. const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
  99. expect(() => appendLegacy('request/header', { header: { config: CONFIG }, reason: 'fallback' }))
  100. .toThrow('unsupported legacy request/header reason "fallback"')
  101. expect(session.events).toHaveLength(0)
  102. })
  103. })
  104. describe('Session.requestContext', () => {
  105. const CAPACITY = { provider: 'mock', model: 'm', contextWindow: 128_000 }
  106. /** A turn-enclosed capacity record; the invariant rejects one outside a turn. */
  107. function seedWith(...records: { provider: string; model: string; contextWindow?: number }[]): SessionEvent[] {
  108. const events: SessionEvent[] = [{
  109. type: 'turn/start', seq: 0, time: 1, data: { turn: 1 },
  110. }]
  111. for (const data of records) {
  112. events.push({ type: 'request/context', seq: events.length, time: 1, data })
  113. }
  114. return events
  115. }
  116. it('reads undefined before any record exists', () => {
  117. expect(Session.create(SessionId('no-capacity')).requestContext()).toBeUndefined()
  118. })
  119. it('folds a seeded log on first read, taking the last record', () => {
  120. // The fold watermark starts at 0 with the seed already in the log, so the
  121. // first read must consume the whole seed rather than skip it.
  122. const session = Session.create(SessionId('seeded-capacity'), seedWith(
  123. CAPACITY,
  124. { ...CAPACITY, model: 'later', contextWindow: 256_000 },
  125. ))
  126. expect(session.requestContext()).toEqual({ provider: 'mock', model: 'later', contextWindow: 256_000 })
  127. })
  128. it('advances incrementally across appends and skips unrelated events', () => {
  129. const session = Session.create(SessionId('incremental-capacity'), seedWith(CAPACITY))
  130. expect(session.requestContext()).toEqual(CAPACITY)
  131. session.append('user/message', createUserMessage({
  132. content: [{ type: 'text', text: 'unrelated' }], source: { kind: 'user' },
  133. }), { surfaceOp: 'append' })
  134. expect(session.requestContext()).toEqual(CAPACITY)
  135. session.append('request/context', { ...CAPACITY, model: 'next', contextWindow: 64_000 })
  136. expect(session.requestContext()).toEqual({ provider: 'mock', model: 'next', contextWindow: 64_000 })
  137. session.append('request/context', { provider: 'mock', model: 'unknown' })
  138. expect(session.requestContext()).toEqual({ provider: 'mock', model: 'unknown' })
  139. })
  140. it('folds a batch appended between two reads', () => {
  141. const session = Session.create(SessionId('batched-capacity'), seedWith(CAPACITY))
  142. expect(session.requestContext()).toEqual(CAPACITY)
  143. session.append('request/context', { ...CAPACITY, contextWindow: 200_000 })
  144. session.append('user/message', createUserMessage({
  145. content: [{ type: 'text', text: 'unrelated' }], source: { kind: 'user' },
  146. }), { surfaceOp: 'append' })
  147. session.append('request/context', { ...CAPACITY, contextWindow: 300_000 })
  148. expect(session.requestContext()?.contextWindow).toBe(300_000)
  149. })
  150. it('exposes a frozen record so a reader cannot desync later comparisons', () => {
  151. const session = Session.create(SessionId('frozen-capacity'), seedWith(CAPACITY))
  152. const held = session.requestContext()
  153. if (held === undefined) throw new Error('expected a folded capacity record')
  154. expect(Object.isFrozen(held)).toBe(true)
  155. expect(() => { (held as { contextWindow?: number }).contextWindow = 1 }).toThrow()
  156. })
  157. })