|
|
@@ -1,141 +1,305 @@
|
|
|
-import { describe, expect, it, vi } from 'vitest'
|
|
|
-import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
|
|
|
-import type { SessionFormatEvent, SessionFormatHeader } from '@deepseek-ai/dsh-session-format'
|
|
|
-import {
|
|
|
- assertReleasedV3Header,
|
|
|
- releasedV2SessionFormatCodec,
|
|
|
- releasedV3SessionFormatCodec,
|
|
|
- restoreReleasedV3Artifact,
|
|
|
- sessionFormatV2ToV3,
|
|
|
-} from '../src/index.ts'
|
|
|
-
|
|
|
-const header: SessionFormatHeader = {
|
|
|
- version: 2, id: 'identity', createdAt: 1, isSeeded: false, delegationDepth: 0,
|
|
|
-}
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+import { createSessionFormatCatalog, SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
|
|
|
+import type { SessionFormatArtifact, SessionFormatEvent, SessionFormatHeader, SessionFormatJsonObject } from '@deepseek-ai/dsh-session-format'
|
|
|
+import { releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, sessionFormatV0ToV1 } from '@deepseek-ai/dsh-session-format-v0-to-v1'
|
|
|
+import { sessionFormatV1ToV2 } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
|
|
+import { assertReleasedV3Header, releasedV2SessionFormatCodec, releasedV3SessionFormatCodec, restoreReleasedV3Artifact, sessionFormatV2ToV3 } from '../src/index.ts'
|
|
|
|
|
|
-describe('v2 to v3 identity migration', () => {
|
|
|
- it('changes only the header version and forwards events and runs without expansion', () => {
|
|
|
- const target = sessionFormatV2ToV3.migrateHeader(header)
|
|
|
- expect(target).toEqual({ ...header, version: 3 })
|
|
|
- expect(header.version).toBe(2)
|
|
|
- const stage = sessionFormatV2ToV3.createStage({
|
|
|
- sourceHeader: header, targetHeader: target, sourceInheritedEventCount: 0, sourceKind: 'decoded',
|
|
|
- })
|
|
|
- const event: SessionFormatEvent = {
|
|
|
- type: 'external/event', seq: 5, time: 12, data: { nested: ['payload'] },
|
|
|
- ignorable: true, sourceEventSeqs: [1, 2], surfaceOp: { replace: [3] },
|
|
|
+const header: SessionFormatHeader = { version: 2, id: 'identity', createdAt: 1, isSeeded: false, delegationDepth: 0 }
|
|
|
+const request = (system?: string) => ({ header: { config: { provider: 'mock', model: 'mock' }, ...(system === undefined ? {} : { system }) }, reason: 'initial' })
|
|
|
+const user = (id = 'user') => ({ role: 'user', id, source: { kind: 'user' }, content: [{ type: 'text', text: id }] })
|
|
|
+const event = (type: string, data: SessionFormatEvent['data'], surfaceOp?: SessionFormatEvent['surfaceOp']): SessionFormatEvent => ({ type, seq: 0, time: 42, data, ...(surfaceOp === undefined ? {} : { surfaceOp }) })
|
|
|
+const dense = (events: readonly SessionFormatEvent[]) => events.map((e, seq) => ({ ...e, seq }))
|
|
|
+const opening = () => [event('turn/start', { turn: 1 }), event('step/start', { turn: 1, step: 1 })]
|
|
|
+function stage(source = header, sourceCut?: number) {
|
|
|
+ const target = sessionFormatV2ToV3.migrateHeader(source)
|
|
|
+ return { target, value: sessionFormatV2ToV3.createStage({ sourceHeader: source, targetHeader: target, sourceInheritedEventCount: sourceCut, sourceKind: 'decoded' }), collector: new SessionFormatEventCollector() }
|
|
|
+}
|
|
|
+function migrate(events: readonly SessionFormatEvent[], source = header, cut: number | undefined = 0): SessionFormatArtifact {
|
|
|
+ const h = stage(source, cut)
|
|
|
+ for (const e of dense(events)) h.value.transformEvent(e, h.collector)
|
|
|
+ const artifact = { header: h.target, inheritedEventCount: h.value.finish(h.collector), events: h.collector.values }
|
|
|
+ return restoreReleasedV3Artifact(artifact, new Set(['feedback/message-put', 'feedback/message-delete']))
|
|
|
+}
|
|
|
+const catalog = createSessionFormatCatalog({
|
|
|
+ currentVersion: 3,
|
|
|
+ codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec, releasedV3SessionFormatCodec],
|
|
|
+ currentEncoder: releasedV3SessionFormatCodec,
|
|
|
+ migrations: [sessionFormatV0ToV1, sessionFormatV1ToV2, sessionFormatV2ToV3],
|
|
|
+ restoreCurrent: artifact => restoreReleasedV3Artifact(artifact, new Set()),
|
|
|
+ restoreTransformedCurrent: artifact => restoreReleasedV3Artifact(artifact, new Set()),
|
|
|
+ restoreCurrentHeader(value) { assertReleasedV3Header(value); return value },
|
|
|
+})
|
|
|
+function requests(events: readonly SessionFormatEvent[], version: 2 | 3) {
|
|
|
+ const surface: SessionFormatEvent[] = []
|
|
|
+ let prompt = ''
|
|
|
+ const result: unknown[] = []
|
|
|
+ for (const e of events) {
|
|
|
+ if (e['surfaceOp'] === 'append') surface.push(e)
|
|
|
+ else if (e['surfaceOp'] !== undefined) {
|
|
|
+ const op = e['surfaceOp'] as { start: number; end: number }
|
|
|
+ const start = surface.findIndex(x => x.seq === op.start)
|
|
|
+ const end = surface.findIndex(x => x.seq === op.end)
|
|
|
+ surface.splice(start, end - start + 1, e)
|
|
|
}
|
|
|
- const run = { runType: 'opaque', firstSeq: 6, eventCount: 2, expand: vi.fn() }
|
|
|
- const context = { emitEvent: vi.fn(), emitRun: vi.fn() }
|
|
|
- stage.transformEvent(event, context)
|
|
|
- stage.transformRun(run, context)
|
|
|
- expect(context.emitEvent.mock.calls[0]?.[0]).toBe(event)
|
|
|
- expect(context.emitRun.mock.calls[0]?.[0]).toBe(run)
|
|
|
- expect(run.expand).not.toHaveBeenCalled()
|
|
|
- expect(stage.finish(context)).toBe(0)
|
|
|
- })
|
|
|
-
|
|
|
- it('derives the inherited cut from scalar end-seed markers', () => {
|
|
|
- const seeded = { ...header, isSeeded: true }
|
|
|
- const stage = sessionFormatV2ToV3.createStage({
|
|
|
- sourceHeader: seeded, targetHeader: { ...seeded, version: 3 },
|
|
|
- sourceInheritedEventCount: undefined, sourceKind: 'transformed',
|
|
|
- })
|
|
|
- const context = new SessionFormatEventCollector()
|
|
|
- for (const data of [null, false, [], {}, { inherited: true }]) {
|
|
|
- stage.transformEvent({ type: 'session/end-seed', seq: 4, time: 1, data }, context)
|
|
|
+ if (e.type === 'request/header') {
|
|
|
+ const h = (e.data as SessionFormatJsonObject)['header'] as SessionFormatJsonObject
|
|
|
+ prompt = typeof h['system'] === 'string' ? h['system'] : ''
|
|
|
+ const messages = surface.flatMap((x) => {
|
|
|
+ const d = x.data as SessionFormatJsonObject
|
|
|
+ const m = (x.type === 'user/message' ? d : d['message']) as SessionFormatJsonObject
|
|
|
+ return x.type === 'system/message' && (m['content'] as unknown[]).length === 0 ? [] : [{ role: m['role'], content: m['content'] }]
|
|
|
+ })
|
|
|
+ if (version === 2 && prompt !== '') messages.unshift({ role: 'system', content: [{ type: 'text', text: prompt }] })
|
|
|
+ result.push(messages)
|
|
|
}
|
|
|
- expect(stage.finish(context)).toBe(4)
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+describe('streaming V2 system prompt migration', () => {
|
|
|
+ it('emits an empty head immediately after first step, preserves chronology, and captures changed and cleared prompts', () => {
|
|
|
+ const input = dense([...opening(), event('user/message', user(), 'append'), event('request/header', request('first')), event('request/header', request('first')), event('request/header', request('changed')), event('request/header', request()), event('request/header', request(''))])
|
|
|
+ const h = stage()
|
|
|
+ h.value.transformEvent(input[0]!, h.collector)
|
|
|
+ expect(h.collector.values).toHaveLength(1)
|
|
|
+ h.value.transformEvent(input[1]!, h.collector)
|
|
|
+ expect(h.collector.values.map(e => e.type)).toEqual(['turn/start', 'step/start', 'system/message'])
|
|
|
+ for (const e of input.slice(2)) h.value.transformEvent(e, h.collector)
|
|
|
+ expect(h.value.finish(h.collector)).toBe(0)
|
|
|
+ const output = migrate(input)
|
|
|
+ expect(output.events.filter(e => e.type === 'system/message')).toHaveLength(4)
|
|
|
+ expect(requests(output.events, 3)).toEqual(requests(input, 2))
|
|
|
+ expect(output.events.filter(e => e.type !== 'system/message').map(e => [e.type, e.time])).toEqual(input.map(e => [e.type, e.time]))
|
|
|
+ expect(output.events.filter(e => e.type === 'request/header').every(e => !Object.hasOwn((e.data as SessionFormatJsonObject)['header'] as SessionFormatJsonObject, 'system'))).toBe(true)
|
|
|
+ expect(migrate(input)).toEqual(output)
|
|
|
+ expect(input[3]!.data).toEqual(request('first'))
|
|
|
})
|
|
|
|
|
|
- it('rejects missing, mismatched, and unseeded inherited cuts', () => {
|
|
|
- const context = new SessionFormatEventCollector()
|
|
|
- for (const [isSeeded, sourceCut, marker] of [[true, undefined, undefined], [true, 2, 1], [false, undefined, 1]] as const) {
|
|
|
- const source = { ...header, isSeeded }
|
|
|
- const stage = sessionFormatV2ToV3.createStage({
|
|
|
- sourceHeader: source, targetHeader: { ...source, version: 3 },
|
|
|
- sourceInheritedEventCount: sourceCut, sourceKind: 'decoded',
|
|
|
- })
|
|
|
- if (marker !== undefined) stage.transformEvent({
|
|
|
- type: 'session/end-seed', seq: marker, time: 1, data: { inherited: true },
|
|
|
- }, context)
|
|
|
- expect(() => stage.finish(context)).toThrow(/inherited/)
|
|
|
+ it('remaps exact local ranges and lists without putting the head inside compaction', () => {
|
|
|
+ const source = dense([...opening(), event('user/message', user('a'), 'append'), event('request/header', request('sys')), event('user/message', user('b'), 'append'), event('compaction/prune', { shadowedRange: { start: 2, end: 4 }, shadowedSeqs: [2, 4], shadowedTokenCount: 20 }), { ...event('user/message', user('replacement'), { op: 'replace', start: 2, end: 4 }), sourceEventSeqs: [2, 4] }, event('command/run', { commandId: 'c', name: 'x', source: { kind: 'user' } }), event('command/done', { commandId: 'c', kind: 'success', sourceEventSeq: 6 }), event('session/title', { title: 'title', messageSeqs: [2, 4], source: { kind: 'fallback' } })])
|
|
|
+ const target = migrate(source)
|
|
|
+ const mapped = target.events.filter(e => e.type !== 'system/message')
|
|
|
+ const prune = mapped[5]!.data as SessionFormatJsonObject
|
|
|
+ expect(prune['shadowedSeqs']).toEqual([mapped[2]!.seq, mapped[4]!.seq])
|
|
|
+ expect(mapped[6]!['sourceEventSeqs']).toEqual(prune['shadowedSeqs'])
|
|
|
+ expect((mapped[8]!.data as SessionFormatJsonObject)['sourceEventSeq']).toBe(mapped[6]!.seq)
|
|
|
+ expect((mapped[9]!.data as SessionFormatJsonObject)['messageSeqs']).toEqual(prune['shadowedSeqs'])
|
|
|
+ expect(() => restoreReleasedV3Artifact({ ...target, events: target.events.map(e => e.type === 'compaction/prune' ? { ...e, data: { ...e.data as SessionFormatJsonObject, shadowedRange: { start: 4, end: 4 }, shadowedSeqs: [4] } } : e) }, new Set())).toThrow(/protected/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps headerless aborted steps and metadata-only logs without inventing a tail request', () => {
|
|
|
+ const empty = migrate([])
|
|
|
+ expect(empty.events).toEqual([])
|
|
|
+ expect(migrate([...opening(), event('user/message', user(), 'append')]).events.at(-1)?.type).toBe('user/message')
|
|
|
+ expect(migrate([event('feedback/record', { text: 'metadata' })]).events).toHaveLength(1)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('detects deterministic head and update identity collisions with source messages in either order', () => {
|
|
|
+ const input = [...opening(), event('user/message', user(), 'append'), event('request/header', request('system'))]
|
|
|
+ const output = migrate(input)
|
|
|
+ const ids = output.events.filter(e => e.type === 'system/message').map(e => ((e.data as SessionFormatJsonObject)['message'] as SessionFormatJsonObject)['id'] as string)
|
|
|
+ for (const id of ids) {
|
|
|
+ const colliding = [...opening(), event('user/message', user(id), 'append'), event('request/header', request('system'))]
|
|
|
+ expect(() => migrate(colliding)).toThrow(/collides/)
|
|
|
}
|
|
|
+ const priorInbox = event('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [user('placeholder')] })
|
|
|
+ const shifted = migrate([priorInbox, ...input])
|
|
|
+ const head = shifted.events.find(e => e.type === 'system/message')!
|
|
|
+ const id = ((head.data as SessionFormatJsonObject)['message'] as SessionFormatJsonObject)['id'] as string
|
|
|
+ expect(() => migrate([event('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [user(id)] }), ...input])).toThrow(/collides/)
|
|
|
})
|
|
|
|
|
|
- it('round-trips v3 records with identical v2 provenance encoding', () => {
|
|
|
- const current = { ...header, version: 3 }
|
|
|
- const event = { type: 'external/event', seq: 3, time: 1, data: null, sourceEventSeqs: [0, 1, 2] }
|
|
|
- const physical = releasedV3SessionFormatCodec.encodeHeader(current, 0)
|
|
|
- expect(physical).toEqual({ ...releasedV2SessionFormatCodec.encodeHeader(header, 0), version: 3 })
|
|
|
- expect(releasedV3SessionFormatCodec.decodeHeader(physical)).toEqual(current)
|
|
|
- const row = releasedV3SessionFormatCodec.encodeEvent(event)
|
|
|
- expect(row).toEqual(releasedV2SessionFormatCodec.encodeEvent(event))
|
|
|
- const decoder = releasedV3SessionFormatCodec.createDecoder(physical, 'strict')
|
|
|
- const context = new SessionFormatEventCollector()
|
|
|
- const first = { type: 'external/event', seq: 0, time: 1, data: null }
|
|
|
- decoder.decodeRow(first, context)
|
|
|
- expect(decoder.header).toEqual(current)
|
|
|
- expect(decoder.finish(context)).toBe(0)
|
|
|
- expect(context.values).toEqual([first])
|
|
|
+ it('refuses pre-step surfaces and changed prompts outside a step rather than reorder source history', () => {
|
|
|
+ expect(() => migrate([event('user/message', user(), 'append'), ...opening()])).toThrow(/before first step/)
|
|
|
+ expect(() => migrate([event('turn/start', { turn: 1 }), event('request/header', request('early')), event('step/start', { turn: 1, step: 1 })])).toThrow(/outside an open step/)
|
|
|
+ expect(() => migrate([...opening(), event('step/end', { turn: 1, step: 1 }), event('request/header', request('late'))])).toThrow(/outside an open step/)
|
|
|
})
|
|
|
|
|
|
- it.each([null, [], false, { version: 2 }])('rejects a non-v3 physical header %j', (value) => {
|
|
|
- expect(() => releasedV3SessionFormatCodec.decodeHeader(value)).toThrow(/format v3 physical/)
|
|
|
+ it.each([undefined, 0, 1, 2])('preserves delivery generation coordinates (%s) and validates ownership before promotion', (version) => {
|
|
|
+ const marker = event('session-log-deepseek/delivery-accepted', { sessionId: header.id, throughSeq: 1, ...(version === undefined ? {} : { sessionFormatVersion: version }) })
|
|
|
+ const target = migrate([...opening(), marker])
|
|
|
+ expect(target.events.at(-1)?.data).toEqual(marker.data)
|
|
|
+ expect(() => migrate([...opening(), { ...marker, data: { ...marker.data as SessionFormatJsonObject, sessionId: 'foreign', sessionFormatVersion: 2 } }])).toThrow(/wrong Session/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('derives unknown seeded cuts after insertion and isolates simultaneous stages', () => {
|
|
|
+ const source = { ...header, isSeeded: true, parentSession: 'parent' }
|
|
|
+ const events = dense([...opening(), event('request/header', request('seed')), event('session-log-deepseek/delivery-accepted', { sessionId: 'parent', throughSeq: 1, sessionFormatVersion: 2 }), event('session/end-seed', { inherited: true })])
|
|
|
+ const a = stage(source, undefined)
|
|
|
+ const b = stage()
|
|
|
+ expect(a.value.headerInheritedEventCount).toBeUndefined()
|
|
|
+ expect(b.value.headerInheritedEventCount).toBe(0)
|
|
|
+ for (const e of events) a.value.transformEvent(e, a.collector)
|
|
|
+ expect(a.value.finish(a.collector)).toBe(6)
|
|
|
+ expect(b.value.finish(b.collector)).toBe(0)
|
|
|
+ expect(migrate(events, source, 4).inheritedEventCount).toBe(6)
|
|
|
+ expect(() => migrate(events, source, 3)).toThrow(/source cut/)
|
|
|
+ expect(() => migrate([], source, undefined)).toThrow(/inherited/)
|
|
|
+ expect(() => migrate([event('session/end-seed', { inherited: true })])).toThrow(/inherited/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('preserves exact TOOL_NOT_STARTED message IDs through coordinate shifts', () => {
|
|
|
+ const callId = 'call-with-dashes'
|
|
|
+ const assistant = event('assistant/message', { turn: 1, step: 1, stream: [], message: { id: 'assistant', role: 'assistant', content: [{ type: 'tool-call', id: callId, name: 'test', arguments: '{}' }], source: { kind: 'model', provider: 'mock', model: 'mock' } } }, 'append')
|
|
|
+ const repair = event('tool/result', { turn: 1, step: 1, error: { name: 'ToolNotStartedError', code: 'TOOL_NOT_STARTED' }, message: { id: 'interrupted-tool-result-' + callId + '-4', role: 'user', source: { kind: 'tool', callId }, content: [{ type: 'tool-result', toolCallId: callId, isError: true, content: [{ type: 'text', text: 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.' }] }] } }, 'append')
|
|
|
+ const input = [...opening(), event('request/header', request('system')), assistant, repair, event('step/end', { turn: 1, step: 1 })]
|
|
|
+ const target = migrate(input)
|
|
|
+ expect(target.events.find(e => e.type === 'tool/result')?.data).toEqual(repair.data)
|
|
|
+ expect(target.events.find(e => e.type === 'tool/result')?.seq).toBe(6)
|
|
|
+ const data = repair.data as SessionFormatJsonObject
|
|
|
+ const historical = { ...repair, data: { ...data, message: { ...data['message'] as SessionFormatJsonObject, id: 'interrupted-tool-result-' + callId + '-999' } } }
|
|
|
+ const inherited = migrate([...input.slice(0, 4), historical, ...input.slice(5)])
|
|
|
+ expect(inherited.events.find(e => e.type === 'tool/result')?.data).toEqual(historical.data)
|
|
|
+ expect(restoreReleasedV3Artifact(inherited, new Set())).toBe(inherited)
|
|
|
+ expect(() => migrate([...input.slice(0, 4), { ...repair, data: { ...data, message: { ...data['message'] as SessionFormatJsonObject, id: 'arbitrary-repair-id' } } }])).toThrow(/canonical historical/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('preserves other-session captures, workflow-local seq, and model input containing source numbers', () => {
|
|
|
+ const reference = { kind: 'session-reference', form: 'recall', version: 1, references: [{ sessionId: 'other', label: 'other', capturedThroughSeq: 99, capturedFormatVersion: 2, compacted: false, originalMessages: 1, retainedMessages: 1, omittedMessages: 0, omittedBytes: 0, truncated: false, inputIndex: 0 }] }
|
|
|
+ const input = [...opening(), event('user/message', { ...user(), source: reference }, 'append'), event('tool-workflow/agent-start', { runId: 'run', seq: 99, label: 'child', childId: 'other' }), event('user/message', user('human'), 'append'), event('session/title-llm-request', { titleProvider: 'mock', messageSeqs: [4], route: { provider: 'mock', model: 'mock' }, system: 'title system', messages: [{ ...user('title'), source: { kind: 'plugin', plugin: 'dsh-session-title-llm' }, content: [{ type: 'text', text: 'source seq=4 (preserved model input)' }] }], maxTokens: 20 })]
|
|
|
+ const target = migrate(input)
|
|
|
+ const kept = target.events.filter(e => e.type !== 'system/message')
|
|
|
+ expect(kept[2]?.data).toEqual(input[2]?.data)
|
|
|
+ expect(kept[3]?.data).toEqual(input[3]?.data)
|
|
|
+ expect((kept[5]!.data as SessionFormatJsonObject)['messageSeqs']).toEqual([5])
|
|
|
+ expect((kept[5]!.data as SessionFormatJsonObject)['messages']).toEqual((input[5]!.data as SessionFormatJsonObject)['messages'])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('expands compact input incrementally through independent stage state', () => {
|
|
|
+ const h = stage()
|
|
|
+ const input = dense([...opening(), event('request/header', request('run'))])
|
|
|
+ h.value.transformRun({ runType: 'test', firstSeq: 0, eventCount: 3, *expand() { yield* input } }, h.collector)
|
|
|
+ expect(h.value.finish(h.collector)).toBe(0)
|
|
|
+ expect(h.collector.values).toEqual(migrate(input).events)
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([0, 1])('derives the inherited cut after V%s assistant chunks collapse upstream', (version) => {
|
|
|
+ const chunks = [
|
|
|
+ event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hello' } }),
|
|
|
+ event('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'finish', reason: { kind: 'stop' } } }),
|
|
|
+ ]
|
|
|
+ const assistant = { ...event('assistant/message', { turn: 1, step: 1, message: { id: 'assistant', role: 'assistant', source: { kind: 'model', provider: 'mock', model: 'mock' }, content: [{ type: 'text', text: 'hello' }] } }, 'append'), sourceEventSeqs: [3, 4] }
|
|
|
+ const source = dense([...opening(), event('request/header', request('seed')), ...chunks, assistant, event('step/end', { turn: 1, step: 1 }), event('turn/end', { turn: 1, reason: { kind: 'completed' } }), event('session/end-seed', {})])
|
|
|
+ const restore = catalog.createRestore({ type: 'session', version, id: header.id, createdAt: 1, delegationDepth: 0, seedLength: 8 }, { recovery: 'strict', validation: 'current' })
|
|
|
+ for (const e of source) restore.decodeRow(e)
|
|
|
+ const output = restore.finish()
|
|
|
+ expect(output.inheritedEventCount).toBe(8)
|
|
|
+ expect(output.events.filter(e => e.type === 'assistant/chunk')).toHaveLength(0)
|
|
|
+ expect(output.events.filter(e => e.type === 'system/message')).toHaveLength(2)
|
|
|
+ expect(output.events.at(-1)?.seq).toBe(output.inheritedEventCount)
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([0, 1, 2])('restores seeded V%s through physical codecs and every adjacent stage', (version) => {
|
|
|
+ const source = dense([...opening(), event('user/message', user(), 'append'), event('request/header', request('seed')), event('step/end', { turn: 1, step: 1 }), event('turn/end', { turn: 1, reason: { kind: 'completed' } }), event('session/end-seed', version === 2 ? { inherited: true } : {})])
|
|
|
+ const physical = version === 2 ? { type: 'session', ...header, version, isSeeded: true } : { type: 'session', version, id: header.id, createdAt: 1, delegationDepth: 0, seedLength: 6 }
|
|
|
+ const restore = catalog.createRestore(physical, { recovery: 'strict', validation: 'current' })
|
|
|
+ for (const e of source) restore.decodeRow(e)
|
|
|
+ const output = restore.finish()
|
|
|
+ expect(output.inheritedEventCount).toBe(8)
|
|
|
+ expect(requests(output.events, 3)).toEqual(requests(source, 2))
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([event('external/opaque', { seq: 1 }), { ...event('external/opaque', {}), ignorable: true }, event('request/header', { ...request(), futureRef: 0 }), event('user/message', { ...user(), source: { kind: 'future', seq: 0 } }, 'append')])('rejects unaudited payloads %j', (bad) => {
|
|
|
+ expect(() => migrate([...opening(), bad])).toThrow(/unclassified|unexpected/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('rejects invalid references, future generation claims, and unclassified message content', () => {
|
|
|
+ expect(() => migrate([...opening(), { ...event('user/message', user(), 'append'), sourceEventSeqs: [99] }])).toThrow(/earlier/)
|
|
|
+ expect(() => migrate([...opening(), event('system/message', {})])).toThrow(/unclassified/)
|
|
|
+ expect(() => migrate([...opening(), event('session-log-deepseek/delivery-accepted', { sessionId: header.id, throughSeq: 1, sessionFormatVersion: 3 })])).toThrow(/format v3|between/)
|
|
|
+ expect(() => migrate([...opening(), event('user/message', { ...user(), content: [{ type: 'future-block', seq: 1 }] }, 'append')])).toThrow(/unclassified message content/)
|
|
|
})
|
|
|
|
|
|
- it.each([false, true])('validates V2 delivery ownership before promotion (inherited=%s)', (inherited) => {
|
|
|
- const source = { ...header, isSeeded: inherited, ...(inherited ? { parentSession: 'parent' } : {}) }
|
|
|
- const stage = sessionFormatV2ToV3.createStage({
|
|
|
- sourceHeader: source, targetHeader: { ...source, version: 3 },
|
|
|
- sourceInheritedEventCount: undefined, sourceKind: 'decoded',
|
|
|
- })
|
|
|
+ it('preserves message feedback identities and rejects unaudited feedback fields', () => {
|
|
|
+ const feedback = event('feedback/message-put', { sessionId: 'other', item: { messageId: 'unchanged', rating: 'positive', version: 'opaque', createdAt: 1, updatedAt: 2 } })
|
|
|
+ expect(migrate([...opening(), feedback]).events.at(-1)?.data).toEqual(feedback.data)
|
|
|
+ const bad = { ...feedback, data: { ...feedback.data as SessionFormatJsonObject, seq: 2 } }
|
|
|
+ expect(() => migrate([...opening(), bad])).toThrow(/unexpected/)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe('native V3 codec and restorer', () => {
|
|
|
+ it('round-trips system messages and empty heads, returning no projected user-message substitutes', () => {
|
|
|
+ const target = migrate([...opening(), event('request/header', request('system'))])
|
|
|
+ const restore = catalog.createRestore(releasedV3SessionFormatCodec.encodeHeader(target.header, 0), { recovery: 'strict', validation: 'current' })
|
|
|
+ for (const e of target.events) restore.decodeRow(releasedV3SessionFormatCodec.encodeEvent(e))
|
|
|
+ const output = restore.finish()
|
|
|
+ expect(output).toEqual(target)
|
|
|
+ expect(restoreReleasedV3Artifact(target, new Set())).toBe(target)
|
|
|
+ expect(target.events.filter(e => e.type === 'user/message')).toHaveLength(0)
|
|
|
+ })
|
|
|
+ it('rejects retired header.system on encode, decode, and logical restore', () => {
|
|
|
+ const bad = { ...event('request/header', request('retired')), seq: 2 }
|
|
|
+ expect(() => releasedV3SessionFormatCodec.encodeEvent(bad)).toThrow(/header.system/)
|
|
|
+ const decoder = releasedV3SessionFormatCodec.createDecoder({ type: 'session', ...header, version: 3 }, 'strict')
|
|
|
const context = new SessionFormatEventCollector()
|
|
|
- stage.transformEvent({ type: 'session-log-deepseek/delivery-accepted', seq: 0, time: 1,
|
|
|
- data: { sessionId: 'parent', sessionFormatVersion: 2 } }, context)
|
|
|
- if (inherited) stage.transformEvent({ type: 'session/end-seed', seq: 1, time: 1, data: { inherited: true } }, context)
|
|
|
- if (inherited) {
|
|
|
- expect(stage.finish(context)).toBe(1)
|
|
|
- stage.transformEvent({ type: 'session-log-deepseek/delivery-accepted', seq: 2, time: 1,
|
|
|
- data: { sessionId: 'parent', sessionFormatVersion: 2 } }, context)
|
|
|
+ for (const e of dense(opening())) decoder.decodeRow(e, context)
|
|
|
+ expect(() => decoder.decodeRow(bad, context)).toThrow(/header.system/)
|
|
|
+ const artifact = { header: { ...header, version: 3 }, inheritedEventCount: 0, events: [...dense(opening()), bad] }
|
|
|
+ expect(() => restoreReleasedV3Artifact(artifact, new Set())).toThrow(/header.system/)
|
|
|
+ })
|
|
|
+ it('rejects malformed native system payloads and foreign payload members', () => {
|
|
|
+ const target = migrate(opening())
|
|
|
+ const system = target.events[2]!
|
|
|
+ const data = system.data as SessionFormatJsonObject
|
|
|
+ const message = data['message'] as SessionFormatJsonObject
|
|
|
+ const invalid = [
|
|
|
+ { ...system, data: { ...data, extra: 0 } },
|
|
|
+ { ...system, data: { ...data, message: { ...message, role: 'user' } } },
|
|
|
+ { ...system, data: { ...data, message: { ...message, source: { kind: 'user' } } } },
|
|
|
+ { ...system, data: { ...data, message: { ...message, content: [{ type: 'text', text: 12 }] } } },
|
|
|
+ { ...system, surfaceOp: { op: 'replace', start: 0, end: 1 }, sourceEventSeqs: [0, 1] },
|
|
|
+ ]
|
|
|
+ for (const bad of invalid) {
|
|
|
+ expect(() => restoreReleasedV3Artifact({ ...target, events: [...target.events.slice(0, 2), bad] }, new Set())).toThrow()
|
|
|
}
|
|
|
- expect(() => stage.finish(context)).toThrow(/wrong Session/)
|
|
|
+ const decoder = releasedV3SessionFormatCodec.createDecoder({ type: 'session', ...header, version: 3 }, 'recoverable')
|
|
|
+ const collector = new SessionFormatEventCollector()
|
|
|
+ for (const e of dense(opening())) decoder.decodeRow(e, collector)
|
|
|
+ decoder.decodeRow(null, collector)
|
|
|
+ expect(() => decoder.decodeRow({ ...event('request/header', request('retired')), seq: 2 }, collector)).toThrow(/header.system/)
|
|
|
})
|
|
|
|
|
|
- it.each([undefined, 0, 1, 2, 4])('preserves delivery marker generation %s verbatim', (version) => {
|
|
|
- const stage = sessionFormatV2ToV3.createStage({
|
|
|
- sourceHeader: header, targetHeader: { ...header, version: 3 },
|
|
|
- sourceInheritedEventCount: 0, sourceKind: 'decoded',
|
|
|
- })
|
|
|
- const event = { type: 'session-log-deepseek/delivery-accepted', seq: 1, time: 2,
|
|
|
- data: { sessionId: header.id, throughSeq: 0, ...(version === undefined ? {} : { sessionFormatVersion: version }) } }
|
|
|
- const context = new SessionFormatEventCollector()
|
|
|
- stage.transformEvent(event, context)
|
|
|
- expect(stage.finish(context)).toBe(0)
|
|
|
- expect(context.values).toEqual([event])
|
|
|
- })
|
|
|
-
|
|
|
- it('checks native v3 delivery ownership without reinterpreting historical markers', () => {
|
|
|
- const artifact = (version: number) => ({
|
|
|
- header: { ...header, version: 3 }, inheritedEventCount: 0, events: [{
|
|
|
- type: 'session-log-deepseek/delivery-accepted', seq: 0, time: 1,
|
|
|
- data: { sessionId: 'other-session', throughSeq: 0, sessionFormatVersion: version },
|
|
|
- }],
|
|
|
- })
|
|
|
- expect(() => restoreReleasedV3Artifact(artifact(3), new Set())).toThrow(/wrong Session/)
|
|
|
- expect(restoreReleasedV3Artifact(artifact(2), new Set()).events).toEqual(artifact(2).events)
|
|
|
- })
|
|
|
-
|
|
|
- it('validates v3 metadata and event admission without mutating the artifact', () => {
|
|
|
- expect(() => { assertReleasedV3Header(header) }).toThrow(/format v3 header/)
|
|
|
- expect(() => { assertReleasedV3Header({ ...header, version: 3, cwd: 'relative' }) }).toThrow(/absolute/)
|
|
|
- const artifact = { header: { ...header, version: 3 }, inheritedEventCount: 0, events: [
|
|
|
- { type: 'external/event', seq: 0, time: 1, data: null, ignorable: true },
|
|
|
- ] }
|
|
|
+ it('rejects system nodes outside the open step and mixed head replacements', () => {
|
|
|
+ const target = migrate([...opening(), event('user/message', user(), 'append'), event('request/header', request('system'))])
|
|
|
+ const systems = target.events.filter(e => e.type === 'system/message')
|
|
|
+ const wrongStep = target.events.map(e => e === systems[0] ? { ...e, data: { ...e.data as SessionFormatJsonObject, step: 2 } } : e)
|
|
|
+ expect(() => restoreReleasedV3Artifact({ ...target, events: wrongStep }, new Set())).toThrow(/open step/)
|
|
|
+ expect(() => restoreReleasedV3Artifact({ ...target, events: target.events.map(e => e === systems[1] ? { ...e, surfaceOp: { op: 'replace', start: 2, end: 3 }, sourceEventSeqs: [2, 3] } : e) }, new Set())).toThrow(/exactly/)
|
|
|
+ })
|
|
|
+ it('round-trips in-history system append, replacement, and compaction without shadowing the head', () => {
|
|
|
+ const base = migrate([...opening(), event('user/message', user(), 'append')])
|
|
|
+ const system = base.events.find(e => e.type === 'system/message')!
|
|
|
+ const message = (system.data as SessionFormatJsonObject)['message'] as SessionFormatJsonObject
|
|
|
+ const append = { ...system, seq: 4, data: { ...system.data as SessionFormatJsonObject, message: { ...message, id: 'tail', source: { kind: 'plugin', plugin: 'context-plugin' }, content: [{ type: 'text', text: 'tail context' }, { type: 'reasoning', text: 'retained content' }] } } }
|
|
|
+ const replace = { ...system, seq: 5, sourceEventSeqs: [4], surfaceOp: { op: 'replace', start: 4, end: 4 } }
|
|
|
+ const prune = { ...event('compaction/prune', { shadowedRange: { start: 5, end: 5 }, shadowedSeqs: [5], shadowedTokenCount: 0 }), seq: 6 }
|
|
|
+ const checkpoint = { ...event('user/message', user('checkpoint'), { op: 'replace', start: 5, end: 5 }), sourceEventSeqs: [5], seq: 7 }
|
|
|
+ const artifact = { ...base, events: [...base.events, append, replace, prune, checkpoint] }
|
|
|
expect(restoreReleasedV3Artifact(artifact, new Set())).toBe(artifact)
|
|
|
- expect(artifact.header.version).toBe(3)
|
|
|
- expect(() => restoreReleasedV3Artifact({ ...artifact, events: [
|
|
|
- { type: 'external/required', seq: 0, time: 1, data: null },
|
|
|
- ] }, new Set())).toThrow(/unknown event type/)
|
|
|
+ const restore = catalog.createRestore(releasedV3SessionFormatCodec.encodeHeader(base.header, 0), { recovery: 'strict', validation: 'current' })
|
|
|
+ for (const e of artifact.events) restore.decodeRow(releasedV3SessionFormatCodec.encodeEvent(e))
|
|
|
+ expect(restore.finish()).toEqual(artifact)
|
|
|
+ const protectedPrune = { ...prune, data: { shadowedRange: { start: 2, end: 2 }, shadowedSeqs: [2], shadowedTokenCount: 0 } }
|
|
|
+ const invalid = { ...base, events: [...base.events, append, replace, protectedPrune] }
|
|
|
+ expect(() => restoreReleasedV3Artifact(invalid, new Set())).toThrow(/protected/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps native ordinary payload and message-source extensions distinct from structural migration admission', () => {
|
|
|
+ const target = migrate([...opening(), event('user/message', user(), 'append')])
|
|
|
+ const events = target.events.map(e => e.type === 'user/message' ? { ...e, data: { ...e.data as SessionFormatJsonObject, installedExtension: true, source: { kind: 'installed-source', revision: 1 } } } : e)
|
|
|
+ const native = { ...target, events }
|
|
|
+ expect(restoreReleasedV3Artifact(native, new Set())).toBe(native)
|
|
|
+ expect(() => migrate([...opening(), event('user/message', { ...user(), installedExtension: true }, 'append')])).toThrow(/unexpected/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('retains equal-generation ignorable events but rejects unknown required events', () => {
|
|
|
+ const artifact = { header: { ...header, version: 3 }, inheritedEventCount: 0, events: [{ ...event('external/event', null), ignorable: true }] }
|
|
|
+ expect(restoreReleasedV3Artifact(artifact, new Set())).toBe(artifact)
|
|
|
+ expect(() => restoreReleasedV3Artifact({ ...artifact, events: [event('external/event', null)] }, new Set())).toThrow(/unknown event/)
|
|
|
+ })
|
|
|
+ it.each([null, [], false, { version: 2 }])('rejects non-v3 physical metadata %j', (value) => {
|
|
|
+ expect(() => releasedV3SessionFormatCodec.decodeHeader(value)).toThrow(/format v3 physical/)
|
|
|
})
|
|
|
})
|