test(snapshot): cover session-scoped sandbox roots
This commit is contained in:
@@ -106,7 +106,7 @@ export interface RunResult {
|
||||
stderr: string
|
||||
/** The session id the server issued (undefined if no session was created). */
|
||||
sessionId?: string
|
||||
/** The temp cwd the session ran in (the bash workspace). */
|
||||
/** The generated cwd the session ran in (the bash workspace). */
|
||||
cwd: string
|
||||
/**
|
||||
* Every persisted session log harvested after the run, ordered primary-first:
|
||||
@@ -137,11 +137,19 @@ export interface RunOptions {
|
||||
childFiles?: string[]
|
||||
/**
|
||||
* Optional `<scenario>/workspace/` directory whose contents are copied into
|
||||
* the temp cwd BEFORE the run — the standard way to seed files the agent
|
||||
* the generated cwd BEFORE the run — the standard way to seed files the agent
|
||||
* operates on (a file to read, edit, or grep). Absent for scenarios that
|
||||
* start from an empty workspace.
|
||||
*/
|
||||
workspaceDir?: string
|
||||
/**
|
||||
* Parent directory for the generated session cwd. Defaults to
|
||||
* `os.tmpdir()`. A scenario that must distinguish its workspace from the
|
||||
* sandbox's always-writable temporary roots can place the generated child
|
||||
* under `os.homedir()` instead. The harness removes only that generated
|
||||
* child, never the supplied parent.
|
||||
*/
|
||||
workspaceParent?: string
|
||||
/**
|
||||
* Alternate LIVE config path for the boot (absolute), overriding
|
||||
* {@link AgentUnderTest.configPath} for this run. A scenario needing a
|
||||
@@ -162,15 +170,15 @@ function scenarioSpillRoot(fixtureFile: string): string {
|
||||
|
||||
/**
|
||||
* Run a scenario end-to-end against a freshly-spawned subprocess. Owns the
|
||||
* child and its temp dirs; always tears them down. Returns the captured stdout
|
||||
* child and its generated dirs; always tears them down. Returns the captured stdout
|
||||
* and (record mode) the harvested session-log path.
|
||||
*
|
||||
* @param input The scenario's input script (steps + optional permission answers).
|
||||
* @param opts The agent to boot, the mode, and the fixture wiring.
|
||||
* @returns The captured stdout/stderr, session id, temp cwd, and harvested logs.
|
||||
* @returns The captured stdout/stderr, session id, generated cwd, and harvested logs.
|
||||
*/
|
||||
export async function runScenario(input: InputScript, opts: RunOptions): Promise<RunResult> {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'acp-snap-cwd-'))
|
||||
const cwd = await mkdtemp(join(opts.workspaceParent ?? tmpdir(), 'acp-snap-cwd-'))
|
||||
const sessionsRoot = await mkdtemp(join(tmpdir(), 'acp-snap-sessions-'))
|
||||
// Fixed path length: spill-policy budgets the preview against the REAL path
|
||||
// before stdout normalization, so tmpdir() length differences churn expected outputs.
|
||||
@@ -184,7 +192,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
|
||||
let sessionLogs: HarvestedLog[] = []
|
||||
const outcome = await (async (): Promise<RunResult> => {
|
||||
// Seed the workspace if the scenario ships one (a file the agent reads/edits).
|
||||
// Copied into the temp cwd so the agent's bash tools see it; the expected outputs
|
||||
// Copied into the generated cwd so the agent's bash tools see it; the expected outputs
|
||||
// normalize the cwd, so the seeded paths stay stable across runs.
|
||||
if (opts.workspaceDir !== undefined && existsSync(opts.workspaceDir)) {
|
||||
await cp(opts.workspaceDir, cwd, { recursive: true })
|
||||
@@ -251,7 +259,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
|
||||
// persistence) and exits. Then await exit so the harvested log is complete.
|
||||
await active.close()
|
||||
// Harvest EVERY persisted log (parent + any subagent children) while the
|
||||
// temp dirs still exist, ordered primary-first.
|
||||
// generated dirs still exist, ordered primary-first.
|
||||
sessionLogs = await harvestSessionLogs(sessionsRoot)
|
||||
return {
|
||||
rawStdout: launched.rawStdout(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pure ACP transcript and session-log normalizers. They scrub session ids, temp cwd, RPC ids,
|
||||
* Pure ACP transcript and session-log normalizers. They scrub session ids, run cwd, RPC ids,
|
||||
* timestamps, and hook duration while preserving deterministic event sequence numbers.
|
||||
* Request-header scrubbers stay composable so one scenario per header class can pin prompt and
|
||||
* tool-schema sidecars while retaining any model-visible prefix in the session log.
|
||||
@@ -29,7 +29,7 @@ const SNAPSHOT_SPILL_PATH_RE = new RegExp(
|
||||
export interface NormalizeContext {
|
||||
/** The session id(s) the run issued — replaced with `{{sessionId}}`. */
|
||||
sessionIds: string[]
|
||||
/** The temp cwd the run used — replaced with `{{cwd}}`. */
|
||||
/** The generated cwd the run used — replaced with `{{cwd}}`. */
|
||||
cwd: string
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,12 @@ export interface Scenario {
|
||||
* {@link headerClass}.
|
||||
*/
|
||||
configPath?: string
|
||||
/**
|
||||
* Parent directory for the generated session cwd. Defaults to the platform
|
||||
* temp directory; set this when temp is itself part of the behavior under
|
||||
* test and the scenario needs an independent project location.
|
||||
*/
|
||||
workspaceParent?: string
|
||||
}
|
||||
|
||||
/** One suite's inputs: the agent to boot, where its fixtures live, and its scenario table. */
|
||||
@@ -178,7 +184,7 @@ export function fixtureContext(fixture: string): NormalizeContext {
|
||||
* The `data.header` payload of every `request/header` event in a session
|
||||
* JSONL, in log order, with the log's volatile values scrubbed first
|
||||
* ({@link normalizeSessionLog}) so headers harvested from different runs —
|
||||
* each embedding its own temp cwd in the composed prompt — compare on equal
|
||||
* each embedding its own generated cwd in the composed prompt — compare on equal
|
||||
* footing.
|
||||
*
|
||||
* @param rawLog The session `.jsonl` content to extract headers from.
|
||||
@@ -488,6 +494,7 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void {
|
||||
// replays from its own script. In RECORD they are harvested, not read.
|
||||
...!RECORDING && childFixtureFiles.length > 0 ? { childFiles: childFixtureFiles.map(file => join(dir, file)) } : {},
|
||||
...existsSync(workspaceDir) ? { workspaceDir } : {},
|
||||
...scenario.workspaceParent !== undefined ? { workspaceParent: scenario.workspaceParent } : {},
|
||||
// A scenario booting an overlay tree passes its own live config; the
|
||||
// bin's replay swap derives the sibling `*cordis.snapshot.yml` from it.
|
||||
...scenario.configPath !== undefined ? { configPath: scenario.configPath } : {},
|
||||
|
||||
Reference in New Issue
Block a user