vfs-example-fixture.spec.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { readFileSync, readdirSync } from 'node:fs'
  2. import { join, relative } from 'node:path'
  3. import { describe, expect, it } from 'vitest'
  4. import { SESSION_FORMAT_VERSION, Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
  5. import {
  6. generationLogFilename,
  7. scanLog,
  8. } from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
  9. import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
  10. import { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
  11. import {
  12. buildVfsExampleFiles,
  13. VFS_EXAMPLE_OLDEST_MESSAGE,
  14. VFS_EXAMPLE_ROOT,
  15. VFS_EXAMPLE_SESSION_IDS,
  16. VFS_EXAMPLE_TAIL_MESSAGE,
  17. VFS_EXAMPLE_TITLE,
  18. } from './vfs-example-fixture.ts'
  19. function filesUnder(root: string): string[] {
  20. const files: string[] = []
  21. const visit = (directory: string): void => {
  22. for (const entry of readdirSync(directory, { withFileTypes: true })) {
  23. const path = join(directory, entry.name)
  24. if (entry.isDirectory()) visit(path)
  25. else if (entry.isFile()) files.push(relative(root, path).replaceAll('\\', '/'))
  26. }
  27. }
  28. visit(root)
  29. return files.sort()
  30. }
  31. function readSession(id: string): ReturnType<typeof scanLog> {
  32. const path = `home/sessions/--dsh-workspace--/${id}/${generationLogFilename(SESSION_FORMAT_VERSION, 'none')}`
  33. const generated = buildVfsExampleFiles().get(path)
  34. if (generated === undefined) throw new Error(`missing generated VFS example Session ${path}`)
  35. return scanLog(Buffer.from(generated))
  36. }
  37. function textOf(event: SessionEvent): string {
  38. if (event.type === 'user/message') {
  39. return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
  40. }
  41. if (event.type === 'assistant/message') {
  42. return event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
  43. }
  44. return ''
  45. }
  46. describe('WebWorker preview VFS example', () => {
  47. it('matches its deterministic source byte for byte', () => {
  48. const expected = buildVfsExampleFiles()
  49. const predecessors = Object.values(VFS_EXAMPLE_SESSION_IDS)
  50. .map(id => `home/sessions/--dsh-workspace--/${id}/session.v2.jsonl`)
  51. expect(filesUnder(VFS_EXAMPLE_ROOT)).toEqual([...expected.keys(), ...predecessors].sort())
  52. for (const [path, content] of expected) {
  53. expect(readFileSync(join(VFS_EXAMPLE_ROOT, path), 'utf8'), path).toBe(content)
  54. }
  55. })
  56. it('keeps the committed cache on the current generated projection', () => {
  57. const committed = JSON.parse(readFileSync(
  58. join(VFS_EXAMPLE_ROOT, 'home/storages/session_projcache.json'),
  59. 'utf8',
  60. )) as {
  61. unit: { name: string; version: number }
  62. tables: {
  63. sessions: Record<string, {
  64. identity: {
  65. formatVersion: number
  66. createdAt: number
  67. cwd: string
  68. isSeeded: boolean
  69. inheritedEventCount: number
  70. }
  71. rows: { title: unknown }
  72. }>
  73. }
  74. }
  75. const generatedText = buildVfsExampleFiles().get('home/storages/session_projcache.json')
  76. if (generatedText === undefined) throw new Error('missing generated VFS example projection cache')
  77. const generated = JSON.parse(generatedText) as typeof committed
  78. expect(committed).toEqual(generated)
  79. expect(committed.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]?.identity.formatVersion)
  80. .toBe(SESSION_FORMAT_VERSION)
  81. expect(generated.unit).toEqual({
  82. name: projectionCacheDomainSpec.name,
  83. version: projectionCacheDomainSpec.version,
  84. })
  85. expect(generated.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]).toMatchObject({
  86. identity: {
  87. formatVersion: SESSION_FORMAT_VERSION,
  88. createdAt: 1_787_472_000_000,
  89. cwd: '/dsh/workspace',
  90. isSeeded: false,
  91. inheritedEventCount: 0,
  92. },
  93. rows: {
  94. title: {
  95. ver: 1,
  96. val: VFS_EXAMPLE_TITLE,
  97. },
  98. },
  99. })
  100. })
  101. it('restores the main production log with paging and tool coverage', () => {
  102. const { meta, inheritedEventCount, events } = readSession(VFS_EXAMPLE_SESSION_IDS.main)
  103. expect(meta).toMatchObject({
  104. id: VFS_EXAMPLE_SESSION_IDS.main,
  105. cwd: '/dsh/workspace',
  106. delegationDepth: 0,
  107. agentPreset: 'standard',
  108. })
  109. expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
  110. expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
  111. expect(() => Session.fromRestore(
  112. SessionId(meta.id),
  113. events,
  114. meta,
  115. inheritedEventCount,
  116. 'detached',
  117. )).not.toThrow()
  118. const messages = events.filter(event =>
  119. (event.type === 'user/message' || event.type === 'assistant/message') && event.surfaceOp === 'append')
  120. expect(messages.length).toBeGreaterThan(50)
  121. expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_OLDEST_MESSAGE))).toBe(true)
  122. expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_TAIL_MESSAGE))).toBe(true)
  123. expect(events.some(event => event.type === 'session/title'
  124. && (event.data as { title?: unknown }).title === VFS_EXAMPLE_TITLE)).toBe(true)
  125. const tools = events.flatMap(event => event.type === 'tool/call' ? [event.data.name] : [])
  126. expect(new Set(tools)).toEqual(new Set([
  127. 'read', 'write', 'bash', 'glob', 'grep', 'web_search', 'todo_write', 'subagent', 'subagent_fork',
  128. ]))
  129. expect(events.some(event => event.type === 'todo/write')).toBe(true)
  130. expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError === true)).toBe(true)
  131. })
  132. it('restores one-shot and continuable child Sessions with durable descriptors', () => {
  133. const expected = [
  134. [VFS_EXAMPLE_SESSION_IDS.oneShot, 'one-shot'],
  135. [VFS_EXAMPLE_SESSION_IDS.continuable, 'continuable'],
  136. ] as const
  137. for (const [id, mode] of expected) {
  138. const { meta, inheritedEventCount, events } = readSession(id)
  139. expect(meta).toMatchObject({
  140. id,
  141. cwd: '/dsh/workspace',
  142. parentSession: VFS_EXAMPLE_SESSION_IDS.main,
  143. origin: 'subagent',
  144. delegationDepth: 1,
  145. agentPreset: 'standard',
  146. })
  147. expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
  148. expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
  149. expect(foldSubagentDescriptor(events.slice(inheritedEventCount))).toMatchObject({ mode })
  150. expect(() => Session.fromRestore(
  151. SessionId(meta.id),
  152. events,
  153. meta,
  154. inheritedEventCount,
  155. 'detached',
  156. )).not.toThrow()
  157. }
  158. })
  159. })