semantic-checkpoint.snapshot.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { readFile, writeFile } from 'node:fs/promises'
  2. import { dirname, join } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { Context } from 'cordis'
  5. import { normalizeSessionLog, scrubRequestHeaders, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
  6. import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
  7. import { CallId } from '@deepseek-ai/dsh-llm'
  8. import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
  9. import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
  10. import { describe, expect, it } from 'vitest'
  11. const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'semantic-checkpoint-snapshots/tool-outcome-unknown')
  12. const replayFixture = join(fixtureDir, 'replay.jsonl')
  13. const replayOverride = join(fixtureDir, 'replay.override.json')
  14. const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
  15. const configPath = fileURLToPath(new URL('../semantic-checkpoint.cordis.snapshot.yml', import.meta.url))
  16. const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
  17. const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
  18. const sessionId = SessionId('semantic-checkpoint-unknown-outcome')
  19. const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
  20. const task = 'Continue safely from the interrupted operation.'
  21. async function seedInterruptedSession(root: string, cwd: string): Promise<string> {
  22. const ctx = new Context()
  23. await ctx.plugin(SessionStore)
  24. await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
  25. const meta: SessionHeader = {
  26. version: SESSION_FORMAT_VERSION,
  27. id: sessionId,
  28. createdAt: 1,
  29. cwd,
  30. delegationDepth: 0,
  31. }
  32. const events: SessionEvent[] = [
  33. { type: 'turn/start', seq: 0, time: 10, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
  34. { type: 'user/message', seq: 1, time: 11, data: { content: [{ type: 'text', text: 'Perform one side-effecting remote mutation.' }], source: { kind: 'user' } }, surfaceOp: 'append' },
  35. { type: 'step/start', seq: 2, time: 12, data: { turn: 1, step: 1 } },
  36. {
  37. type: 'assistant/message',
  38. seq: 3,
  39. time: 13,
  40. data: {
  41. turn: 1,
  42. step: 1,
  43. content: [{ type: 'tool-call', id: CallId('unknown-outcome-call'), name: 'write_remote', arguments: '{"value":1}' }],
  44. provenance: { provider: 'deepseek', model: 'deepseek-v4-flash' },
  45. },
  46. surfaceOp: 'append',
  47. },
  48. {
  49. type: 'tool/call',
  50. seq: 4,
  51. time: 14,
  52. data: {
  53. turn: 1,
  54. step: 1,
  55. callId: CallId('unknown-outcome-call'),
  56. name: 'write_remote',
  57. arguments: '{"value":1}',
  58. },
  59. },
  60. ]
  61. try {
  62. await ctx.sessionPersistence.create(meta)
  63. await ctx.sessionPersistence.append(sessionId, events)
  64. const location = ctx.sessionPersistence.locate(meta)
  65. if (location === undefined) throw new Error('JSONL backend did not locate the seeded session')
  66. return location.path
  67. } finally {
  68. await ctx.fiber.dispose()
  69. }
  70. }
  71. describe('semantic checkpoint recovery snapshot', () => {
  72. it('resumes an unknown tool outcome through the headless stream-json app', async () => {
  73. let cwd = ''
  74. let sessionPath = ''
  75. const result = await runLoaderSmoke({
  76. label: 'semantic checkpoint headless stream-json snapshot',
  77. tempDirPrefix: 'dsh-semantic-snapshot-',
  78. binScript,
  79. configPath,
  80. binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
  81. tsconfigPath,
  82. env: {
  83. DSH_SNAPSHOT_FILE: replayFixture,
  84. DSH_SNAPSHOT_OVERRIDE: replayOverride,
  85. },
  86. prepare: async (runCwd) => {
  87. cwd = runCwd
  88. sessionPath = await seedInterruptedSession(join(runCwd, '.sessions'), runCwd)
  89. },
  90. inspect: async () => {
  91. const normalization: NormalizeContext = { sessionIds: [sessionId], cwd }
  92. const session = scrubRequestHeaders(normalizeSessionLog(await readFile(sessionPath, 'utf8'), normalization))
  93. if (refreshing) await writeFile(sessionExpected, session)
  94. expect(session).toBe(await readFile(sessionExpected, 'utf8'))
  95. expect(session).toContain('TOOL_OUTCOME_UNKNOWN')
  96. expect(session).toContain('Do not retry blindly.')
  97. },
  98. })
  99. expect(result.stderr).toBe('')
  100. const records = result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
  101. expect(records.at(-1)).toMatchObject({
  102. type: 'result',
  103. success: true,
  104. sessionId,
  105. result: 'I will verify the external state before deciding whether to retry the side-effecting operation.',
  106. reason: { kind: 'completed' },
  107. })
  108. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  109. })