request-header.spec.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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({ config: CONFIG, system: '', tools: [] })).toEqual({ config: CONFIG })
  14. const full = canonicalHeader({ config: CONFIG, system: 's', tools: [tool('a')] })
  15. expect(full).toEqual({ config: CONFIG, system: 's', tools: [tool('a')] })
  16. })
  17. })
  18. describe('headerEquals', () => {
  19. const base = canonicalHeader({ config: CONFIG, system: 's', tools: [tool('a')] })
  20. it('compares every canonical field and preserves tool order', () => {
  21. expect(headerEquals(base, structuredClone(base))).toBe(true)
  22. expect(headerEquals(base, { ...base, config: { provider: 'mock', model: 'other' } })).toBe(false)
  23. expect(headerEquals(base, {
  24. ...base,
  25. config: { ...base.config, reasoningEffort: ReasoningEffortId('high') },
  26. })).toBe(false)
  27. expect(headerEquals(base, { ...base, system: 'other' })).toBe(false)
  28. expect(headerEquals(base, { ...base, tools: [] })).toBe(false)
  29. expect(headerEquals(base, { ...base, tools: [tool('a', 'changed')] })).toBe(false)
  30. expect(headerEquals({ config: CONFIG, tools: [tool('a'), tool('b')] }, { config: CONFIG, tools: [tool('b'), tool('a')] })).toBe(false)
  31. })
  32. it('treats absent and empty tool arrays as equivalent canonical absence', () => {
  33. expect(headerEquals({ config: CONFIG }, { config: CONFIG, tools: [] })).toBe(true)
  34. })
  35. })
  36. describe('foldRequestHeader', () => {
  37. it('returns the supplied baseline when no snapshot follows', () => {
  38. const from: EpochHeader = { config: CONFIG, system: 'baseline' }
  39. const unrelated: SessionEvent[] = [
  40. { type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
  41. ]
  42. expect(foldRequestHeader(unrelated)).toBeUndefined()
  43. expect(foldRequestHeader(unrelated, from)).toBe(from)
  44. })
  45. it('takes the latest full snapshot and skips unrelated events', () => {
  46. const session = new Session(SessionId('fold'))
  47. session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
  48. session.append('request/header', { header: { config: CONFIG, system: 'first' }, reason: 'initial' })
  49. session.append('user/message', createUserMessage({
  50. content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
  51. }), { surfaceOp: 'append' })
  52. session.append('request/header', { header: { config: { provider: 'mock', model: 'other' }, tools: [] }, reason: 'change' })
  53. expect(foldRequestHeader(session.events)).toEqual({ config: { provider: 'mock', model: 'other' } })
  54. })
  55. })
  56. describe('legacy request-header format', () => {
  57. it('rejects request/header-delta in seeds and untyped appends', () => {
  58. const legacy = [{
  59. type: 'request/header-delta', seq: 0, time: 1, data: { config: CONFIG },
  60. }] as unknown as SessionEvent[]
  61. expect(() => new Session(SessionId('legacy'), legacy)).toThrow(/unsupported legacy request\/header-delta/)
  62. const session = new Session(SessionId('legacy-append-delta'))
  63. const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
  64. expect(() => appendLegacy('request/header-delta', { config: CONFIG }))
  65. .toThrow(/unsupported legacy request\/header-delta/)
  66. expect(session.events).toHaveLength(0)
  67. })
  68. it('rejects the removed fallback reason in seeds and untyped appends', () => {
  69. const legacy = [{
  70. type: 'request/header', seq: 0, time: 1, data: { header: { config: CONFIG }, reason: 'fallback' },
  71. }] as unknown as SessionEvent[]
  72. expect(() => new Session(SessionId('legacy-seed-reason'), legacy))
  73. .toThrow('unsupported legacy request/header reason "fallback"')
  74. const session = new Session(SessionId('legacy-append-reason'))
  75. const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
  76. expect(() => appendLegacy('request/header', { header: { config: CONFIG }, reason: 'fallback' }))
  77. .toThrow('unsupported legacy request/header reason "fallback"')
  78. expect(session.events).toHaveLength(0)
  79. })
  80. })