|
|
@@ -461,9 +461,30 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
expect(() => scanLog(Buffer.from('{"type":"event"}\n'))).toThrow(/session header/)
|
|
|
})
|
|
|
|
|
|
+ it.each([
|
|
|
+ ['missing', undefined],
|
|
|
+ ['a string', '1'],
|
|
|
+ ['fractional', 1.5],
|
|
|
+ ['negative', -1],
|
|
|
+ ])('rejects a session header with %s delegationDepth', (_label, delegationDepth) => {
|
|
|
+ const log = JSON.stringify({
|
|
|
+ type: 'session',
|
|
|
+ version: 0,
|
|
|
+ id: 'invalid-depth',
|
|
|
+ createdAt: 1,
|
|
|
+ ...delegationDepth === undefined ? {} : { delegationDepth },
|
|
|
+ }) + '\n'
|
|
|
+ expect(() => scanLog(Buffer.from(log))).toThrow(/session header/)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('rejects a session header with negative-zero delegationDepth', () => {
|
|
|
+ const log = '{"type":"session","version":0,"id":"invalid-depth","createdAt":1,"delegationDepth":-0}\n'
|
|
|
+ expect(() => scanLog(Buffer.from(log))).toThrow(/session header/)
|
|
|
+ })
|
|
|
+
|
|
|
it('a seq gap after the last turn/end bounds the preserved tail (torn fragment tolerated)', () => {
|
|
|
const log = [
|
|
|
- JSON.stringify({ type: 'session', version: 0, id: 'g', createdAt: 1 }),
|
|
|
+ JSON.stringify({ type: 'session', version: 0, id: 'g', createdAt: 1, delegationDepth: 0 }),
|
|
|
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
|
|
|
JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1
|
|
|
].join('\n') + '\n'
|
|
|
@@ -475,7 +496,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
|
|
|
it('rejects a seq gap BEFORE a later committed turn/end (committed data damaged)', () => {
|
|
|
const log = [
|
|
|
- JSON.stringify({ type: 'session', version: 0, id: 'g2', createdAt: 1 }),
|
|
|
+ JSON.stringify({ type: 'session', version: 0, id: 'g2', createdAt: 1, delegationDepth: 0 }),
|
|
|
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
|
|
|
JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1
|
|
|
JSON.stringify({ type: 'turn/end', seq: 3, time: 3, data: { turn: 1, reason: { kind: 'completed' } } }),
|
|
|
@@ -487,7 +508,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
|
|
|
it('rejects a corrupt line BEFORE a later committed turn/end (committed data damaged)', () => {
|
|
|
const log = [
|
|
|
- JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1 }),
|
|
|
+ JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1, delegationDepth: 0 }),
|
|
|
'{not json', // corrupt, sits in the committed region (a turn/end follows)
|
|
|
JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }),
|
|
|
].join('\n') + '\n'
|
|
|
@@ -495,7 +516,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
})
|
|
|
|
|
|
it('a header-only log (no event lines at all) preserves nothing — committedBytes is the header', () => {
|
|
|
- const log = JSON.stringify({ type: 'session', version: 0, id: 'h0', createdAt: 1 }) + '\n'
|
|
|
+ const log = JSON.stringify({ type: 'session', version: 0, id: 'h0', createdAt: 1, delegationDepth: 0 }) + '\n'
|
|
|
const scanned = scanLog(Buffer.from(log))
|
|
|
expect(scanned.events).toEqual([])
|
|
|
// committedBytes falls back to the header line's end (no preserved events).
|
|
|
@@ -504,7 +525,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
|
|
|
it('a corrupt line after the last turn/end bounds the preserved tail', () => {
|
|
|
const log = [
|
|
|
- JSON.stringify({ type: 'session', version: 0, id: 'c2', createdAt: 1 }),
|
|
|
+ JSON.stringify({ type: 'session', version: 0, id: 'c2', createdAt: 1, delegationDepth: 0 }),
|
|
|
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
|
|
|
'{not json', // corrupt crash fragment, no turn/end committed
|
|
|
].join('\n') + '\n'
|
|
|
@@ -515,7 +536,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
|
|
|
|
|
|
it('tolerates a seq gap AFTER a turn/end (uncommitted tail)', () => {
|
|
|
const log = [
|
|
|
- JSON.stringify({ type: 'session', version: 0, id: 't', createdAt: 1 }),
|
|
|
+ JSON.stringify({ type: 'session', version: 0, id: 't', createdAt: 1, delegationDepth: 0 }),
|
|
|
JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }),
|
|
|
JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }),
|
|
|
JSON.stringify({ type: 'step/start', seq: 9, time: 3, data: { turn: 2, step: 1 } }), // gap in uncommitted tail
|
|
|
@@ -593,7 +614,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
|
|
// `readFirstLine` accumulates chunks before `list()` parses it.
|
|
|
const bucket = join(root, '_no-cwd')
|
|
|
await mkdir(bucket, { recursive: true })
|
|
|
- const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, pad: 'x'.repeat(9000) })
|
|
|
+ const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, delegationDepth: 0, pad: 'x'.repeat(9000) })
|
|
|
await writeFile(join(bucket, 'big.jsonl'), bigHeader + '\n')
|
|
|
const ids = (await ctx.sessionPersistence.list()).map(x => x.id)
|
|
|
expect(ids).toContain('big')
|