vfs-example-fixture.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { readFileSync, readdirSync } from 'node:fs'
  2. import { join, relative } from 'node:path'
  3. import { describe, expect, it } from 'vitest'
  4. import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
  5. import { scanLog } from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
  6. import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
  7. import {
  8. buildVfsExampleFiles,
  9. VFS_EXAMPLE_OLDEST_MESSAGE,
  10. VFS_EXAMPLE_ROOT,
  11. VFS_EXAMPLE_SESSION_IDS,
  12. VFS_EXAMPLE_TAIL_MESSAGE,
  13. VFS_EXAMPLE_TITLE,
  14. } from './vfs-example-fixture.ts'
  15. function filesUnder(root: string): string[] {
  16. const files: string[] = []
  17. const visit = (directory: string): void => {
  18. for (const entry of readdirSync(directory, { withFileTypes: true })) {
  19. const path = join(directory, entry.name)
  20. if (entry.isDirectory()) visit(path)
  21. else if (entry.isFile()) files.push(relative(root, path).replaceAll('\\', '/'))
  22. }
  23. }
  24. visit(root)
  25. return files.sort()
  26. }
  27. function readSession(id: string): ReturnType<typeof scanLog> {
  28. return scanLog(readFileSync(
  29. join(VFS_EXAMPLE_ROOT, 'home/sessions/--dsh-workspace--', id, 'session.jsonl'),
  30. ))
  31. }
  32. function textOf(event: SessionEvent): string {
  33. if (event.type === 'user/message') {
  34. return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
  35. }
  36. if (event.type === 'assistant/message') {
  37. return event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
  38. }
  39. return ''
  40. }
  41. describe('WebWorker preview VFS example', () => {
  42. it('matches its deterministic source byte for byte', () => {
  43. const expected = buildVfsExampleFiles()
  44. expect(filesUnder(VFS_EXAMPLE_ROOT)).toEqual([...expected.keys()].sort())
  45. for (const [path, content] of expected) {
  46. expect(readFileSync(join(VFS_EXAMPLE_ROOT, path), 'utf8'), path).toBe(content)
  47. }
  48. })
  49. it('seeds the cold-list title cache against the main log identity', () => {
  50. const cache = JSON.parse(readFileSync(
  51. join(VFS_EXAMPLE_ROOT, 'home/storages/session_projcache.json'),
  52. 'utf8',
  53. )) as {
  54. unit: { name: string; version: number }
  55. tables: { sessions: Record<string, { identity: { createdAt: number; cwd: string }; rows: { title: unknown } }> }
  56. }
  57. expect(cache.unit).toEqual({ name: 'session_projcache', version: 3 })
  58. expect(cache.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]).toMatchObject({
  59. identity: { createdAt: 1_787_472_000_000, cwd: '/dsh/workspace' },
  60. rows: { title: { ver: 1, val: VFS_EXAMPLE_TITLE } },
  61. })
  62. })
  63. it('restores the main production log with paging and tool coverage', () => {
  64. const { meta, events } = readSession(VFS_EXAMPLE_SESSION_IDS.main)
  65. expect(meta).toMatchObject({
  66. id: VFS_EXAMPLE_SESSION_IDS.main,
  67. cwd: '/dsh/workspace',
  68. delegationDepth: 0,
  69. agentPreset: 'standard',
  70. })
  71. expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
  72. expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
  73. expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
  74. const messages = events.filter(event =>
  75. (event.type === 'user/message' || event.type === 'assistant/message') && event.surfaceOp === 'append')
  76. expect(messages.length).toBeGreaterThan(50)
  77. expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_OLDEST_MESSAGE))).toBe(true)
  78. expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_TAIL_MESSAGE))).toBe(true)
  79. expect(events.some(event => event.type === 'session/title'
  80. && (event.data as { title?: unknown }).title === VFS_EXAMPLE_TITLE)).toBe(true)
  81. const tools = events.flatMap(event => event.type === 'tool/call' ? [event.data.name] : [])
  82. expect(new Set(tools)).toEqual(new Set([
  83. 'read', 'write', 'bash', 'glob', 'grep', 'web_search', 'todo_write', 'subagent', 'subagent_fork',
  84. ]))
  85. expect(events.some(event => event.type === 'todo/write')).toBe(true)
  86. expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError === true)).toBe(true)
  87. })
  88. it('restores one-shot and continuable child Sessions with durable descriptors', () => {
  89. const expected = [
  90. [VFS_EXAMPLE_SESSION_IDS.oneShot, 'one-shot'],
  91. [VFS_EXAMPLE_SESSION_IDS.continuable, 'continuable'],
  92. ] as const
  93. for (const [id, mode] of expected) {
  94. const { meta, events } = readSession(id)
  95. expect(meta).toMatchObject({
  96. id,
  97. cwd: '/dsh/workspace',
  98. parentSession: VFS_EXAMPLE_SESSION_IDS.main,
  99. origin: 'subagent',
  100. delegationDepth: 1,
  101. agentPreset: 'standard',
  102. })
  103. expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
  104. expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
  105. expect(foldSubagentDescriptor(events.slice(meta.seedLength ?? 0))).toMatchObject({ mode })
  106. expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
  107. }
  108. })
  109. })