stat.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. /** The `stat` endpoint: the same gates as `read`, answering identity and freshness without content. */
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { mkdir, symlink, writeFile } from 'node:fs/promises'
  4. import { join } from 'node:path'
  5. import { FsVersion } from '@deepseek-ai/dsh-fs'
  6. import { failureOf, openWorkspace, signal, type Harness } from './harness.ts'
  7. let harness: Harness
  8. beforeEach(async () => {
  9. harness = await openWorkspace('dsh-workspace-files-stat-')
  10. })
  11. afterEach(async () => {
  12. await harness.dispose()
  13. })
  14. describe('workspaceFiles.stat', () => {
  15. it('returns the absolute path, a version, and the byte size', async () => {
  16. await writeFile(join(harness.workspace, 'notes.txt'), 'hello\n', 'utf8')
  17. const result = await harness.endpoint().stat(harness.scope, 'notes.txt', signal())
  18. expect(result.absolutePath).toBe(harness.ctx.fs.processPath(await harness.ctx.fs.resolve(join(harness.workspace, 'notes.txt'))))
  19. expect(result.version.length).toBeGreaterThan(0)
  20. expect(result.bytes).toBe(6)
  21. })
  22. it('answers with the version a read of the same file reports, and a new one after a write', async () => {
  23. const path = join(harness.workspace, 'notes.txt')
  24. await writeFile(path, 'one\n', 'utf8')
  25. const endpoint = harness.endpoint()
  26. const before = await endpoint.stat(harness.scope, 'notes.txt', signal())
  27. const page = await endpoint.read(harness.scope, 'notes.txt', {}, signal())
  28. expect(page.version).toBe(before.version)
  29. await writeFile(path, 'one\ntwo\n', 'utf8')
  30. const after = await endpoint.stat(harness.scope, 'notes.txt', signal())
  31. expect(after.version).not.toBe(before.version)
  32. expect(after.bytes).toBe(8)
  33. })
  34. it('rejects under a signal the caller already aborted, before any path resolves', async () => {
  35. const controller = new AbortController()
  36. controller.abort()
  37. await expect(harness.endpoint().stat(harness.scope, 'notes.txt', controller.signal)).rejects.toThrow()
  38. })
  39. it('resolves the workspace root and then the file under the caller\'s signal', async () => {
  40. await writeFile(join(harness.workspace, 'notes.txt'), 'hello\n', 'utf8')
  41. const fs = harness.ctx.fs
  42. const original = fs.resolve.bind(fs)
  43. const spy = vi.spyOn(fs, 'resolve').mockImplementation((path, opts) => original(path, opts))
  44. const controller = new AbortController()
  45. await harness.endpoint().stat(harness.scope, 'notes.txt', controller.signal)
  46. expect(spy.mock.calls.map(([, opts]) => opts?.signal)).toEqual([controller.signal, controller.signal])
  47. spy.mockRestore()
  48. })
  49. it('omits bytes when the backend reports no size', async () => {
  50. await writeFile(join(harness.workspace, 'notes.txt'), 'hello\n', 'utf8')
  51. vi.spyOn(harness.ctx.fs, 'stat').mockResolvedValue({ version: FsVersion('v-sizeless'), type: 'file' })
  52. const result = await harness.endpoint().stat(harness.scope, 'notes.txt', signal())
  53. expect(result).toEqual({ absolutePath: result.absolutePath, version: 'v-sizeless' })
  54. })
  55. it('accepts outside files and refuses symlinks, directories, missing and empty paths', async () => {
  56. await writeFile(join(harness.outside, 'secret.txt'), 'no', 'utf8')
  57. await symlink(join(harness.outside, 'secret.txt'), join(harness.workspace, 'link.txt'))
  58. await mkdir(join(harness.workspace, 'src'))
  59. const endpoint = harness.endpoint()
  60. expect(await failureOf(endpoint.stat(harness.scope, 'link.txt', signal()))).toMatchObject({
  61. code: 'workspace-file/not-regular-file',
  62. details: { kind: 'symlink' },
  63. })
  64. expect((await failureOf(endpoint.stat(harness.scope, 'src', signal()))).details).toMatchObject({ kind: 'directory' })
  65. expect(await endpoint.stat(harness.scope, join(harness.outside, 'secret.txt'), signal())).toMatchObject({ bytes: 2 })
  66. expect((await failureOf(endpoint.stat(harness.scope, 'nope.txt', signal()))).code).toBe('workspace-file/not-found')
  67. expect((await failureOf(endpoint.stat(harness.scope, '', signal()))).code).toBe('gateway/bad-request')
  68. })
  69. })