shipped-composition.e2e.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { readdir, readFile } from 'node:fs/promises'
  2. import { fileURLToPath } from 'node:url'
  3. import { join } from 'node:path'
  4. import { describe, expect, it } from 'vitest'
  5. import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
  6. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  7. import { COMPOSITION_REPLY_TEXT } from './fixtures/composition-echo-llm.ts'
  8. import { COMPOSITION_SETTLED_MARKER } from './fixtures/composition-settled.ts'
  9. import { runTuiPtySmoke } from './pty-harness.ts'
  10. import { acknowledgeTuiFirstRunWelcome } from '../src/tui-onboarding/tui-first-run-welcome.ts'
  11. const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
  12. const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
  13. const PERMISSION_SUMMARY = 'current preset workspace-write (available: read-only, workspace-write, danger-full-access)'
  14. // An overlay over the shipped tree, so the catalog under test is the one
  15. // `base.cordis.yml` + `tui.cordis.yml` assemble; the tail only swaps the model
  16. // and redirects session artifacts.
  17. const keylessTail = fileURLToPath(new URL('./fixtures/composition-keyless-tail.cordis.yml', import.meta.url))
  18. /**
  19. * The catalog the shipped `dsh` TUI puts in front of the model, as the loop
  20. * logged it, minus the ripgrep-dependent pair below.
  21. * The absences are the composition's security decisions, not incidental gaps:
  22. * the `cordis_*` toolset executes model-written JavaScript that no sandbox row
  23. * confines, `web_fetch` chooses its own request target, and `mcp_*` servers
  24. * spawn outside `ctx.bash`. The composition Agent Note owns the rationale and
  25. * its sources.
  26. */
  27. const EXPECTED_TUI_TOOLS = [
  28. 'ask_user_question',
  29. 'bash',
  30. 'create_goal',
  31. 'edit',
  32. 'exit_plan_mode',
  33. 'get_goal',
  34. 'list_agents',
  35. 'ralph',
  36. 'read',
  37. 'send_message',
  38. 'skill',
  39. 'str_replace_editor',
  40. 'subagent',
  41. 'subagent_fork',
  42. 'task_kill',
  43. 'task_list',
  44. 'task_output',
  45. 'todo_write',
  46. 'update_goal',
  47. 'web_search',
  48. 'workflow',
  49. 'write',
  50. ]
  51. /**
  52. * `glob` and `grep` come from `dsh-tool-fs-search`, which spawns the PACKAGED
  53. * ripgrep binary (`@vscode/ripgrep`) through the subprocess seam, so the pair
  54. * is always present on every host — asserted as fixed members, not a host
  55. * dependency.
  56. */
  57. const RIPGREP_TOOLS = ['glob', 'grep']
  58. /** The assembled request header the smoke asserts on. */
  59. interface LoggedHeader {
  60. /** Assembled tool names, sorted. */
  61. names: string[]
  62. /** `bash`'s assembled parameter properties; the escalation pair is present only under a confining executor. */
  63. bashArguments: Record<string, unknown>
  64. /** Initial permission facts pinned by the shipped composition. */
  65. permissionEvents: Array<[string, unknown]>
  66. }
  67. /**
  68. * Read the request header the loop assembled for its first request from the
  69. * session log the smoke's workspace persisted — the model-visible composition
  70. * itself, not a registry projection taken beside it.
  71. * @param cwd - the smoke's temporary workspace.
  72. * @returns the assembled catalog, system prompt, and `bash` argument shape.
  73. */
  74. async function loggedHeader(cwd: string): Promise<LoggedHeader> {
  75. const sessionsDir = join(cwd, '.sessions')
  76. const entries = await readdir(sessionsDir, { recursive: true })
  77. // A single keyless run writes one session log.
  78. const logRelPath = entries.find(name => name.endsWith('.jsonl'))
  79. if (logRelPath === undefined) throw new Error(`no session log written under ${sessionsDir}`)
  80. const events = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean)
  81. .map(line => JSON.parse(line) as SessionEvent)
  82. const header = events.find(event => event.type === 'request/header')
  83. if (header === undefined || header.type !== 'request/header') {
  84. throw new Error(`session log ${logRelPath} has no request/header event`)
  85. }
  86. const tools = header.data.header.tools ?? []
  87. const bash = tools.find(schema => schema.name === 'bash')
  88. return {
  89. names: tools.map(schema => schema.name).sort(),
  90. bashArguments: (bash?.parameters as { properties?: Record<string, unknown> } | undefined)?.properties ?? {},
  91. permissionEvents: events.flatMap(event =>
  92. event.type === 'permission/preset' || event.type === 'sandbox/mode' || event.type === 'approval/policy'
  93. ? [[event.type, event.data] as [string, unknown]]
  94. : []),
  95. }
  96. }
  97. describe('shipped dsh composition (real Loader tree in a PTY)', () => {
  98. it('assembles exactly the shipped TUI catalog', async () => {
  99. let observed: LoggedHeader | undefined
  100. const output = await runTuiPtySmoke({
  101. label: 'dsh shipped composition',
  102. tempDirPrefix: 'dsh-shipped-tui-',
  103. binScript: dshBinScript,
  104. tsconfigPath,
  105. configPath: keylessTail,
  106. env: { DEEPSEEK_API_KEY: 'keyless-composition-no-call', DSH_TELEMETRY_DISABLED: '1' },
  107. prepare: cwd => acknowledgeTuiFirstRunWelcome(join(cwd, '.dsh')),
  108. // Artifact CI builds and smokes concurrently on a contended runner.
  109. ...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
  110. actions: [
  111. { waitFor: COMPOSITION_SETTLED_MARKER, send: '/permission\r' },
  112. { waitFor: PERMISSION_SUMMARY, send: 'Describe the shipped composition.\r' },
  113. { waitFor: COMPOSITION_REPLY_TEXT, send: '/exit\r' },
  114. ],
  115. inspect: async (cwd) => { observed = await loggedHeader(cwd) },
  116. })
  117. expect(output).toContain(COMPOSITION_REPLY_TEXT)
  118. expect(output).toContain(PERMISSION_SUMMARY)
  119. expect(observed?.names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TUI_TOOLS)
  120. // The packaged ripgrep binary ships with the dependency, so the pair is a
  121. // fixed roster member on every host.
  122. expect(observed?.names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
  123. expect(observed?.bashArguments).toHaveProperty('sandbox_permissions')
  124. expect(observed?.bashArguments).toHaveProperty('justification')
  125. expect(observed?.permissionEvents).toEqual([
  126. ['permission/preset', { preset: 'workspace-write' }],
  127. ['sandbox/mode', { mode: 'workspace-write' }],
  128. ['approval/policy', { policy: 'ask' }],
  129. ])
  130. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  131. })