shell-activity-files.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. /** Private activity records reject stale input evidence and incomplete observations. */
  2. import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { dirname, join } from 'node:path'
  5. import { afterEach, expect, it, vi } from 'vitest'
  6. import type { SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess'
  7. import { prepareShellActivity, ShellActivity } from '../src/shell-activity.ts'
  8. vi.mock('node:fs', async (original) => {
  9. const fs = await original<typeof import('node:fs')>()
  10. return { ...fs, writeFileSync: vi.fn(fs.writeFileSync) }
  11. })
  12. const cleanups: Array<() => void> = []
  13. afterEach(() => { for (const cleanup of cleanups.splice(0).reverse()) cleanup(); vi.mocked(writeFileSync).mockReset() })
  14. const spec: SubprocessTerminalSpawnSpec = { argv: ['/bin/zsh', '-i'], cwd: '/', rows: 24, cols: 80, terminalType: 'xterm-256color', graceMs: 100, shellActivity: true }
  15. it('accepts only fresh complete records from the original shell and advances revisions on input', () => {
  16. const directory = mkdtempSync(join(tmpdir(), 'dsh-activity-record-'))
  17. cleanups.push(() => { rmSync(directory, { recursive: true, force: true }) })
  18. const activity = new ShellActivity(directory, [], {})
  19. expect(activity.inspect(123).state).toBe('unknown')
  20. const state = join(directory, 'state')
  21. writeFileSync(state, '123:1:idle\n')
  22. const idle = activity.inspect(123)
  23. expect(idle.state).toBe('idle')
  24. expect(activity.inspect(123)).toEqual(idle)
  25. activity.invalidate()
  26. expect(activity.inspect(123)).toEqual({ state: 'unknown', revision: idle.revision + 1 })
  27. writeFileSync(state, '123:2:busy')
  28. expect(activity.inspect(123).state).toBe('busy')
  29. writeFileSync(state, '999:2:idle\n')
  30. expect(activity.inspect(123).state).toBe('unknown')
  31. writeFileSync(state, '123:3:id')
  32. expect(activity.inspect(123).state).toBe('unknown')
  33. activity.dispose()
  34. expect(activity.inspect(123).state).toBe('unknown')
  35. })
  36. it('leaves unsupported executables, platforms, and custom launch arguments untouched', () => {
  37. expect(prepareShellActivity({ ...spec, argv: ['/bin/fish', '-i'] }, {}, 'darwin')).toBeUndefined()
  38. expect(prepareShellActivity({ ...spec, argv: ['/bin/zsh', '-l'] }, {}, 'darwin')).toBeUndefined()
  39. expect(prepareShellActivity({ ...spec, argv: ['/bin/zsh', '-l', '-i'] }, {}, 'darwin')).toBeUndefined()
  40. expect(prepareShellActivity({ ...spec, shellActivity: false }, {}, 'darwin')).toBeUndefined()
  41. expect(prepareShellActivity(spec, {}, 'win32')).toBeUndefined()
  42. })
  43. it('supports an unset original ZDOTDIR and removes private files on disposal', () => {
  44. const activity = prepareShellActivity(spec, {}, 'darwin')!
  45. cleanups.push(() => { activity.dispose() })
  46. expect(activity.env.ZDOTDIR).toBeDefined()
  47. expect(existsSync(activity.env.ZDOTDIR!)).toBe(true)
  48. activity.dispose()
  49. expect(existsSync(activity.env.ZDOTDIR!)).toBe(false)
  50. })
  51. it('restores a quoted original ZDOTDIR before sourcing user startup files', () => {
  52. const env = Object.freeze({ ZDOTDIR: "/shell config/user's $settings", OTHER: 'preserved' })
  53. const activity = prepareShellActivity(spec, env, 'linux')!
  54. cleanups.push(() => { activity.dispose() })
  55. const directory = activity.env.ZDOTDIR!
  56. expect(directory).not.toBe(env.ZDOTDIR)
  57. expect(activity.env).toEqual({ ...env, ZDOTDIR: directory })
  58. expect(activity.argv).toEqual(spec.argv)
  59. expect(readFileSync(join(directory, '.zshenv'), 'utf8').split('\n').slice(0, 2)).toEqual([
  60. "ZDOTDIR='/shell config/user'\\''s $settings'",
  61. '[[ ! -r ${ZDOTDIR:-$HOME}/.zshenv ]] || builtin source "${ZDOTDIR:-$HOME}/.zshenv"',
  62. ])
  63. activity.dispose()
  64. expect(existsSync(directory)).toBe(false)
  65. })
  66. it('removes the allocated private directory when writing its startup file fails', () => {
  67. let directory: string | undefined
  68. vi.mocked(writeFileSync).mockImplementationOnce((path) => {
  69. directory = dirname(String(path))
  70. throw new Error('disk unavailable')
  71. })
  72. expect(() => prepareShellActivity(spec, {}, 'darwin')).toThrow('disk unavailable')
  73. expect(directory).toBeDefined()
  74. expect(existsSync(directory!)).toBe(false)
  75. })