import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot' /** * Snapshot suite for the plan-mode composition (`../cordis.yml`, swapped to * the sibling `cordis.snapshot.yml` replay overlay by the bin under * `DSH_SNAPSHOT=replay`). * * The recorded scenarios re-execute the fs tools AND the sandboxed bash * executor for real on replay (the replay overlay swaps only the model): the * recorded `cat` runs under the host's actual runner (Seatbelt on macOS, * bwrap on Linux CI), under the sandbox's own configured mode — plan mode * does not change it. The recorded commands stay `cat`-shaped — * byte-identical across those backends and across GNU/BSD userlands — and * the transcripts carry NO sandbox denial (a denied command's stderr is the * backend's dialect; the denial→marker path is pinned at dsh-tool-bash's * unit tier). Prompts pin the model to RELATIVE paths, because a recorded * absolute temp path would neither replay on another host nor normalize * (the normalizers scrub the RUN's own cwd, not the recording's). */ function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] { switch (value) { case undefined: case '': case 'replay': return 'replay' case 'record': return 'record' case 'refresh': return 'refresh' default: throw new Error(`unknown DSH_SNAPSHOT mode: ${value}`) } } const SCENARIOS: Scenario[] = [ // Protocol-only (keyless, authored): the session-mode surface this // composition adds — availableModes/currentModeId advertised on // session/new, the optimistic current_mode_update a session/set_mode // answers with, and the loud rejection of an unknown mode id — as committed // wire bytes. No model turn → no headers, so its class membership is // vacuous; it joins 'plan' because the factory requires every scenario's // class to carry a pin. { name: 'modes-advertise', hasModelTurn: false, recorded: false, headerClass: 'plan' }, // The full plan-mode arc, and NECESSARILY the pinned-header scenario for // the 'plan' class: the first request ships the configured mode section and // the full toolset, including exit_plan_mode. Approval removes only the // section; the following changed header carries byte-identical tool schemas. // The arc: setMode(plan) → the model runs a real `cat` inside plan and // presents the plan via exit_plan_mode → the scripted elicitation approves // → the very next step already edits for real, mid-turn. { name: 'plan-mode', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'plan', expectedHeaderChanges: 1 }, // The keep-planning branch: one presentation, the scripted review answers // with free-text feedback (no approval), and the corrective isError carries // it back verbatim — the session stays in plan mode, so the log holds one // plan-shaped header, uniform with the pin's first. { name: 'plan-mode-reject', hasModelTurn: true, recorded: true, headerClass: 'plan' }, ] defineAcpSnapshotSuite({ agent: { binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)), configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)), tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)), }, snapshotsDir: join(dirname(fileURLToPath(import.meta.url)), 'snapshots'), scenarios: SCENARIOS, mode: snapshotModeFromEnv(process.env.DSH_SNAPSHOT), })