Files
deepseek-harness/packages/session/session-format/tests/catalog.spec.ts
T

369 lines
13 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
createSessionFormatCatalog,
SessionFormatUnsupportedMigrationError,
type SessionFormatArtifact,
type SessionFormatCatalogOptions,
type SessionFormatCodec,
type SessionFormatCurrentEncoder,
type SessionFormatEvent,
type SessionFormatEventRun,
type SessionFormatMigration,
type SessionFormatMigrationContext,
type SessionFormatMigrationStageInput,
} from '../src/index.ts'
function codec(version: number, inheritedEventCount = 0): SessionFormatCodec & SessionFormatCurrentEncoder {
return {
version,
decodeHeader(value: unknown) {
return value as SessionFormatArtifact['header']
},
createDecoder(headerValue: unknown) {
return {
header: headerValue as SessionFormatArtifact['header'],
headerInheritedEventCount: inheritedEventCount,
decodeRow(rowValue: unknown, context: SessionFormatMigrationContext) {
context.emitEvent(rowValue as SessionFormatEvent)
},
finish: () => inheritedEventCount,
}
},
encodeHeader(header) {
return header
},
encodeEvent(event) {
return event
},
}
}
function identityMigrationStage(inheritedEventCount: number | undefined) {
return {
...(inheritedEventCount === undefined ? {} : { headerInheritedEventCount: inheritedEventCount }),
transformEvent(
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
) {
context.emitEvent(event)
},
transformRun(
run: SessionFormatEventRun,
context: SessionFormatMigrationContext,
) {
context.emitRun(run)
},
finish: () => inheritedEventCount ?? 0,
}
}
function edge(overrides: Partial<SessionFormatMigration> = {}): SessionFormatMigration {
return {
name: '@test/v0-to-v1',
fromVersion: 0,
toVersion: 1,
migrateHeader: header => ({ ...header, version: 1 }),
createStage: ({ sourceInheritedEventCount }) => identityMigrationStage(sourceInheritedEventCount),
validateTargetHeader: () => {},
...overrides,
}
}
function catalog(
migration: SessionFormatMigration = edge(),
restoreVersion = 1,
sourceCodec: SessionFormatCodec = codec(0),
currentEncoder: SessionFormatCurrentEncoder = codec(1),
restoreCurrentHeader: SessionFormatCatalogOptions['restoreCurrentHeader'] = header => header,
) {
const currentCodec = codec(1)
return createSessionFormatCatalog({
currentVersion: 1,
codecs: [sourceCodec, currentCodec],
currentEncoder,
migrations: [migration],
restoreCurrent: artifact => ({
...artifact,
header: { ...artifact.header, version: restoreVersion },
}),
restoreTransformedCurrent: artifact => ({
...artifact,
header: { ...artifact.header, version: restoreVersion },
}),
restoreCurrentHeader,
})
}
const oldHeader = {
version: 0,
id: 'old',
createdAt: 1,
isSeeded: false,
delegationDepth: 0,
} as const
const event = { type: 'turn/start', seq: 0, time: 2, data: { turn: 1 } } as const
function throwUnknown(value: unknown): never {
throw value
}
describe('Session format catalog', () => {
it('classifies headers and restores physical rows through the compiled chain', () => {
const createStage = vi.fn(({ sourceInheritedEventCount }: SessionFormatMigrationStageInput) =>
identityMigrationStage(sourceInheritedEventCount))
const current = catalog(edge({ createStage }))
expect(current.readHeader(oldHeader)).toEqual({
status: 'migration-required',
storedVersion: 0,
targetVersion: 1,
header: { ...oldHeader, version: 1 },
})
expect(current.readHeader({ version: 2 })).toMatchObject({
status: 'unsupported', storedVersion: 2, targetVersion: 1,
})
expect(current.readHeader({ ...oldHeader, version: 1 })).toMatchObject({
status: 'current', storedVersion: 1, targetVersion: 1,
})
expect(current.readHeader({ version: 'broken' })).toMatchObject({ status: 'malformed', targetVersion: 1 })
const restore = current.createRestore(oldHeader, { recovery: 'strict', validation: 'current' })
restore.decodeRow(event)
expect(restore.finish()).toMatchObject({ header: { version: 1 }, events: [event] })
expect(createStage).toHaveBeenCalledOnce()
})
it('restores current rows without a migration and encodes one record at a time', () => {
const current = catalog()
const header = { ...oldHeader, version: 1 }
const restore = current.createRestore(header, { recovery: 'strict', validation: 'current' })
restore.decodeRow(event)
expect(restore.finish()).toEqual({ header, inheritedEventCount: 0, events: [event] })
expect(current.encodeCurrentHeader(header, 0)).toEqual(header)
expect(current.encodeCurrentEvent(event)).toEqual(event)
expect(() => current.encodeCurrentHeader(oldHeader, 0)).toThrow(/requires Session format v1/)
const physicalOnly = current.createRestore(header, { recovery: 'strict', validation: 'transformed' })
physicalOnly.decodeRow(event)
expect(physicalOnly.finish()).toEqual({ header, inheritedEventCount: 0, events: [event] })
const transformed = current.createRestore(oldHeader, { recovery: 'strict', validation: 'transformed' })
transformed.decodeRow(event)
expect(transformed.finish()).toMatchObject({ header: { version: 1 }, events: [event] })
})
it.each([
new Error('target artifact is incompatible'),
'non-Error target artifact refusal',
])('classifies migrated target validation failures as unsupported', (targetFailure) => {
const current = createSessionFormatCatalog({
currentVersion: 1,
codecs: [codec(0), codec(1)],
currentEncoder: codec(1),
migrations: [edge()],
restoreCurrent: artifact => artifact,
restoreTransformedCurrent: () => { throw targetFailure },
restoreCurrentHeader: header => header,
})
const restore = current.createRestore(oldHeader, { recovery: 'strict', validation: 'transformed' })
restore.decodeRow(event)
try {
restore.finish()
throw new Error('expected transformed target validation to fail')
} catch (error: unknown) {
expect(error).toBeInstanceOf(SessionFormatUnsupportedMigrationError)
expect((error as Error).cause).toBe(targetFailure)
}
})
it('preserves current-validation failures and explicit migration refusals', () => {
const currentFailure = new Error('installed current validation failed')
const current = createSessionFormatCatalog({
currentVersion: 1,
codecs: [codec(0), codec(1)],
currentEncoder: codec(1),
migrations: [edge()],
restoreCurrent: () => { throw currentFailure },
restoreTransformedCurrent: artifact => artifact,
restoreCurrentHeader: header => header,
}).createRestore(oldHeader, { recovery: 'strict', validation: 'current' })
expect(() => current.finish()).toThrow(currentFailure)
const explicit = new SessionFormatUnsupportedMigrationError('explicit target refusal')
const transformed = createSessionFormatCatalog({
currentVersion: 1,
codecs: [codec(0), codec(1)],
currentEncoder: codec(1),
migrations: [edge()],
restoreCurrent: artifact => artifact,
restoreTransformedCurrent: () => { throw explicit },
restoreCurrentHeader: header => header,
}).createRestore(oldHeader, { recovery: 'strict', validation: 'transformed' })
expect(() => transformed.finish()).toThrow(explicit)
})
it('rejects a current encoder that returns a non-current header', () => {
const badEncoder: SessionFormatCurrentEncoder = {
...codec(1),
encodeHeader: header => ({ ...header, version: 0 }),
}
const current = catalog(edge(), 1, codec(0), badEncoder)
expect(() => current.encodeCurrentHeader({ ...oldHeader, version: 1 }, 0))
.toThrow(/non-current header/)
})
it('rejects duplicate, missing, and future codec declarations', () => {
const options = {
currentVersion: 1,
migrations: [edge()],
currentEncoder: codec(1),
restoreCurrent: (value: SessionFormatArtifact) => value,
restoreTransformedCurrent: (value: SessionFormatArtifact) => value,
restoreCurrentHeader: (value: SessionFormatArtifact['header']) => value,
}
expect(() => createSessionFormatCatalog({ ...options, codecs: [codec(0), codec(0), codec(1)] }))
.toThrow(/codec v0 is duplicated/)
expect(() => createSessionFormatCatalog({ ...options, codecs: [codec(0)] })).toThrow(/codec v1 is missing/)
expect(() => createSessionFormatCatalog({ ...options, codecs: [codec(0), codec(1), codec(2)] }))
.toThrow(/codec v2 is newer/)
})
it('preserves unsupported header and migration failures', () => {
const refusingCodec: SessionFormatCodec = {
...codec(0),
decodeHeader: () => { throw new Error('bad header') },
createDecoder: () => { throw new Error('bad body') },
}
const current = catalog(edge(), 1, refusingCodec)
expect(current.readHeader(oldHeader)).toMatchObject({ status: 'malformed', reason: 'bad header' })
expect(() => current.createRestore(oldHeader, { recovery: 'strict', validation: 'current' }))
.toThrow('bad body')
expect(() => current.createRestore({ version: 2 }, { recovery: 'strict', validation: 'current' }))
.toThrow(/newer/)
const nonError = catalog({
...edge(),
}, 1, {
...codec(0),
decodeHeader: () => throwUnknown('non-Error header failure'),
})
expect(nonError.readHeader(oldHeader)).toMatchObject({
status: 'malformed', reason: 'non-Error header failure',
})
})
it('classifies migrated header refusals as unsupported and current corruption as malformed', () => {
const unsupported = catalog(edge({
validateTargetHeader: () => { throw new Error('target header lacks marker') },
}))
expect(unsupported.readHeader(oldHeader)).toMatchObject({
status: 'unsupported',
reason: expect.stringContaining('target header lacks marker') as string,
})
const malformed = catalog(
edge(),
1,
codec(0),
codec(1),
() => { throw new Error('current header is corrupt') },
)
expect(malformed.readHeader({ ...oldHeader, version: 1 })).toMatchObject({
status: 'malformed',
reason: 'current header is corrupt',
})
})
it('enforces decoder, stage, and restorer lifecycle results', () => {
const changedCut = edge({
createStage: () => ({
headerInheritedEventCount: 1,
transformEvent: (candidate, context) => { context.emitEvent(candidate) },
transformRun: (candidate, context) => { context.emitRun(candidate) },
finish: () => 0,
}),
})
const changed = catalog(changedCut, 1, codec(0, 1)).createRestore(
{ ...oldHeader, isSeeded: true },
{ recovery: 'strict', validation: 'current' },
)
expect(() => changed.finish()).toThrow(/changed its predeclared inherited cut/)
const trailing = catalog(edge({
createStage: () => ({
headerInheritedEventCount: 0,
transformEvent: () => {},
transformRun: () => {},
finish: (context) => { context.emitEvent(event); return 0 },
}),
})).createRestore(oldHeader, { recovery: 'strict', validation: 'current' })
expect(trailing.finish().events).toEqual([event])
const wrongVersion = catalog(edge(), 0).createRestore(
oldHeader,
{ recovery: 'strict', validation: 'current' },
)
expect(() => wrongVersion.finish()).toThrow(/returned v0/)
const changedDecoderCut: SessionFormatCodec = {
...codec(0),
createDecoder(headerValue) {
return {
header: headerValue as SessionFormatArtifact['header'],
headerInheritedEventCount: 0,
decodeRow: () => {},
finish: () => 1,
}
},
}
const changedDecoder = catalog(edge(), 1, changedDecoderCut).createRestore(
oldHeader,
{ recovery: 'strict', validation: 'current' },
)
expect(() => changedDecoder.finish()).toThrow(/decoder changed its predeclared inherited cut/)
const deferredCut: SessionFormatCodec = {
...codec(0),
createDecoder(headerValue) {
return {
header: headerValue as SessionFormatArtifact['header'],
decodeRow: () => {},
finish: () => 0,
}
},
}
expect(catalog(edge(), 1, deferredCut).createRestore(
oldHeader,
{ recovery: 'strict', validation: 'current' },
).finish().inheritedEventCount).toBe(0)
})
it('expands an unhandled compact run without an intermediate array', () => {
const run: SessionFormatEventRun = {
runType: 'test-run', firstSeq: 0, eventCount: 1, expand: function* () { yield event },
}
const source: SessionFormatCodec = {
...codec(0),
createDecoder(headerValue) {
return {
header: headerValue as SessionFormatArtifact['header'],
headerInheritedEventCount: 0,
decodeRow(_rowValue, context) { context.emitRun(run) },
finish: () => 0,
}
},
}
const restore = catalog(edge(), 1, source).createRestore(
oldHeader,
{ recovery: 'strict', validation: 'current' },
)
restore.decodeRow({})
expect(restore.finish().events).toEqual([event])
})
})