فهرست منبع

fix(scripts): validate historical fixture layout with source codecs

Tianyi Cui 6 روز پیش
والد
کامیت
a4dcff874e
2فایلهای تغییر یافته به همراه49 افزوده شده و 57 حذف شده
  1. 30 0
      scripts/session-fixture-layout.spec.ts
  2. 19 57
      scripts/session-fixture-layout.ts

+ 30 - 0
scripts/session-fixture-layout.spec.ts

@@ -106,6 +106,36 @@ describe('canonicalSessionFixture', () => {
     expect(canonicalSessionFixture(projected)).toBe(projected)
   })
 
+  it.each([0, 1, 2])('preserves physically valid v%i bytes without requiring migration to current', (version) => {
+    const header = { type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: false } : {}) }
+    const content = [
+      JSON.stringify(header),
+      JSON.stringify({ type: 'user/message', data: { role: 'user', id: 'historical-user', source: { kind: 'user' }, content: [] }, surfaceOp: 'append' }),
+      '',
+    ].join('\n')
+    expect(canonicalSessionFixture(content)).toBe(content)
+  })
+
+  it.each([0, 1, 2])('rejects v%i sequence gaps and invalid provenance ranges with source line diagnostics', (version) => {
+    const header = JSON.stringify({ type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: false } : {}) })
+    expect(() => canonicalSessionFixture(`${header}\n{"type":"feedback/record","seq":3,"data":{"text":"gap"}}\n`, 'gap.jsonl'))
+      .toThrow(/gap\.jsonl: session snapshot line 2:.*seq/)
+    expect(() => canonicalSessionFixture(`${header}\n{"type":"feedback/record","data":{},"sourceEventSeqs":[[2,0]]}\n`, 'range.jsonl'))
+      .toThrow(/range\.jsonl: session snapshot line 2:/)
+  })
+
+  it.each([0, 1, 2])('finalizes the v%i source inherited cut', (version) => {
+    const header = { type: 'session', version, id: 'historical', createdAt: 1, delegationDepth: 0, ...(version === 2 ? { isSeeded: true } : { seedLength: 1 }) }
+    expect(() => canonicalSessionFixture(`${JSON.stringify(header)}\n`, 'cut.jsonl'))
+      .toThrow(/cut\.jsonl: session snapshot line 1:.*(?:inherited|seed)/)
+  })
+
+  it('refuses unsupported generation headers', () => {
+    const header = { type: 'session', version: 99, id: 'future', createdAt: 1, isSeeded: false, delegationDepth: 0 }
+    expect(() => canonicalSessionFixture(`${JSON.stringify(header)}\n`, 'future.jsonl'))
+      .toThrow(/future\.jsonl: session snapshot line 1:.*99/)
+  })
+
   it('fails loud on malformed records after a session header', () => {
     expect(() => canonicalSessionFixture(`${HEADER}\n{not-json}\n`, 'broken.jsonl'))
       .toThrow(/broken\.jsonl: session snapshot line 2 contains invalid JSON/)

+ 19 - 57
scripts/session-fixture-layout.ts

@@ -11,6 +11,13 @@ import {
 } from '@deepseek-ai/dsh-session'
 import type { SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session'
 import { sessionFormatCatalog } from '@deepseek-ai/dsh-session-format-catalog'
+import { SessionFormatEventCollector, type SessionFormatArtifactDecoder, type SessionFormatCodec } from '@deepseek-ai/dsh-session-format'
+import { releasedV0SessionFormatCodec, releasedV1SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v0-to-v1'
+import { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2'
+
+const historicalCodecs: readonly SessionFormatCodec[] = [
+  releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec,
+]
 
 /** Physical persistence artifacts validated by the WebWorker runtime fixture spec. */
 const WEBWORKER_PHYSICAL_SESSION_FIXTURE_ROOT =
@@ -58,51 +65,6 @@ function validationHeader(value: unknown): unknown {
   return header
 }
 
-function validationRow(source: Readonly<Record<string, unknown>>): Record<string, unknown> {
-  if (source.type !== 'request/header') return { ...source }
-  const data = source.data
-  if (data === null || typeof data !== 'object' || Array.isArray(data)) return { ...source }
-  const header = (data as Record<string, unknown>).header
-  if (header === null || typeof header !== 'object' || Array.isArray(header)) return { ...source }
-  if ((header as Record<string, unknown>).tools !== '{{tools}}') return { ...source }
-  return {
-    ...source,
-    data: {
-      ...data,
-      header: { ...header, tools: [] },
-    },
-  }
-}
-
-function restoreRequestHeaderTokens(
-  events: readonly SessionEvent[],
-  rows: readonly Readonly<Record<string, unknown>>[],
-): SessionEvent[] {
-  const sources = rows.filter(row => row.type === 'request/header')
-  let sourceIndex = 0
-  return events.map((event) => {
-    if (event.type !== 'request/header') return event
-    const source = sources[sourceIndex]
-    sourceIndex += 1
-    const sourceData = source?.data
-    const sourceHeader = sourceData !== null && typeof sourceData === 'object' && !Array.isArray(sourceData)
-      ? (sourceData as Record<string, unknown>).header
-      : undefined
-    if (sourceHeader === null || typeof sourceHeader !== 'object' || Array.isArray(sourceHeader)
-      || (sourceHeader as Record<string, unknown>).tools !== '{{tools}}') return event
-    return {
-      ...event,
-      data: {
-        ...event.data,
-        header: {
-          ...(event.data as unknown as { header: Record<string, unknown> }).header,
-          tools: '{{tools}}',
-        },
-      },
-    } as SessionEvent
-  })
-}
-
 function renderFixture(headerLine: string, events: readonly SessionEvent[]): string {
   return [
     headerLine,
@@ -191,29 +153,28 @@ function parseFixtureRows(content: string, headerValue: unknown): SessionEvent[]
       }
     })
   }
-  let restore: ReturnType<typeof sessionFormatCatalog.createRestore>
+  const collector = new SessionFormatEventCollector()
+  let decoder: SessionFormatArtifactDecoder
   try {
-    restore = sessionFormatCatalog.createRestore(validationHeader(headerValue), {
-      recovery: 'strict',
-      validation: 'current',
-    })
+    const version = (headerValue as Record<string, unknown>).version
+    const codec = historicalCodecs.find(candidate => candidate.version === version)
+    if (codec === undefined) throw new Error(`unsupported session fixture version ${String(version)}`)
+    decoder = codec.createDecoder(validationHeader(headerValue), 'strict')
   } catch (error) {
     const detail = error instanceof Error ? error.message : String(error)
     throw new Error(`session snapshot line 1: ${detail}`, { cause: error })
   }
   for (const [index, row] of rows.entries()) {
     try {
-      restore.decodeRow(validationRow(row))
+      decoder.decodeRow(row, collector)
     } catch (error) {
       const detail = error instanceof Error ? error.message : String(error)
       throw new Error(`session snapshot line ${rowLines[index] ?? 1}: ${detail}`, { cause: error })
     }
   }
   try {
-    return restoreRequestHeaderTokens(
-      [...restore.finish().events] as unknown as SessionEvent[],
-      rows,
-    )
+    decoder.finish(collector)
+    return [...collector.values] as unknown as SessionEvent[]
   } catch (error) {
     const detail = error instanceof Error ? error.message : String(error)
     const line = fixtureDiagnosticLine(error, rowLines, eventLines)
@@ -246,8 +207,9 @@ function withoutEnvelope(events: readonly SessionEvent[]): Array<Omit<SessionEve
 
 /**
  * Canonicalize one JSONL document when its first record is a session header.
- * The header line remains byte-identical; body records decode to logical events,
- * re-encode one event per row, and omit storage sequence/time envelopes.
+ * Historical generations receive strict source-codec validation and remain byte-identical;
+ * conversion to the current format is not required. Current body records re-encode
+ * one event per row and omit storage sequence/time envelopes; their header is preserved.
  * Non-session JSONL returns undefined.
  *
  * @param content - JSONL source text.