session-fixture-layout.spec.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { resolve } from 'node:path'
  2. import { describe, expect, it } from 'vitest'
  3. import { createAssistantMessage } from '@deepseek-ai/dsh-llm'
  4. import { SESSION_FORMAT_VERSION, SessionSeq, type SessionEvent } from '@deepseek-ai/dsh-session'
  5. import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
  6. import {
  7. canonicalSessionFixture,
  8. inspectSessionFixtureLayouts,
  9. isPhysicalSessionFixture,
  10. } from './session-fixture-layout.ts'
  11. const HEADER = ` {"type":"session","version":${SESSION_FORMAT_VERSION},"id":"fixture","createdAt":1,"isSeeded":false,"delegationDepth":0} `
  12. const root = resolve(import.meta.dirname, '..')
  13. const FIXTURE_MESSAGE = createAssistantMessage({
  14. content: [{ type: 'text', text: 'part-0part-1part-2part-3' }],
  15. source: { provider: 'mock', model: 'mock' },
  16. })
  17. const FIXTURE_STREAM: SessionEvent<'assistant/message'>['data']['stream'] = [
  18. {
  19. type: 'text-chunks',
  20. time0: 10,
  21. index: 0,
  22. dt: [1, 1, 1],
  23. texts: ['part-0', 'part-1', 'part-2', 'part-3'],
  24. },
  25. { type: 'chunk', time: 14, chunk: { type: 'finish', reason: { kind: 'stop' } } },
  26. ]
  27. function assistantMessage(): SessionEvent<'assistant/message'> {
  28. return {
  29. type: 'assistant/message',
  30. seq: SessionSeq(2),
  31. time: 14,
  32. data: {
  33. turn: 1,
  34. step: 1,
  35. message: FIXTURE_MESSAGE,
  36. stream: FIXTURE_STREAM,
  37. },
  38. surfaceOp: 'append',
  39. }
  40. }
  41. function fixtureEvents(): SessionEvent[] {
  42. return [
  43. { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
  44. { type: 'step/start', seq: SessionSeq(1), time: 2, data: { turn: 1, step: 1 } },
  45. assistantMessage(),
  46. ]
  47. }
  48. function unpackedFixture(): string {
  49. return [HEADER, ...fixtureEvents().map(event => JSON.stringify(event)), ''].join('\n')
  50. }
  51. function decodedBody(content: string): SessionEvent[] {
  52. return parseSessionLog(content)
  53. }
  54. describe('canonicalSessionFixture', () => {
  55. it('preserves the header line and nested compact stream losslessly', () => {
  56. const canonical = canonicalSessionFixture(unpackedFixture(), 'fixture.jsonl')
  57. expect(canonical).toBeDefined()
  58. expect(canonical?.split('\n')[0]).toBe(HEADER)
  59. const message = canonical?.split('\n')
  60. .map(line => JSON.parse(line || '{}') as Record<string, unknown>)
  61. .find(record => record.type === 'assistant/message')
  62. expect(message).toMatchObject({
  63. type: 'assistant/message',
  64. data: {
  65. stream: FIXTURE_STREAM,
  66. },
  67. })
  68. expect(message).not.toHaveProperty('seq')
  69. expect(message).not.toHaveProperty('time')
  70. expect(decodedBody(canonical ?? '').map(({ seq: _seq, time: _time, ...event }) => event))
  71. .toStrictEqual(fixtureEvents().map(({ seq: _seq, time: _time, ...event }) => event))
  72. })
  73. it('ignores JSONL whose first record is not a session header', () => {
  74. expect(canonicalSessionFixture('{"type":"session_event"}\n{"value":1}\n')).toBeUndefined()
  75. })
  76. it('is idempotent for an already packed fixture', () => {
  77. const packed = canonicalSessionFixture(unpackedFixture())
  78. expect(packed).toBeDefined()
  79. expect(canonicalSessionFixture(packed ?? '')).toBe(packed)
  80. })
  81. it('is idempotent for an already projected fixture', () => {
  82. const projected = [
  83. HEADER,
  84. '{"type":"turn/start","data":{"turn":1}}',
  85. '',
  86. ].join('\n')
  87. expect(canonicalSessionFixture(projected)).toBe(projected)
  88. })
  89. it('preserves owner-restored request-header tokens in current projected fixtures', () => {
  90. const projected = [
  91. HEADER,
  92. '{"type":"turn/start","data":{"turn":1}}',
  93. '{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"tools":"{{tools}}"},"reason":"initial"}}',
  94. '',
  95. ].join('\n')
  96. expect(canonicalSessionFixture(projected)).toBe(projected)
  97. expect(decodedBody(projected)[1]).not.toHaveProperty('data.header.tools')
  98. })
  99. it.each([0, 1, 2])('preserves v%i request-header tokens and source bytes after validation', (version) => {
  100. const source = [
  101. JSON.stringify({ type: 'session', version, id: 'fixture', createdAt: 1, delegationDepth: 0, ...version >= 2 ? { isSeeded: false } : {} }),
  102. '{"type":"turn/start","data":{"turn":1}}',
  103. '{"type":"step/start","data":{"turn":1,"step":1}}',
  104. '{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}',
  105. '',
  106. ].join('\n')
  107. expect(canonicalSessionFixture(source)).toBe(source)
  108. const request = decodedBody(source).find(event => event.type === 'request/header')
  109. expect(request).not.toHaveProperty('data.header.tools')
  110. expect(request).not.toHaveProperty('data.header.system')
  111. })
  112. it('keeps genuine empty current tools for semantic replay to reject', () => {
  113. const source = [
  114. HEADER,
  115. '{"type":"turn/start","data":{"turn":1}}',
  116. '{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"tools":[]},"reason":"initial"}}',
  117. '',
  118. ].join('\n')
  119. const canonical = canonicalSessionFixture(source)
  120. expect(canonical).toBe(source)
  121. expect(() => decodedBody(canonical!)).toThrow(/session snapshot line 3:.*empty optional header fields must be omitted/)
  122. })
  123. it.each([0, 1, 2])('preserves physically valid v%i bytes without requiring migration to current', (version) => {
  124. const header = { type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: false } : {}) }
  125. const content = [
  126. JSON.stringify(header),
  127. JSON.stringify({ type: 'user/message', data: { role: 'user', id: 'historical-user', source: { kind: 'user' }, content: [] }, surfaceOp: 'append' }),
  128. '',
  129. ].join('\n')
  130. expect(canonicalSessionFixture(content)).toBe(content)
  131. })
  132. it.each([0, 1, 2])('rejects v%i sequence gaps and invalid provenance ranges with source line diagnostics', (version) => {
  133. const header = JSON.stringify({ type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: false } : {}) })
  134. expect(() => canonicalSessionFixture(`${header}\n{"type":"feedback/record","seq":3,"data":{"text":"gap"}}\n`, 'gap.jsonl'))
  135. .toThrow(/gap\.jsonl: session snapshot line 2:.*seq/)
  136. expect(() => canonicalSessionFixture(`${header}\n{"type":"feedback/record","data":{},"sourceEventSeqs":[[2,0]]}\n`, 'range.jsonl'))
  137. .toThrow(/range\.jsonl: session snapshot line 2:/)
  138. })
  139. it.each([0, 1, 2])('finalizes the v%i source inherited cut', (version) => {
  140. const header = { type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: true } : { seedLength: 1 }) }
  141. expect(() => canonicalSessionFixture(`${JSON.stringify(header)}\n`, 'cut.jsonl'))
  142. .toThrow(/cut\.jsonl: session snapshot line 1:.*(?:inherited|seed)/)
  143. })
  144. it('refuses unsupported generation headers', () => {
  145. const header = { type: 'session', version: 99, id: 'future', createdAt: 1, isSeeded: false, delegationDepth: 0 }
  146. expect(() => canonicalSessionFixture(`${JSON.stringify(header)}\n`, 'future.jsonl'))
  147. .toThrow(/future\.jsonl: session snapshot line 1:.*99/)
  148. })
  149. it('fails loud on malformed records after a session header', () => {
  150. expect(() => canonicalSessionFixture(`${HEADER}\n{not-json}\n`, 'broken.jsonl'))
  151. .toThrow(/broken\.jsonl: session snapshot line 2 contains invalid JSON/)
  152. })
  153. it('labels malformed packed rows with the fixture path and line', () => {
  154. const releasedHeader = '{"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0}'
  155. expect(() => canonicalSessionFixture(`${releasedHeader}\n{"type":"text-chunks"}\n`, 'broken.jsonl'))
  156. .toThrow(/broken\.jsonl: session snapshot line 2: released text-chunks row 0 lacks required member "data"/)
  157. })
  158. })
  159. describe('isPhysicalSessionFixture', () => {
  160. it('recognizes fixtures that preserve physical persistence encoding', () => {
  161. expect(isPhysicalSessionFixture(
  162. 'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
  163. )).toBe(true)
  164. expect(isPhysicalSessionFixture(
  165. 'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.v1.jsonl',
  166. )).toBe(true)
  167. expect(isPhysicalSessionFixture(
  168. 'scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl',
  169. )).toBe(true)
  170. expect(isPhysicalSessionFixture(
  171. 'scripts/snapshots/python-sdk-single-exe/advanced/session.1.v1.jsonl',
  172. )).toBe(true)
  173. expect(isPhysicalSessionFixture(
  174. 'scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl',
  175. )).toBe(true)
  176. expect(isPhysicalSessionFixture(
  177. 'scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl',
  178. )).toBe(true)
  179. expect(isPhysicalSessionFixture(
  180. 'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
  181. )).toBe(false)
  182. expect(isPhysicalSessionFixture(
  183. 'scripts/snapshots/python-sdk-single-exe/advanced/requests.jsonl',
  184. )).toBe(false)
  185. expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
  186. })
  187. })
  188. it('keeps every session-format JSONL fixture projected into canonical event layout', () => {
  189. const nonCanonical = inspectSessionFixtureLayouts(root)
  190. .filter(fixture => fixture.source !== fixture.canonical)
  191. .map(fixture => fixture.path)
  192. expect(
  193. nonCanonical,
  194. 'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.',
  195. ).toEqual([])
  196. })