preset-migration.snapshot.ts 4.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. /** Cold V2 restoration mounts the shipped PTC preset and publishes only a V3 successor. */
  2. import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
  3. import { dirname, join } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { zstdCompressSync, zstdDecompressSync } from 'node:zlib'
  6. import { describe, expect, it } from 'vitest'
  7. import { SessionId } from '@deepseek-ai/dsh-session'
  8. import { generationLogPath } from '../../../packages/session/session-persistence-jsonl/src/format.ts'
  9. import { scanZstdFrames } from '../../../packages/session/session-persistence-jsonl/src/zstd.ts'
  10. import type {} from '@deepseek-ai/dsh-agent-presets'
  11. import type {} from '@deepseek-ai/dsh-api-session-controller'
  12. import { normalizeSessionSnapshots } from '@deepseek-ai/dsh-session-snapshot'
  13. import { launchWebScaffold, webSnapshotMode } from './scaffold.ts'
  14. const fixturePath = fileURLToPath(new URL('../../../snapshots/web/preset-migration/session.v2.jsonl', import.meta.url))
  15. describe.skipIf(webSnapshotMode() === 'record')('historical preset restoration through the Web Host', () => {
  16. it.each([false, true])('resumes code as PTC (selection events=%s)', async (withSelections) => {
  17. const scaffold = await launchWebScaffold()
  18. try {
  19. const id = SessionId('preset-migration')
  20. const fixture = await readFile(fixturePath, 'utf8')
  21. const [fixtureHeader, ...fixtureEvents] = fixture.trimEnd().split('\n')
  22. .map((line): Record<string, unknown> => JSON.parse(line) as Record<string, unknown>)
  23. const events = withSelections ? fixtureEvents : fixtureEvents.filter(event => event['type'] !== 'agent-preset/selected')
  24. const header = { ...fixtureHeader, id, cwd: scaffold.workspaceCwd }
  25. const rows: Record<string, unknown>[] = events.map((event, seq) => ({ ...event, seq, time: seq + 2 }))
  26. const source = Buffer.concat([header, ...rows].map(row => zstdCompressSync(Buffer.from(JSON.stringify(row) + '\n'))))
  27. const predecessor = generationLogPath(scaffold.persistenceRoot, scaffold.workspaceCwd, id, 2, 'zstd')
  28. const successor = join(dirname(predecessor), 'session.v3.jsonl.zstd')
  29. await mkdir(dirname(predecessor), { recursive: true })
  30. await writeFile(predecessor, source)
  31. const reader = await scaffold.ctx.sessionPersistence.open(id, 'read')
  32. try {
  33. expect(reader.header.agentPreset).toBe('ptc')
  34. await reader.read()
  35. } finally {
  36. await reader.close()
  37. }
  38. await expect(readFile(successor)).rejects.toMatchObject({ code: 'ENOENT' })
  39. const resolved = await scaffold.ctx.sessionController.resolveAgent(id)
  40. if ('error' in resolved) throw resolved.error
  41. expect(scaffold.ctx.agentPresets.composedPreset(resolved.agent.ctx)).toBe('ptc')
  42. expect(resolved.agent.session.header.agentPreset).toBe('ptc')
  43. expect(resolved.agent.session.snapshotEvents()
  44. .filter(event => event.type === 'agent-preset/selected')
  45. .map(event => event.data.agentPreset)).toEqual(withSelections ? ['ptc', 'standard', 'ptc'] : [])
  46. const publishedBytes = await readFile(successor)
  47. const published = Buffer.concat(scanZstdFrames(publishedBytes).frames
  48. .map(({ start, end }) => zstdDecompressSync(publishedBytes.subarray(start, end)))).toString('utf8')
  49. const expected = [
  50. { ...header, version: 3, agentPreset: 'ptc' },
  51. ...rows.map(row => row['type'] === 'agent-preset/selected'
  52. && (row['data'] as { agentPreset: string }).agentPreset === 'code'
  53. ? { ...row, data: { agentPreset: 'ptc' } }
  54. : row),
  55. // Agent activation closes its restored prefix with a fresh seed marker.
  56. { type: 'session/end-seed', seq: rows.length, time: 0, data: {} },
  57. ].map(row => JSON.stringify(row)).join('\n') + '\n'
  58. const context = { sessionIds: [id], cwd: scaffold.workspaceCwd }
  59. expect(normalizeSessionSnapshots([published], context)).toEqual(normalizeSessionSnapshots([expected], context))
  60. expect(await readFile(predecessor)).toEqual(source)
  61. expect((await readdir(dirname(predecessor))).filter(name => name.endsWith('.jsonl.zstd')).sort())
  62. .toEqual(['session.v2.jsonl.zstd', 'session.v3.jsonl.zstd'])
  63. } finally {
  64. await scaffold.close()
  65. }
  66. })
  67. })