loader-composition.e2e.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { realpathSync } from 'node:fs'
  2. import { readFile, readdir } from 'node:fs/promises'
  3. import { join } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { describe, expect, it } from 'vitest'
  6. import { type SessionEvent } from '@deepseek-ai/dsh-session'
  7. import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
  8. /**
  9. * Keyless REAL-composition coverage for parent-session cwd inheritance: a
  10. * test-only cordis.yml boots the headless app through the Loader with the ACP
  11. * backend's `cwd` omitted, a scripted model delegates once, and the scripted
  12. * mock ACP child echoes where it actually ran plus the workspace it was
  13. * announced — both must be the parent session's cwd. Mock-only composition, so
  14. * only this keyless tier applies (the with-key tier lives in subagent-acp.e2e.ts).
  15. */
  16. const driver = fileURLToPath(new URL(
  17. '../../../../examples/acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts',
  18. import.meta.url,
  19. ))
  20. const configPath = fileURLToPath(new URL(
  21. '../../../../examples/acp-agent/tests/fixtures/subagent/subagent-acp/cordis.yml',
  22. import.meta.url,
  23. ))
  24. const mockServer = fileURLToPath(new URL('./mock-acp-server.ts', import.meta.url))
  25. const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
  26. async function jsonlFiles(dir: string): Promise<string[]> {
  27. const entries = await readdir(dir, { withFileTypes: true })
  28. const paths = await Promise.all(entries.map(async (entry) => {
  29. const path = join(dir, entry.name)
  30. if (entry.isDirectory()) return jsonlFiles(path)
  31. return entry.isFile() && entry.name.endsWith('.jsonl') ? [path] : []
  32. }))
  33. return paths.flat()
  34. }
  35. describe('ACP subagent cwd inheritance through a real cordis.yml', () => {
  36. it('runs the child in the parent session workspace and announces it as the ACP session cwd', async () => {
  37. let events: SessionEvent[] = []
  38. let workspace = ''
  39. const { stderr } = await runLoaderSmoke({
  40. label: 'acp-subagent cwd composition smoke',
  41. tempDirPrefix: 'acp-subagent-cwd-e2e-',
  42. binScript: driver,
  43. libBinScript: driver,
  44. configPath,
  45. tsconfigPath: repoTsconfig,
  46. env: { DSH_TEST_MOCK_ACP_SERVER: mockServer },
  47. inspect: async (cwd) => {
  48. // The child reports realpaths; canonicalize the temp workspace to match.
  49. workspace = realpathSync(cwd)
  50. const logs = await jsonlFiles(join(cwd, '.sessions'))
  51. expect(logs).toHaveLength(1)
  52. const lines = (await readFile(logs[0] as string, 'utf8')).trimEnd().split('\n')
  53. events = lines.slice(1).map(line => JSON.parse(line) as SessionEvent)
  54. },
  55. })
  56. expect(stderr).not.toContain('UNHANDLED')
  57. // The tool result carries the child's two-line echo: its real process.cwd()
  58. // and the cwd the backend announced in `session/new` — both the parent
  59. // session's workspace, never the harness process's launch directory.
  60. const results = events.filter(event => event.type === 'tool/result')
  61. expect(results).toHaveLength(1)
  62. const resultText = results[0]!.data.message.content[0].content
  63. .filter(block => block.type === 'text')
  64. .map(block => block.text)
  65. .join('')
  66. expect(resultText).toBe(`${workspace}\n${workspace}`)
  67. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  68. })