harness.ts 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /**
  2. * Shared fixture: a real local backend over a temp workspace beside a sibling
  3. * directory outside it, and a sandbox policy whose only job is naming the root.
  4. *
  5. * The real backend, not a mocked `ctx.fs`, because the gates under test are
  6. * only meaningful against a real filesystem: a symlink that leaves the
  7. * workspace, a file whose byte size exceeds the cap, and bytes that are not
  8. * text. A fake provider would let a string-prefix containment check pass this
  9. * file, which is exactly the defect the gate exists to prevent.
  10. */
  11. import { mkdir, mkdtemp, rm } from 'node:fs/promises'
  12. import { tmpdir } from 'node:os'
  13. import { join } from 'node:path'
  14. import { Context } from '@deepseek-ai/cordis'
  15. import type { Agent } from '@deepseek-ai/dsh-agent'
  16. import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
  17. import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
  18. import { WorkspaceFiles, type Config } from '../src/index.ts'
  19. /** The Agent shape the service reads: only its session reaches the policy. */
  20. export const agent = { id: 'a-test', session: { id: 's-test' } } as unknown as Agent
  21. export const signal = (): AbortSignal => new AbortController().signal
  22. /** One temp workspace and the context serving it. */
  23. export interface Harness {
  24. readonly workspace: string
  25. readonly outside: string
  26. readonly ctx: Context
  27. /**
  28. * The service under test, at the given caps. One per test: the service key is
  29. * global to the Context, so a second call with caps is a defect in the test.
  30. */
  31. endpoint(caps?: Partial<Config>): WorkspaceFiles
  32. dispose(): Promise<void>
  33. }
  34. /**
  35. * Create the workspace, its outside sibling, and a context with the local
  36. * backend rooted at the workspace.
  37. * @param prefix - temp directory prefix naming the suite.
  38. * @returns the harness; dispose it in `afterEach`.
  39. */
  40. export async function openWorkspace(prefix: string): Promise<Harness> {
  41. const root = await mkdtemp(join(tmpdir(), prefix))
  42. const workspace = join(root, 'workspace')
  43. const outside = join(root, 'outside')
  44. await mkdir(workspace, { recursive: true })
  45. await mkdir(outside, { recursive: true })
  46. const ctx = new Context()
  47. const fiber = await ctx.plugin(LocalFileSystem, { cwd: workspace })
  48. // The policy is the service's only source for the workspace root, so the
  49. // fake supplies exactly that and nothing else.
  50. ctx.provide('sandboxPolicy', { resolve: () => ({ mode: 'workspace-write', workspaceRoot: workspace }) } as never)
  51. let service: WorkspaceFiles | undefined
  52. return {
  53. workspace,
  54. outside,
  55. ctx,
  56. endpoint: (caps) => {
  57. if (service !== undefined) {
  58. if (caps !== undefined) throw new Error('the harness serves one WorkspaceFiles per test; hoist the endpoint')
  59. return service
  60. }
  61. service = new WorkspaceFiles(ctx, {
  62. maxBytes: caps?.maxBytes ?? 1024 * 1024,
  63. maxFileBytes: caps?.maxFileBytes ?? 1024 * 1024,
  64. maxLines: caps?.maxLines ?? 5000,
  65. maxEntries: caps?.maxEntries ?? 2000,
  66. })
  67. return service
  68. },
  69. dispose: async () => {
  70. await fiber.dispose()
  71. await rm(root, { recursive: true, force: true })
  72. },
  73. }
  74. }
  75. /**
  76. * Await an operation expected to fail with a Remote error.
  77. * @param operation - the call under test.
  78. * @returns the Remote failure's code and details.
  79. */
  80. export async function failureOf(operation: Promise<unknown>): Promise<{ code: string; details: unknown }> {
  81. try {
  82. await operation
  83. } catch (error: unknown) {
  84. const failure = remoteErrorOf(error)
  85. // A non-Remote throw is a defect in the service, not an expected outcome.
  86. if (failure === undefined) throw error
  87. return { code: failure.code, details: failure.details }
  88. }
  89. throw new Error('expected the operation to fail')
  90. }