Quellcode durchsuchen

test(session): cover observed migration shapes

Tianyi Cui vor 1 Monat
Ursprung
Commit
1762d63e6c

+ 25 - 0
packages/session/session-persistence-jsonl/tests/fixtures/released-v0-real-shapes.jsonl

@@ -0,0 +1,25 @@
+{"type":"session","version":0,"id":"released-v0-real-shapes","createdAt":1,"cwd":"/work","delegationDepth":0}
+{"type":"turn/start","seq":0,"time":2,"data":{"turn":1}}
+{"type":"user/message","seq":1,"time":3,"data":{"id":"user-a","role":"user","content":[{"type":"text","text":"A"}],"source":{"kind":"user"}},"surfaceOp":"append"}
+{"type":"user/message","seq":2,"time":4,"data":{"id":"user-b","role":"user","content":[{"type":"text","text":"B"}],"source":{"kind":"user"}},"surfaceOp":"append"}
+{"type":"user/message","seq":3,"time":5,"data":{"id":"user-c","role":"user","content":[{"type":"text","text":"C"}],"source":{"kind":"user"}},"surfaceOp":"append"}
+{"type":"user/message","seq":4,"time":6,"data":{"id":"user-d","role":"user","content":[{"type":"text","text":"D"}],"source":{"kind":"user"}},"surfaceOp":"append"}
+{"type":"step/start","seq":5,"time":7,"data":{"turn":1,"step":1}}
+{"type":"request/header","seq":6,"time":8,"data":{"header":{"config":{"provider":"mock","model":"mock"}},"reason":"initial"}}
+{"type":"llm/retry","seq":7,"time":9,"data":{"retryId":"retry-1","turn":1,"step":1,"provider":"mock","mode":"normal","policyKey":"[\"normal\",1,[\"RATE_LIMIT\"],1.5,1.5,0]","retry":1,"maxRetries":1,"delayMs":1.5,"failure":{"message":"retry","code":"RATE_LIMIT","status":429}}}
+{"type":"llm/retry-started","seq":8,"time":10,"data":{"retryId":"retry-1","turn":1,"step":1,"retry":1}}
+{"type":"assistant/chunk","seq":9,"time":11,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":10,"time":12,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"hello"}}}
+{"type":"assistant/chunk","seq":11,"time":13,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"hello"}}}}
+{"type":"assistant/chunk","seq":12,"time":14,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1,"outputTokens":1}}}}
+{"type":"assistant/chunk","seq":13,"time":15,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":14,"time":16,"data":{"turn":1,"step":1,"message":{"id":"assistant-1","role":"assistant","content":[{"type":"text","text":"hello"}],"source":{"kind":"model","provider":"mock","model":"mock"}},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"step/end","seq":15,"time":17,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":16,"time":18,"data":{"id":"checkpoint-1","role":"user","content":[{"type":"text","text":"prior checkpoint"}],"source":{"kind":"plugin","plugin":"test"}},"sourceEventSeqs":[1],"surfaceOp":{"op":"replace","start":1,"end":1}}
+{"type":"compaction/start","seq":17,"time":19,"data":{"compactionId":"compaction-2","turn":1}}
+{"type":"compaction/summary","seq":18,"time":20,"data":{"compactionId":"compaction-2","summary":[{"type":"text","text":"summary"}],"shadowedRange":{"start":16,"end":4},"shadowedSeqs":[16,2,3,4],"shadowedTokenCount":4,"provider":"mock","model":"mock"}}
+{"type":"user/message","seq":19,"time":21,"data":{"id":"checkpoint-2","role":"user","content":[{"type":"text","text":"current checkpoint"}],"source":{"kind":"plugin","plugin":"compact","compactionId":"compaction-2"}},"sourceEventSeqs":[17,18,16,2,3,4],"surfaceOp":{"op":"replace","start":16,"end":4}}
+{"type":"compaction/end","seq":20,"time":22,"data":{"compactionId":"compaction-2","turn":1}}
+{"type":"user/message","seq":21,"time":23,"data":{"id":"late-user","role":"user","content":[{"type":"text","text":"late"}],"source":{"kind":"user"}},"surfaceOp":"append"}
+{"type":"session/title-llm-request","seq":22,"time":24,"data":{"titleProvider":"title-1","messageSeqs":[21],"route":{"provider":"mock","model":"mock"},"system":"title","messages":[{"id":"title-request","role":"user","content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":21,\"text\":\"late\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"}}],"maxTokens":20}}
+{"type":"turn/end","seq":23,"time":25,"data":{"turn":1,"reason":{"kind":"completed"}}}

+ 39 - 0
packages/session/session-persistence-jsonl/tests/jsonl.spec.ts

@@ -623,6 +623,45 @@ describe('JsonlSessionPersistence: immutable format generations', () => {
       .toEqual(['session.jsonl', 'session.v2.jsonl'])
   })
 
+  it('migrates released-v0 retry, repeated-compaction, provenance, and late-title shapes', async () => {
+    const id = SessionId('released-v0-real-shapes')
+    const sourcePath = historicalLogPath(root, '/work', id)
+    const currentPath = rawLogPath(root, '/work', id)
+    const source = await readFile(resolve(
+      'packages/session/session-persistence-jsonl/tests/fixtures/released-v0-real-shapes.jsonl',
+    ))
+    await mkdir(dirname(sourcePath), { recursive: true })
+    await writeFile(sourcePath, source)
+
+    const restored = await readAll(ctx.sessionPersistence, id)
+
+    expect(restored.meta).toMatchObject({ id, version: SESSION_FORMAT_VERSION, cwd: '/work' })
+    expect(restored.events.find(event => event.type === 'llm/retry'))
+      .toMatchObject({ data: { delayMs: 1.5 } })
+    expect(restored.events.find(event => event.type === 'compaction/summary')).toMatchObject({
+      data: {
+        shadowedRange: { start: 11, end: 4 },
+        shadowedSeqs: [11, 2, 3, 4],
+      },
+    })
+    const titleRequest = restored.events.find(event => event.type === 'session/title-llm-request')
+    expect(titleRequest?.data.messageSeqs).toEqual([16])
+    const titleBlock = titleRequest?.data.messages[0]?.content[0]
+    expect(titleBlock).toMatchObject({ type: 'text' })
+    if (titleBlock?.type !== 'text') throw new Error('fixture title request lacks its text block')
+    expect(titleBlock.text).toContain('{"seq":21,"text":"late"}')
+    const currentRows = (await readFile(currentPath, 'utf8')).trimEnd().split('\n')
+      .map(line => JSON.parse(line) as Record<string, unknown>)
+    expect(currentRows.find(row => row['type'] === 'user/message'
+      && (row['data'] as { source?: { plugin?: string } }).source?.plugin === 'compact'))
+      .toMatchObject({
+        seq: 14,
+        sourceEventSeqs: [12, 13, 11, 2, 3, 4],
+        surfaceOp: { op: 'replace', start: 11, end: 4 },
+      })
+    expect(await readFile(sourcePath)).toEqual(source)
+  })
+
   it('publishes v2 beside an unchanged physical v1 source with packed chunk rows', async () => {
     const header = meta('released-v1-read', '/work')
     const sourcePath = generationLogPath(root, header.cwd, header.id, 1, 'none')