capture.spec.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. /** Whole-file captures and the mutation paths that trigger them. */
  2. import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { captureFile, mutationPath, sameCapture } from '../src/capture.ts'
  6. import { scratchDir } from './support.ts'
  7. const cleanups: Array<() => Promise<unknown>> = []
  8. afterEach(async () => {
  9. for (const cleanup of cleanups.reverse()) await cleanup()
  10. cleanups.length = 0
  11. })
  12. describe('captureFile', () => {
  13. it('stores text once by content, reads no more than the cap, and classifies absent, oversized, binary, and non-regular paths', async () => {
  14. const root = await scratchDir('dsh-capture-', cleanups)
  15. const store = join(root, 'captures')
  16. await writeFile(join(root, 'a.txt'), 'one\ntwo\n')
  17. await writeFile(join(root, 'same.txt'), 'one\ntwo\n')
  18. await writeFile(join(root, 'big.txt'), 'x'.repeat(17))
  19. await writeFile(join(root, 'bin.dat'), Uint8Array.of(65, 0, 66))
  20. await mkdir(join(root, 'dir'))
  21. const a = await captureFile(join(root, 'a.txt'), store, 16)
  22. const same = await captureFile(join(root, 'same.txt'), store, 16)
  23. expect(a).toMatchObject({ kind: 'file', binary: false })
  24. expect(same).toEqual(a)
  25. if (a?.kind !== 'file' || same === undefined) throw new Error('expected a stored copy')
  26. expect(await readFile(a.file, 'utf8')).toBe('one\ntwo\n')
  27. expect(await readdir(store)).toHaveLength(1)
  28. expect(await captureFile(join(root, 'big.txt'), store, 16)).toEqual({ kind: 'oversized' })
  29. expect(await captureFile(join(root, 'missing.txt'), store, 16)).toEqual({ kind: 'absent' })
  30. expect(await captureFile(join(root, 'bin.dat'), store, 16)).toMatchObject({ kind: 'file', binary: true })
  31. expect(await captureFile(join(root, 'dir'), store, 16)).toBeUndefined()
  32. // The cap is inclusive: exactly `maxBytes` is stored, one byte more is not.
  33. await writeFile(join(root, 'edge.txt'), 'y'.repeat(16))
  34. expect(await captureFile(join(root, 'edge.txt'), store, 16)).toMatchObject({ kind: 'file' })
  35. expect(sameCapture({ kind: 'absent' }, { kind: 'absent' })).toBe(true)
  36. expect(sameCapture({ kind: 'absent' }, { kind: 'oversized' })).toBe(false)
  37. expect(sameCapture(a, same)).toBe(true)
  38. expect(sameCapture(a, { kind: 'file', file: 'elsewhere', binary: false })).toBe(false)
  39. // Unread content is never known to match: two oversized sides may differ.
  40. expect(sameCapture({ kind: 'oversized' }, { kind: 'oversized' })).toBe(false)
  41. expect(sameCapture({ kind: 'oversized' }, a)).toBe(false)
  42. })
  43. })
  44. describe('mutationPath', () => {
  45. it('names the path of write, edit, and mutating editor calls', () => {
  46. expect(mutationPath('write', { file_path: 'a.txt', content: 'x' })).toBe('a.txt')
  47. expect(mutationPath('edit', { file_path: 'a.txt', old_string: 'x', new_string: 'y' })).toBe('a.txt')
  48. expect(mutationPath('str_replace_editor', { command: 'create', path: 'b.txt', file_text: 'b' })).toBe('b.txt')
  49. expect(mutationPath('str_replace_editor', { command: 'str_replace', path: 'b.txt', old_str: 'b' })).toBe('b.txt')
  50. expect(mutationPath('str_replace_editor', { command: 'insert', path: 'b.txt', insert_line: 1, new_str: 'i' })).toBe('b.txt')
  51. })
  52. it('yields nothing for reads, unknown tools, and incomplete arguments', () => {
  53. expect(mutationPath('write', undefined)).toBeUndefined()
  54. expect(mutationPath('write', [])).toBeUndefined()
  55. expect(mutationPath('read', { file_path: 'a.txt' })).toBeUndefined()
  56. expect(mutationPath('write', { file_path: ' ', content: 'x' })).toBeUndefined()
  57. expect(mutationPath('write', { file_path: 'a.txt' })).toBeUndefined()
  58. expect(mutationPath('edit', { file_path: 'a.txt', old_string: 'x' })).toBeUndefined()
  59. expect(mutationPath('str_replace_editor', { command: 'view', path: 'b.txt' })).toBeUndefined()
  60. expect(mutationPath('str_replace_editor', { command: 'create', path: '' })).toBeUndefined()
  61. })
  62. })