Represent tool results as tool-role messages and migrate released wrappers without losing outer own JSON fields, call identity, error state, or writer field order. Refuse nested or wrapper-only information that has no lossless current representation before successor publication. Validate current event relationships directly instead of projecting V4 data back into V3. Keep lifecycle, PTC, title/command, compaction, system-field and retired-syntax checks on catalog restoration and ordinary JSONL opens. The Worker JSON equality helper must inspect own keys so it cannot admit PTC arguments that Node rejects. Keep the source role change, required native admission and consumer fixture updates in one complete review layer. Explain shared-type changes in the persistence record and the native reader rationale in its existing note. Use checked branded fixture identities and discriminant narrowing instead of unknown double casts; fixture values and assertions remain intact. This recomposes the original tool-role work by _Kerman with the independently validated migration and admission repairs by Tianyi Cui. Frozen generations and snapshot input version selection are retained. Co-authored-by: Tianyi Cui <[email protected]>
218 lines
9.5 KiB
TypeScript
218 lines
9.5 KiB
TypeScript
import { resolve } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { createAssistantMessage } from '@deepseek-ai/dsh-llm'
|
|
import { SESSION_FORMAT_VERSION, SessionSeq, type SessionEvent } from '@deepseek-ai/dsh-session'
|
|
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
|
import {
|
|
canonicalSessionFixture,
|
|
inspectSessionFixtureLayouts,
|
|
isPhysicalSessionFixture,
|
|
} from './session-fixture-layout.ts'
|
|
|
|
const HEADER = ` {"type":"session","version":${SESSION_FORMAT_VERSION},"id":"fixture","createdAt":1,"isSeeded":false,"delegationDepth":0} `
|
|
const root = resolve(import.meta.dirname, '..')
|
|
const historicalVersions = Array.from({ length: SESSION_FORMAT_VERSION }, (_, version) => version)
|
|
const FIXTURE_MESSAGE = createAssistantMessage({
|
|
content: [{ type: 'text', text: 'part-0part-1part-2part-3' }],
|
|
source: { provider: 'mock', model: 'mock' },
|
|
})
|
|
const FIXTURE_STREAM: SessionEvent<'assistant/message'>['data']['stream'] = [
|
|
{
|
|
type: 'text-chunks',
|
|
time0: 10,
|
|
index: 0,
|
|
dt: [1, 1, 1],
|
|
texts: ['part-0', 'part-1', 'part-2', 'part-3'],
|
|
},
|
|
{ type: 'chunk', time: 14, chunk: { type: 'finish', reason: { kind: 'stop' } } },
|
|
]
|
|
|
|
function assistantMessage(): SessionEvent<'assistant/message'> {
|
|
return {
|
|
type: 'assistant/message',
|
|
seq: SessionSeq(2),
|
|
time: 14,
|
|
data: {
|
|
turn: 1,
|
|
step: 1,
|
|
message: FIXTURE_MESSAGE,
|
|
stream: FIXTURE_STREAM,
|
|
},
|
|
surfaceOp: 'append',
|
|
}
|
|
}
|
|
|
|
function fixtureEvents(): SessionEvent[] {
|
|
return [
|
|
{ type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
|
|
{ type: 'step/start', seq: SessionSeq(1), time: 2, data: { turn: 1, step: 1 } },
|
|
assistantMessage(),
|
|
]
|
|
}
|
|
|
|
function unpackedFixture(): string {
|
|
return [HEADER, ...fixtureEvents().map(event => JSON.stringify(event)), ''].join('\n')
|
|
}
|
|
|
|
function decodedBody(content: string): SessionEvent[] {
|
|
return parseSessionLog(content)
|
|
}
|
|
|
|
describe('canonicalSessionFixture', () => {
|
|
it('preserves the header line and nested compact stream losslessly', () => {
|
|
const canonical = canonicalSessionFixture(unpackedFixture(), 'fixture.jsonl')
|
|
expect(canonical).toBeDefined()
|
|
expect(canonical?.split('\n')[0]).toBe(HEADER)
|
|
const message = canonical?.split('\n')
|
|
.map(line => JSON.parse(line || '{}') as Record<string, unknown>)
|
|
.find(record => record.type === 'assistant/message')
|
|
expect(message).toMatchObject({
|
|
type: 'assistant/message',
|
|
data: {
|
|
stream: FIXTURE_STREAM,
|
|
},
|
|
})
|
|
expect(message).not.toHaveProperty('seq')
|
|
expect(message).not.toHaveProperty('time')
|
|
expect(decodedBody(canonical ?? '').map(({ seq: _seq, time: _time, ...event }) => event))
|
|
.toStrictEqual(fixtureEvents().map(({ seq: _seq, time: _time, ...event }) => event))
|
|
})
|
|
|
|
it('ignores JSONL whose first record is not a session header', () => {
|
|
expect(canonicalSessionFixture('{"type":"session_event"}\n{"value":1}\n')).toBeUndefined()
|
|
})
|
|
|
|
it('is idempotent for an already packed fixture', () => {
|
|
const packed = canonicalSessionFixture(unpackedFixture())
|
|
expect(packed).toBeDefined()
|
|
expect(canonicalSessionFixture(packed ?? '')).toBe(packed)
|
|
})
|
|
|
|
it('is idempotent for an already projected fixture', () => {
|
|
const projected = [
|
|
HEADER,
|
|
'{"type":"turn/start","data":{"turn":1}}',
|
|
'',
|
|
].join('\n')
|
|
expect(canonicalSessionFixture(projected)).toBe(projected)
|
|
})
|
|
|
|
it('preserves owner-restored request-header tokens in current projected fixtures', () => {
|
|
const projected = [
|
|
HEADER,
|
|
'{"type":"turn/start","data":{"turn":1}}',
|
|
'{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"tools":"{{tools}}"},"reason":"initial"}}',
|
|
'',
|
|
].join('\n')
|
|
expect(canonicalSessionFixture(projected)).toBe(projected)
|
|
expect(decodedBody(projected)[1]).not.toHaveProperty('data.header.tools')
|
|
})
|
|
|
|
it.each([0, 1, 2])('preserves v%i request-header tokens and source bytes after validation', (version) => {
|
|
const source = [
|
|
JSON.stringify({ type: 'session', version, id: 'fixture', createdAt: 1, delegationDepth: 0, ...version >= 2 ? { isSeeded: false } : {} }),
|
|
'{"type":"turn/start","data":{"turn":1}}',
|
|
'{"type":"step/start","data":{"turn":1,"step":1}}',
|
|
'{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}',
|
|
'',
|
|
].join('\n')
|
|
expect(canonicalSessionFixture(source)).toBe(source)
|
|
const request = decodedBody(source).find(event => event.type === 'request/header')
|
|
expect(request).not.toHaveProperty('data.header.tools')
|
|
expect(request).not.toHaveProperty('data.header.system')
|
|
})
|
|
|
|
it('keeps genuine empty current tools for semantic replay to reject', () => {
|
|
const source = [
|
|
HEADER,
|
|
'{"type":"turn/start","data":{"turn":1}}',
|
|
'{"type":"request/header","data":{"header":{"config":{"provider":"mock","model":"mock"},"tools":[]},"reason":"initial"}}',
|
|
'',
|
|
].join('\n')
|
|
const canonical = canonicalSessionFixture(source)
|
|
expect(canonical).toBe(source)
|
|
expect(() => decodedBody(canonical!)).toThrow(/session snapshot line 1:.*seed request\/header.*empty tools/)
|
|
})
|
|
|
|
it.each(historicalVersions)('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(historicalVersions)('rejects v%i sequence gaps and invalid source-event 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(historicalVersions)('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/)
|
|
})
|
|
|
|
it('labels malformed packed rows with the fixture path and line', () => {
|
|
const releasedHeader = '{"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0}'
|
|
expect(() => canonicalSessionFixture(`${releasedHeader}\n{"type":"text-chunks"}\n`, 'broken.jsonl'))
|
|
.toThrow(/broken\.jsonl: session snapshot line 2: released text-chunks row 0 lacks required member "data"/)
|
|
})
|
|
})
|
|
|
|
describe('isPhysicalSessionFixture', () => {
|
|
it('recognizes fixtures that preserve physical persistence encoding', () => {
|
|
expect(isPhysicalSessionFixture(
|
|
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.v1.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'scripts/snapshots/python-sdk-single-exe/advanced/session.1.v1.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'scripts/snapshots/python-sdk-single-exe/restart/session.2.jsonl',
|
|
)).toBe(true)
|
|
expect(isPhysicalSessionFixture(
|
|
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
|
|
)).toBe(false)
|
|
expect(isPhysicalSessionFixture(
|
|
'scripts/snapshots/python-sdk-single-exe/advanced/requests.jsonl',
|
|
)).toBe(false)
|
|
expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
|
|
})
|
|
})
|
|
|
|
it('keeps every session-format JSONL fixture projected into canonical event layout', () => {
|
|
const nonCanonical = inspectSessionFixtureLayouts(root)
|
|
.filter(fixture => fixture.source !== fixture.canonical)
|
|
.map(fixture => fixture.path)
|
|
expect(
|
|
nonCanonical,
|
|
'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.',
|
|
).toEqual([])
|
|
})
|