@tanstack/ai-sandbox 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"journal-conformance.js","names":[],"sources":["../../../src/testkit/journal-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for the agent output journal.\n *\n * The journal design rests on two provider-level claims: a command string is\n * framed through a POSIX shell (so `>>` redirection works), and `tail -c +N -f`\n * is available. Both are asserted here against a real sandbox rather than\n * assumed from the audit.\n *\n * A provider that cannot satisfy them MUST declare `unsupported.reason`. There\n * is deliberately no silent-skip path: a conformance case that quietly returns\n * prints as a pass, which is how an unimplemented capability ships green. The\n * three FOLLOW cases obey the same rule through a second declaration,\n * {@link JournalConformanceConfig.followUnsupported} — see {@link itFollows} for\n * why the strategy has to be declared rather than detected at registration time,\n * and {@link expectDeclaredStrategy} for what keeps the declaration honest.\n *\n * THE THIRD FOLLOW CASE TESTS THE OTHER SIDE OF THE BOUNDARY, and it is here\n * because the first two do not. `killableProcesses` is what selects `'follow'`\n * over `'poll'`, and a wrong `true` means `tail -f` is spawned on the assumption\n * it can be reclaimed — leaking one follower per run when it cannot. The two\n * follow cases only ever asserted that the READER stops, which\n * `journal-reader.ts`'s `untilAborted` guarantees on its own by abandoning the\n * pipe the moment the signal fires. So both of them pass a provider whose\n * `kill()` is `() => Promise.resolve()`, and three of the four `true`\n * declarations in this repo were in fact false: Docker's `stream.destroy()` only\n * detached the client, local-process's `sh -c` forks so signalling the shell left\n * the command alive, and Vercel's `kill()` never called the SDK's real\n * `Command.kill` at all. Every one of them shipped green through this suite.\n * \"kills the sandbox-side process, not just the host's view of it\" is the case\n * that fails them — see its own comment for how it probes.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { randomUUID } from 'node:crypto'\nimport { describe, expect, it } from 'vitest'\nimport {\n exitSentinelLine,\n journalExistsCommand,\n journalPaths,\n journalReadCommand,\n journaledCommand,\n} from '../journal'\nimport { journalReadStrategy, readJournal } from '../journal-reader'\nimport type { JournalPaths } from '../journal'\nimport type { SandboxHandle } from '../contracts'\n\nexport interface JournalConformanceConfig {\n /** Provider name, used in the describe title. */\n name: string\n /** Create a live sandbox plus its teardown. */\n createHandle: () => Promise<{\n handle: SandboxHandle\n dispose: () => Promise<void>\n }>\n /**\n * Declare that this provider cannot journal, with the reason. Registers a\n * skipped case whose title carries the reason. Omit it and the suite runs.\n */\n unsupported?: { reason: string }\n /**\n * Declare that this provider's reads take the POLL strategy rather than the\n * FOLLOW one — i.e. `journalReadStrategy` answers `'poll'` for its handles,\n * because it lacks `backgroundProcesses` or `killableProcesses`. The two follow\n * cases then register as NAMED skips carrying the reason.\n *\n * Declare this ONLY when the provider really cannot follow. It is checked\n * against a live handle in a case that always runs\n * ({@link expectDeclaredStrategy}), so a wrong declaration fails the suite in\n * either direction rather than quietly removing coverage.\n */\n followUnsupported?: { reason: string }\n}\n\n/**\n * Per-case timeout. Every case here spawns a real sandbox and a real agent.\n *\n * 180s, not the 60s this used to be, and it matches the ceiling\n * `takeover-conformance.ts` already gives its heaviest cases. It is the one\n * wall-clock number left in the file and it is deliberately far outside the range\n * any healthy run needs: a case here makes half a dozen provider round-trips, and\n * ONE `docker exec` on a loaded daemon has been measured at 9.6s (see\n * `takeover-conformance.ts`'s `countingExec`) and at 20–45s on a saturated one, so\n * a 60s budget put the timeout itself in the same load-sensitive class as the\n * assertions that were removed from these cases — measured going red on cases that\n * pass in 7–13s each on a quiet machine.\n *\n * This bound exists only so a genuine hang FAILS instead of parking CI; it is not\n * an assertion about speed, and nothing here should be tuned to sit near it.\n */\nconst CASE_TIMEOUT_MS = 180_000\n\n/**\n * Register a case that only means anything on a provider whose reads FOLLOW.\n *\n * `journalReadStrategy` needs a live handle and a live handle needs the async\n * `createHandle`, so the strategy is not knowable when the cases are registered.\n * It is therefore DECLARED, and the declaration selects `it` or `it.skip` here.\n *\n * This exists because the alternative — checking the strategy inside the case and\n * returning early — is the silent-skip the module doc forbids. Such a case prints\n * `✓` with a duration and a title claiming a property was verified while every\n * real assertion in it (including the incremental-delivery handshake, which is\n * the entire reason the follow path exists) was skipped. A named `it.skip` prints\n * `↓` with the reason instead.\n */\nfunction itFollows(\n config: JournalConformanceConfig,\n title: string,\n fn: () => Promise<void>,\n): void {\n const unsupported = config.followUnsupported\n if (unsupported === undefined) {\n it(title, fn, CASE_TIMEOUT_MS)\n return\n }\n it.skip(\n `${title} — follow strategy unsupported: ${unsupported.reason}`,\n fn,\n CASE_TIMEOUT_MS,\n )\n}\n\n/**\n * Assert the live handle's read strategy is the one the config DECLARED.\n *\n * BOTH directions are defects, and neither is a skip. A provider that declared\n * `followUnsupported` but whose handles do follow silently loses the two cases it\n * could pass. One that declared nothing but polls would reach the follow\n * assertions and fail them for a reason unrelated to journaling — which is what\n * the previous `expect(handle.capabilities.killableProcesses).toBe(false)` branch\n * did to a provider with `backgroundProcesses: false, killableProcesses: true`.\n * Either way the config does not describe the provider, and that is worth\n * failing.\n */\nfunction expectDeclaredStrategy(\n handle: SandboxHandle,\n config: JournalConformanceConfig,\n): void {\n expect(journalReadStrategy(handle)).toBe(\n config.followUnsupported === undefined ? 'follow' : 'poll',\n )\n}\n\n/** Decode the base64 frame a journal read command produces into raw text. */\nfunction decodeJournalRead(stdout: string): string {\n return Buffer.from(stdout.replace(/\\s+/g, ''), 'base64').toString('utf8')\n}\n\n/**\n * Block until the run's journal file exists in the sandbox.\n *\n * Through the shell (`journalExistsCommand`), never `handle.fs.exists` — see\n * rule 3 in `../journal.ts`: on local-process the two resolve `/tmp`\n * differently, so an `fs` probe would report the wrong file.\n *\n * Exported for `./reaper-conformance.ts`, which needs the same bounded,\n * shell-only wait before probing a still-producing run. Internal to the testkit;\n * not part of the `./testkit` public surface.\n */\nexport async function waitForJournal(\n handle: SandboxHandle,\n paths: JournalPaths,\n): Promise<void> {\n const deadline = Date.now() + 15_000\n for (;;) {\n const probe = await handle.process.exec(journalExistsCommand(paths))\n if (probe.exitCode === 0) return\n if (Date.now() > deadline) {\n throw new Error(`journal conformance: ${paths.journal} never appeared`)\n }\n await sleep(100)\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * An absolute path inside the sandbox that no other case, suite, or machine will\n * touch.\n *\n * Every character is in `[A-Za-z0-9./-]`, so these interpolate into the shell\n * commands below as a single word without quoting. `/tmp` and not the workspace:\n * on local-process a shell redirect reaches the host's real `/tmp` while\n * `handle.fs` resolves under the sandbox root (see rule 3 in `../journal.ts`),\n * and everything here is written AND read through the shell so the two never have\n * to agree.\n */\nfunction noncePath(label: string): string {\n return `/tmp/tanstack-journal-conformance-${label}-${randomUUID()}`\n}\n\n/** Iteration cap on the kill probe's loop, so nothing can outlive the suite. */\nconst PROBE_MAX_TICKS = 600\n\n/**\n * Bound on a journal read, so a reader that delivers nothing FAILS instead of\n * parking CI.\n *\n * Never an assertion, and deliberately far above anything a healthy read needs\n * (measured: 10–18s for the follow cases on both providers). Each case that uses\n * it proves its property some other way — a causal handshake, or\n * `backstop.aborted` — so this number can be raised freely and must never be the\n * thing a case is tuned against.\n */\nconst READ_BACKSTOP_MS = 90_000\n\n/**\n * How long to let an asynchronous kill land before the quiet window opens.\n *\n * A kill is asynchronous on every provider here — Docker signals through a\n * second `exec`, local-process signals a process group and lets the OS reap — so\n * one more heartbeat tick immediately after `kill()` resolves is not a survivor.\n */\nconst KILL_SETTLE_MS = 5_000\n\n/**\n * The quiet window: how long the heartbeat must stay frozen.\n *\n * This is NOT a load-sensitive bound, and the asymmetry is the point. A dead\n * process can never write again, so a slow or busy machine can only make this\n * window MORE reliable, never less — unlike a \"must happen within Nms\" ceiling,\n * which fails on load. Only a live survivor can end this window, and a live\n * survivor writes once a second.\n */\nconst HEARTBEAT_QUIET_MS = 6_000\n\n/**\n * Byte count of `path`, according to the SANDBOX'S OWN shell, or `null` when it\n * cannot be read.\n *\n * `wc -c` through the shell, never `handle.fs`: on local-process the two resolve\n * `/tmp` differently (rule 3), so an `fs` probe would answer about a file the\n * sandbox never wrote and the growth below would look frozen from the first\n * sample — a vacuous pass. Parsed strictly rather than coerced, so a shell\n * diagnostic cannot become `NaN` and compare unequal to itself.\n */\nasync function fileSize(\n handle: SandboxHandle,\n path: string,\n): Promise<number | null> {\n const probe = await handle.process.exec(`wc -c < ${path} 2>/dev/null`)\n const text = probe.stdout.trim()\n return /^\\d+$/.test(text) ? Number(text) : null\n}\n\n/**\n * Wait until `path` has grown to at least `bytes`, i.e. the probe process is\n * provably DOING WORK inside the sandbox, and answer whether it got there.\n *\n * Returning the observation rather than throwing keeps the verdict inside the\n * case's own `expect`: this is the \"before\" half of the assertion, and it is what\n * makes the \"after\" half a live detector instead of a formality.\n */\nasync function waitForTicks(\n handle: SandboxHandle,\n path: string,\n bytes: number,\n): Promise<boolean> {\n const deadline = Date.now() + 30_000\n for (;;) {\n const size = await fileSize(handle, path)\n if (size !== null && size >= bytes) return true\n if (Date.now() > deadline) return false\n // Matched to the heartbeat's own 1s period on purpose. Every poll is a\n // provider round-trip, so a 250ms interval spent three of them per tick it\n // could not possibly observe — pure pressure on the very `exec` path the rest\n // of the case depends on.\n await sleep(1_000)\n }\n}\n\n/**\n * Assert `createHandle` satisfies the journal conformance contract. Each `it`\n * gets a fresh sandbox via `createHandle`/`dispose`, so implementations may\n * share process state across calls without cross-test bleed only if\n * `createHandle` returns an isolated sandbox.\n */\nexport function runJournalConformance(config: JournalConformanceConfig): void {\n describe(`journal conformance — ${config.name}`, () => {\n if (config.unsupported) {\n it.skip(`unsupported: ${config.unsupported.reason}`, () => {\n expect(true).toBe(true)\n })\n return\n }\n\n it(\n \"redirects a command's stdout into the journal and appends the exit sentinel\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n // Checked HERE, in a case that always runs, because\n // `followUnsupported` gates the two follow cases below: a declaration\n // that does not match the live handle must fail the suite rather than\n // remove coverage from it. This is the only place a `poll` declaration\n // can be caught, since the cases it skips never execute.\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-${Date.now()}`)\n const command = journaledCommand(\n `printf '{\"a\":1}\\\\n{\"b\":2}\\\\n'`,\n paths,\n )\n const proc = await handle.process.spawn(command)\n expect(await proc.wait()).toBe(0)\n\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`{\"a\":1}\\n{\"b\":2}\\n${exitSentinelLine(paths, 0)}\\n`)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n \"records the agent's non-zero exit in the sentinel\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-exit-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand('exit 7', paths),\n )\n await proc.wait()\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`${exitSentinelLine(paths, 7)}\\n`)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n \"keeps the agent's stderr out of the journal\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-err-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand(\n `printf '{\"a\":1}\\\\n'; printf 'a warning\\\\n' 1>&2`,\n paths,\n ),\n )\n await proc.wait()\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`{\"a\":1}\\n${exitSentinelLine(paths, 0)}\\n`)\n expect(text).not.toContain('a warning')\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n 'reads incrementally from a byte offset with absolute positions',\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-seek-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand(`printf '{\"a\":1}\\\\n{\"b\":2}\\\\n'`, paths),\n )\n await proc.wait()\n\n const all = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n strategy: 'poll',\n pollIntervalMs: 0,\n // Not an assertion — see {@link READ_BACKSTOP_MS}. This was\n // `AbortSignal.timeout(5_000)`, and it is a POLL read, so it costs one\n // provider round-trip per line: measured going red on a saturated\n // Docker daemon where a single `exec` took ~20s, while the lines it\n // asserts were perfectly correct.\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n all.push(line)\n if (all.length === 3) break\n }\n expect(all.map((l) => l.line)).toEqual([\n '{\"a\":1}',\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n\n const resumed = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: all[0]?.endPosition ?? 0,\n strategy: 'poll',\n pollIntervalMs: 0,\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n resumed.push(line)\n if (resumed.length === 2) break\n }\n expect(resumed.map((l) => l.line)).toEqual([\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n expect(resumed[0]?.endPosition).toBe(all[1]?.endPosition)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n // This case is the reason `journalFollowCommand` pipes into nothing.\n // `tail -f journal | base64` delivers ZERO bytes while the agent is still\n // running — measured on GNU coreutils 8.32 `base64` and on busybox 1.36.1\n // `base64` in Alpine — because the encoder buffers its stdout until its\n // stdin closes, which only happens when the reader kills `tail`.\n //\n // INCREMENTAL, NOT MERELY EVENTUAL, AND PROVED CAUSALLY RATHER THAN BY A\n // STOPWATCH. The agent writes its first line and then BLOCKS on a gate file\n // that only this reader can create, and it creates it only on receiving that\n // first line. So the agent cannot reach its second line until the first was\n // delivered — receiving `{\"b\":2}` at all IS the proof of incremental\n // delivery, and the `toEqual` below is the whole assertion. A buffering\n // filter reintroduced onto the follow path deadlocks instead: nothing is\n // delivered, the gate is never created, the agent never writes its second\n // line, the read ends on its signal and `seen` is empty.\n //\n // This replaced `expect(firstLineMs).toBeLessThan(3_000)`, which measured\n // MACHINE LOAD as much as behavior: it was observed failing in whole-suite\n // fleet runs while passing 5/5 in isolation, because one `exec`/`spawn` is a\n // provider round-trip whose latency this suite does not control (a\n // `docker exec` on a loaded daemon has been measured at 9.6s). Same instinct\n // as `takeover-conformance.ts`'s `countingExec`: anchor on the property, not\n // on the clock. A bound that goes red on a busy machine teaches people to\n // ignore the suite. Do NOT \"fix\" a failure here by widening a window —\n // there is no window left to widen.\n itFollows(\n config,\n 'follows a journal that is still being written, delivering each line before the next is produced',\n async () => {\n // Every assertion below sits after an `await`, so a case that threw its way\n // out of the loop early would report an unrelated failure; this one reports\n // \"nothing was asserted\", which is the failure this case used to HIDE.\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n const gate = noncePath('follow-gate')\n try {\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-follow-${Date.now()}`)\n // The wait is bounded in the SANDBOX too, and on timeout it emits a\n // line that names what went wrong instead of the expected one — so a\n // gate that never arrives fails the `toEqual` with `{\"gate\":\"never\"}`\n // rather than eventually satisfying it. 30 ticks so that diagnostic\n // lands INSIDE `CASE_TIMEOUT_MS`; a longer cap would just time the case\n // out and lose the message.\n const agentCommand =\n `printf '{\"a\":1}\\\\n'; ` +\n `i=0; while [ ! -f ${gate} ]; do ` +\n `i=$((i+1)); ` +\n `if [ $i -gt 30 ]; then printf '{\"gate\":\"never\"}\\\\n'; break; fi; ` +\n `sleep 1; done; ` +\n `printf '{\"b\":2}\\\\n'`\n // Not awaited anywhere: reading the `__exit` sentinel below IS the\n // proof it finished. (`SpawnHandle.wait()` is not safe to call after the\n // fact on every provider — local-process registers a `close` listener at\n // call time, so a `wait()` issued after the process already exited never\n // resolves.)\n void handle.process.spawn(journaledCommand(agentCommand, paths))\n // `tail` on a file that does not exist yet exits immediately, and the\n // agent's spawn and the reader's spawn race. Waiting removes that race\n // WITHOUT touching the property under test: with a buffering filter on\n // the follow path the journal still exists, `tail` still runs, and the\n // reader still receives nothing.\n await waitForJournal(handle, paths)\n // The premise, pinned: the gate is genuinely absent, so the agent\n // really is blocked and its second line really is downstream of this\n // reader. Without this a pre-existing gate path would make the case\n // pass without following anything.\n expect(\n (await handle.process.exec(`test -e ${gate}`)).exitCode,\n ).not.toBe(0)\n const seen: Array<string> = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n // Not the assertion — see {@link READ_BACKSTOP_MS}. The gate's own\n // 30-tick cap fires well inside it, so the `{\"gate\":\"never\"}`\n // diagnostic still reaches this reader.\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n seen.push(line.line)\n // Releases the agent, and only from inside the stream. `touch` is\n // not portable to every BusyBox build with these flags, so this\n // creates the file with a redirect, through the shell — `handle.fs`\n // would write a path the agent's `test -f` cannot see on\n // local-process (rule 3).\n if (seen.length === 1) {\n await handle.process.exec(`: >> ${gate}`)\n }\n if (seen.length === 3) break\n }\n expect(seen).toEqual([\n '{\"a\":1}',\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n } finally {\n // Unblocks the agent even when the case failed, so no `while` loop\n // outlives it on a provider whose sandbox teardown does not reap.\n await handle.process.exec(`: >> ${gate}`).catch(() => undefined)\n await dispose()\n }\n },\n )\n\n // The follow read must obey its own AbortSignal rather than waiting for the\n // provider's `kill` to close the stream — a provider whose kill misses a\n // grandchild would otherwise hang the reader past its deadline.\n itFollows(\n config,\n 'stops a follow read when its signal aborts, without a consumer break',\n async () => {\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n try {\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-abort-${Date.now()}`)\n // Outlives the read on purpose: the journal must still be open, and the\n // agent still running, when the signal fires.\n const agent = await handle.process.spawn(\n journaledCommand(`printf '{\"a\":1}\\\\n'; sleep 30`, paths),\n )\n try {\n await waitForJournal(handle, paths)\n const seen: Array<string> = []\n // The signal fires ON the first line rather than on a stopwatch. The\n // old form was `AbortSignal.timeout(3_000)`, which asked the provider\n // to spawn a `tail` AND deliver a line inside 3s — measured failing on\n // a loaded Windows machine (git-bash `sh` + `tail`, `seen` came back\n // empty) while passing in isolation, the same load-sensitivity as the\n // `firstLineMs` bound the case above replaced. Aborting from inside the\n // stream keeps the property exactly: the agent is still running, the\n // journal is still open, and the reader must stop because its SIGNAL\n // said so.\n const stop = new AbortController()\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. It is not the assertion — `backstopped` below proves it\n // was not what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n signal: AbortSignal.any([stop.signal, backstop]),\n })) {\n seen.push(line.line)\n // No `break`, ever: the pre-fix reader honored a consumer break but\n // rode straight past its signal, so a `break` here would pass it.\n stop.abort()\n }\n expect({ seen, backstopped: backstop.aborted }).toEqual({\n seen: ['{\"a\":1}'],\n // The loop ended, and NOT because the backstop timed out — which is\n // the causal witness that \"the signal ends it at all\", with no clock\n // in the assertion.\n backstopped: false,\n })\n } finally {\n await agent.kill()\n }\n } finally {\n await dispose()\n }\n },\n )\n\n // THE CAPABILITY, not the reader's reaction to it.\n //\n // `killableProcesses` is the flag `journalReadStrategy` reads to choose\n // `'follow'`, and the promise it makes is about the SANDBOX-SIDE process:\n // `tail -f` may be spawned because the caller can reclaim it. Every other\n // case in this file asserts only that the READER stopped, which\n // `untilAborted` delivers unilaterally by abandoning the pipe — so all of\n // them pass a provider whose `kill()` is `() => Promise.resolve()`. This one\n // asks the sandbox itself.\n //\n // WHAT IT SPAWNS, AND WHY THAT SHAPE. The heartbeat loop is BACKGROUNDED\n // (`( … ) & wait`), so the long-lived work is a grandchild of the wrapper\n // shell rather than the wrapper itself. That is deliberate: it is the shape\n // of the defects this bites on. `sh -c '<cmd>'` does not reliably exec its\n // command, so a provider that signals only the wrapper leaves the real work\n // running — measured on local-process POSIX, where `sh -c 'sleep 987654321'`\n // survived `child.kill('SIGKILL')` — and Docker's `kill -SIG -\"$pid\"` group\n // form exists precisely so a backgrounded grandchild is not orphaned. A probe\n // that `exec`ed itself into the wrapper would be reclaimed by the correct and\n // the broken implementation alike, and prove nothing.\n //\n // WHY IT MEASURES WORK AND NOT EXISTENCE. `kill -0 <pid>` was the obvious\n // probe and it is WRONG here, measured: alpine's PID 1 under this provider is\n // `tail -f /dev/null`, which never `wait()`s, so a correctly killed child\n // lingers as an unreaped `[sleep]` forever and `kill -0` answers 0 for it.\n // That fails a healthy provider. `ps` is no better and is why the identity\n // here is a file rather than a nonce in the command line: on local-process\n // under Windows the shell is git-bash, whose MSYS `ps` prints only the process\n // IMAGE PATH (`/usr/bin/sleep`) and never argv — verified, `ps`, `ps -ef` and\n // `ps -W` all omit it — so `ps | grep <nonce>` would match nothing there, and\n // \"no match\" is indistinguishable from \"it is gone\": a vacuous pass on\n // exactly the provider whose kill was broken. (`pgrep` is worse: BusyBox has\n // it, git-bash does not.) A host-side census is not portable at all — Docker's\n // container-side process has no host process, and a remote provider has none\n // either.\n //\n // So the probe is a heartbeat: the process appends one byte per second to a\n // nonce-named file, through the shell. Only a RUNNING process can do that. A\n // zombie cannot, a killed process cannot, and no other test on the machine\n // writes to that path.\n //\n // BOTH OBSERVATIONS ARE ASSERTED, in one object, and the \"before\" one is not\n // decoration: a frozen-file check passes trivially against a file that never\n // grew at all, which is the exact failure shape this whole review keeps\n // turning up. `tickedBeforeKill` is what makes the detector live.\n itFollows(\n config,\n \"kills the sandbox-side process, not just the host's view of it\",\n async () => {\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n const heartbeat = noncePath('killprobe-hb')\n const stop = noncePath('killprobe-stop')\n try {\n expectDeclaredStrategy(handle, config)\n // The `stop` file is how a SURVIVOR is reclaimed in teardown, since a\n // provider that fails this case cannot be trusted to kill it and the\n // suite must not leak a spinner either way. The tick cap is the second\n // net, for a teardown that never ran at all.\n const probe = await handle.process.spawn(\n `( i=0; while [ ! -f ${stop} ] && [ $i -lt ${PROBE_MAX_TICKS} ]; do ` +\n `printf '.' >> ${heartbeat}; i=$((i+1)); sleep 1; ` +\n `done ) & wait`,\n )\n // Two bytes, not one: one byte is \"it started\", two is \"it is looping\".\n const tickedBeforeKill = await waitForTicks(handle, heartbeat, 2)\n\n await probe.kill()\n await sleep(KILL_SETTLE_MS)\n const atSettle = await fileSize(handle, heartbeat)\n await sleep(HEARTBEAT_QUIET_MS)\n const afterQuietWindow = await fileSize(handle, heartbeat)\n\n expect({\n tickedBeforeKill,\n // An UNREADABLE sample counts as a tick, i.e. fails: the file was\n // provably readable a moment ago (`tickedBeforeKill`), so `null` here\n // means the probe itself broke and the quiet window proves nothing.\n // Two `null`s compare equal, which would otherwise read as \"frozen\".\n tickedAfterKill:\n atSettle === null ||\n afterQuietWindow === null ||\n atSettle !== afterQuietWindow,\n }).toEqual({ tickedBeforeKill: true, tickedAfterKill: false })\n } finally {\n await handle.process\n .exec(`: >> ${stop}; rm -f ${heartbeat}`)\n .catch(() => undefined)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"journal-conformance.js","names":[],"sources":["../../../src/testkit/journal-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for the agent output journal.\n *\n * The journal design rests on two provider-level claims: a command string is\n * framed through a POSIX shell (so `>>` redirection works), and `tail -c +N -f`\n * is available. Both are asserted here against a real sandbox rather than\n * assumed from the audit.\n *\n * A provider that cannot satisfy them MUST declare `unsupported.reason`. There\n * is deliberately no silent-skip path: a conformance case that quietly returns\n * prints as a pass, which is how an unimplemented capability ships green. The\n * three FOLLOW cases obey the same rule through a second declaration,\n * {@link JournalConformanceConfig.followUnsupported} — see {@link itFollows} for\n * why the strategy has to be declared rather than detected at registration time,\n * and {@link expectDeclaredStrategy} for what keeps the declaration honest.\n *\n * THE THIRD FOLLOW CASE TESTS THE OTHER SIDE OF THE BOUNDARY, and it is here\n * because the first two do not. `killableProcesses` is what selects `'follow'`\n * over `'poll'`, and a wrong `true` means `tail -f` is spawned on the assumption\n * it can be reclaimed — leaking one follower per run when it cannot. The two\n * follow cases only ever asserted that the READER stops, which\n * `journal-reader.ts`'s `untilAborted` guarantees on its own by abandoning the\n * pipe the moment the signal fires. So both of them pass a provider whose\n * `kill()` is `() => Promise.resolve()`, and three of the four `true`\n * declarations in this repo were in fact false: Docker's `stream.destroy()` only\n * detached the client, local-process's `sh -c` forks so signalling the shell left\n * the command alive, and Vercel's `kill()` never called the SDK's real\n * `Command.kill` at all. Every one of them shipped green through this suite.\n * \"kills the sandbox-side process, not just the host's view of it\" is the case\n * that fails them — see its own comment for how it probes.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { randomUUID } from 'node:crypto'\nimport { describe, expect, it } from 'vitest'\nimport {\n exitSentinelLine,\n journalExistsCommand,\n journalPaths,\n journalReadCommand,\n journaledCommand,\n} from '../journal'\nimport { journalReadStrategy, readJournal } from '../journal-reader'\nimport type { JournalPaths } from '../journal'\nimport type { SandboxHandle } from '../contracts'\n\nexport interface JournalConformanceConfig {\n /** Provider name, used in the describe title. */\n name: string\n /** Create a live sandbox plus its teardown. */\n createHandle: () => Promise<{\n handle: SandboxHandle\n dispose: () => Promise<void>\n }>\n /**\n * Declare that this provider cannot journal, with the reason. Registers a\n * skipped case whose title carries the reason. Omit it and the suite runs.\n */\n unsupported?: { reason: string }\n /**\n * Declare that this provider's reads take the POLL strategy rather than the\n * FOLLOW one — i.e. `journalReadStrategy` answers `'poll'` for its handles,\n * because it lacks `backgroundProcesses` or `killableProcesses`. The two follow\n * cases then register as NAMED skips carrying the reason.\n *\n * Declare this ONLY when the provider really cannot follow. It is checked\n * against a live handle in a case that always runs\n * ({@link expectDeclaredStrategy}), so a wrong declaration fails the suite in\n * either direction rather than quietly removing coverage.\n */\n followUnsupported?: { reason: string }\n}\n\n/**\n * Per-case timeout. Every case here spawns a real sandbox and a real agent.\n *\n * 180s, not the 60s this used to be, and it matches the ceiling\n * `takeover-conformance.ts` already gives its heaviest cases. It is the one\n * wall-clock number left in the file and it is deliberately far outside the range\n * any healthy run needs: a case here makes half a dozen provider round-trips, and\n * ONE `docker exec` on a loaded daemon has been measured at 9.6s (see\n * `takeover-conformance.ts`'s `countingExec`) and at 20–45s on a saturated one, so\n * a 60s budget put the timeout itself in the same load-sensitive class as the\n * assertions that were removed from these cases — measured going red on cases that\n * pass in 7–13s each on a quiet machine.\n *\n * This bound exists only so a genuine hang FAILS instead of parking CI; it is not\n * an assertion about speed, and nothing here should be tuned to sit near it.\n */\nconst CASE_TIMEOUT_MS = 180_000\n\n/**\n * Register a case that only means anything on a provider whose reads FOLLOW.\n *\n * `journalReadStrategy` needs a live handle and a live handle needs the async\n * `createHandle`, so the strategy is not knowable when the cases are registered.\n * It is therefore DECLARED, and the declaration selects `it` or `it.skip` here.\n *\n * This exists because the alternative — checking the strategy inside the case and\n * returning early — is the silent-skip the module doc forbids. Such a case prints\n * `✓` with a duration and a title claiming a property was verified while every\n * real assertion in it (including the incremental-delivery handshake, which is\n * the entire reason the follow path exists) was skipped. A named `it.skip` prints\n * `↓` with the reason instead.\n */\nfunction itFollows(\n config: JournalConformanceConfig,\n title: string,\n fn: () => Promise<void>,\n): void {\n const unsupported = config.followUnsupported\n if (unsupported === undefined) {\n it(title, fn, CASE_TIMEOUT_MS)\n return\n }\n it.skip(\n `${title} — follow strategy unsupported: ${unsupported.reason}`,\n fn,\n CASE_TIMEOUT_MS,\n )\n}\n\n/**\n * Assert the live handle's read strategy is the one the config DECLARED.\n *\n * BOTH directions are defects, and neither is a skip. A provider that declared\n * `followUnsupported` but whose handles do follow silently loses the two cases it\n * could pass. One that declared nothing but polls would reach the follow\n * assertions and fail them for a reason unrelated to journaling — which is what\n * the previous `expect(handle.capabilities.killableProcesses).toBe(false)` branch\n * did to a provider with `backgroundProcesses: false, killableProcesses: true`.\n * Either way the config does not describe the provider, and that is worth\n * failing.\n */\nfunction expectDeclaredStrategy(\n handle: SandboxHandle,\n config: JournalConformanceConfig,\n): void {\n expect(journalReadStrategy(handle)).toBe(\n config.followUnsupported === undefined ? 'follow' : 'poll',\n )\n}\n\n/** Decode the base64 frame a journal read command produces into raw text. */\nfunction decodeJournalRead(stdout: string): string {\n return Buffer.from(stdout.replace(/\\s+/g, ''), 'base64').toString('utf8')\n}\n\n/**\n * Block until the run's journal file exists in the sandbox.\n *\n * Through the shell (`journalExistsCommand`), never `handle.fs.exists` — see\n * rule 3 in `../journal.ts`: on local-process the two resolve `/tmp`\n * differently, so an `fs` probe would report the wrong file.\n *\n * Exported for `./reaper-conformance.ts`, which needs the same bounded,\n * shell-only wait before probing a still-producing run. Internal to the testkit;\n * not part of the `./testkit` public surface.\n */\nexport async function waitForJournal(\n handle: SandboxHandle,\n paths: JournalPaths,\n): Promise<void> {\n const deadline = Date.now() + 15_000\n for (;;) {\n const probe = await handle.process.exec(journalExistsCommand(paths))\n if (probe.exitCode === 0) return\n if (Date.now() > deadline) {\n throw new Error(`journal conformance: ${paths.journal} never appeared`)\n }\n await sleep(100)\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * An absolute path inside the sandbox that no other case, suite, or machine will\n * touch.\n *\n * Every character is in `[A-Za-z0-9./-]`, so these interpolate into the shell\n * commands below as a single word without quoting. `/tmp` and not the workspace:\n * on local-process a shell redirect reaches the host's real `/tmp` while\n * `handle.fs` resolves under the sandbox root (see rule 3 in `../journal.ts`),\n * and everything here is written AND read through the shell so the two never have\n * to agree.\n */\nfunction noncePath(label: string): string {\n return `/tmp/tanstack-journal-conformance-${label}-${randomUUID()}`\n}\n\n/** Iteration cap on the kill probe's loop, so nothing can outlive the suite. */\nconst PROBE_MAX_TICKS = 600\n\n/**\n * Bound on a journal read, so a reader that delivers nothing FAILS instead of\n * parking CI.\n *\n * Never an assertion, and deliberately far above anything a healthy read needs\n * (measured: 10–18s for the follow cases on both providers). Each case that uses\n * it proves its property some other way — a causal handshake, or\n * `backstop.aborted` — so this number can be raised freely and must never be the\n * thing a case is tuned against.\n */\nconst READ_BACKSTOP_MS = 90_000\n\n/**\n * How long to let an asynchronous kill land before the quiet window opens.\n *\n * A kill is asynchronous on every provider here — Docker signals through a\n * second `exec`, local-process signals a process group and lets the OS reap — so\n * one more heartbeat tick immediately after `kill()` resolves is not a survivor.\n */\nconst KILL_SETTLE_MS = 5_000\n\n/**\n * The quiet window: how long the heartbeat must stay frozen.\n *\n * This is NOT a load-sensitive bound, and the asymmetry is the point. A dead\n * process can never write again, so a slow or busy machine can only make this\n * window MORE reliable, never less — unlike a \"must happen within Nms\" ceiling,\n * which fails on load. Only a live survivor can end this window, and a live\n * survivor writes once a second.\n */\nconst HEARTBEAT_QUIET_MS = 6_000\n\n/**\n * Byte count of `path`, according to the SANDBOX'S OWN shell, or `null` when it\n * cannot be read.\n *\n * `wc -c` through the shell, never `handle.fs`: on local-process the two resolve\n * `/tmp` differently (rule 3), so an `fs` probe would answer about a file the\n * sandbox never wrote and the growth below would look frozen from the first\n * sample — a vacuous pass. Parsed strictly rather than coerced, so a shell\n * diagnostic cannot become `NaN` and compare unequal to itself.\n */\nasync function fileSize(\n handle: SandboxHandle,\n path: string,\n): Promise<number | null> {\n const probe = await handle.process.exec(`wc -c < ${path} 2>/dev/null`)\n const text = probe.stdout.trim()\n return /^\\d+$/.test(text) ? Number(text) : null\n}\n\n/**\n * Wait until `path` has grown to at least `bytes`, i.e. the probe process is\n * provably DOING WORK inside the sandbox, and answer whether it got there.\n *\n * Returning the observation rather than throwing keeps the verdict inside the\n * case's own `expect`: this is the \"before\" half of the assertion, and it is what\n * makes the \"after\" half a live detector instead of a formality.\n */\nasync function waitForTicks(\n handle: SandboxHandle,\n path: string,\n bytes: number,\n): Promise<boolean> {\n const deadline = Date.now() + 30_000\n for (;;) {\n const size = await fileSize(handle, path)\n if (size !== null && size >= bytes) return true\n if (Date.now() > deadline) return false\n // Matched to the heartbeat's own 1s period on purpose. Every poll is a\n // provider round-trip, so a 250ms interval spent three of them per tick it\n // could not possibly observe — pure pressure on the very `exec` path the rest\n // of the case depends on.\n await sleep(1_000)\n }\n}\n\n/**\n * Assert `createHandle` satisfies the journal conformance contract. Each `it`\n * gets a fresh sandbox via `createHandle`/`dispose`, so implementations may\n * share process state across calls without cross-test bleed only if\n * `createHandle` returns an isolated sandbox.\n */\nexport function runJournalConformance(config: JournalConformanceConfig): void {\n describe(`journal conformance — ${config.name}`, () => {\n if (config.unsupported) {\n it.skip(`unsupported: ${config.unsupported.reason}`, () => {\n expect(true).toBe(true)\n })\n return\n }\n\n it(\n \"redirects a command's stdout into the journal and appends the exit sentinel\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n // Checked HERE, in a case that always runs, because\n // `followUnsupported` gates the two follow cases below: a declaration\n // that does not match the live handle must fail the suite rather than\n // remove coverage from it. This is the only place a `poll` declaration\n // can be caught, since the cases it skips never execute.\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-${Date.now()}`)\n const command = journaledCommand(\n `printf '{\"a\":1}\\\\n{\"b\":2}\\\\n'`,\n paths,\n )\n const proc = await handle.process.spawn(command)\n expect(await proc.wait()).toBe(0)\n\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`{\"a\":1}\\n{\"b\":2}\\n${exitSentinelLine(paths, 0)}\\n`)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n \"records the agent's non-zero exit in the sentinel\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-exit-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand('exit 7', paths),\n )\n await proc.wait()\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`${exitSentinelLine(paths, 7)}\\n`)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n \"keeps the agent's stderr out of the journal\",\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-err-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand(\n `printf '{\"a\":1}\\\\n'; printf 'a warning\\\\n' 1>&2`,\n paths,\n ),\n )\n await proc.wait()\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n expect(text).toBe(`{\"a\":1}\\n${exitSentinelLine(paths, 0)}\\n`)\n expect(text).not.toContain('a warning')\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n it(\n 'reads incrementally from a byte offset with absolute positions',\n async () => {\n const { handle, dispose } = await config.createHandle()\n try {\n const paths = journalPaths(`conf-seek-${Date.now()}`)\n const proc = await handle.process.spawn(\n journaledCommand(`printf '{\"a\":1}\\\\n{\"b\":2}\\\\n'`, paths),\n )\n await proc.wait()\n\n const all = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n strategy: 'poll',\n pollIntervalMs: 0,\n // Not an assertion — see {@link READ_BACKSTOP_MS}. This was\n // `AbortSignal.timeout(5_000)`, and it is a POLL read, so it costs one\n // provider round-trip per line: measured going red on a saturated\n // Docker daemon where a single `exec` took ~20s, while the lines it\n // asserts were perfectly correct.\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n all.push(line)\n if (all.length === 3) break\n }\n expect(all.map((l) => l.line)).toEqual([\n '{\"a\":1}',\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n\n const resumed = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: all[0]?.endPosition ?? 0,\n strategy: 'poll',\n pollIntervalMs: 0,\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n resumed.push(line)\n if (resumed.length === 2) break\n }\n expect(resumed.map((l) => l.line)).toEqual([\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n expect(resumed[0]?.endPosition).toBe(all[1]?.endPosition)\n } finally {\n await dispose()\n }\n },\n CASE_TIMEOUT_MS,\n )\n\n // This case is the reason `journalFollowCommand` pipes into nothing.\n // `tail -f journal | base64` delivers ZERO bytes while the agent is still\n // running — measured on GNU coreutils 8.32 `base64` and on busybox 1.36.1\n // `base64` in Alpine — because the encoder buffers its stdout until its\n // stdin closes, which only happens when the reader kills `tail`.\n //\n // INCREMENTAL, NOT MERELY EVENTUAL, AND PROVED CAUSALLY RATHER THAN BY A\n // STOPWATCH. The agent writes its first line and then BLOCKS on a gate file\n // that only this reader can create, and it creates it only on receiving that\n // first line. So the agent cannot reach its second line until the first was\n // delivered — receiving `{\"b\":2}` at all IS the proof of incremental\n // delivery, and the `toEqual` below is the whole assertion. A buffering\n // filter reintroduced onto the follow path deadlocks instead: nothing is\n // delivered, the gate is never created, the agent never writes its second\n // line, the read ends on its signal and `seen` is empty.\n //\n // This replaced `expect(firstLineMs).toBeLessThan(3_000)`, which measured\n // MACHINE LOAD as much as behavior: it was observed failing in whole-suite\n // fleet runs while passing 5/5 in isolation, because one `exec`/`spawn` is a\n // provider round-trip whose latency this suite does not control (a\n // `docker exec` on a loaded daemon has been measured at 9.6s). Same instinct\n // as `takeover-conformance.ts`'s `countingExec`: anchor on the property, not\n // on the clock. A bound that goes red on a busy machine teaches people to\n // ignore the suite. Do NOT \"fix\" a failure here by widening a window —\n // there is no window left to widen.\n itFollows(\n config,\n 'follows a journal that is still being written, delivering each line before the next is produced',\n async () => {\n // Every assertion below sits after an `await`, so a case that threw its way\n // out of the loop early would report an unrelated failure; this one reports\n // \"nothing was asserted\", which is the failure this case used to HIDE.\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n const gate = noncePath('follow-gate')\n try {\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-follow-${Date.now()}`)\n // The wait is bounded in the SANDBOX too, and on timeout it emits a\n // line that names what went wrong instead of the expected one — so a\n // gate that never arrives fails the `toEqual` with `{\"gate\":\"never\"}`\n // rather than eventually satisfying it. 30 ticks so that diagnostic\n // lands INSIDE `CASE_TIMEOUT_MS`; a longer cap would just time the case\n // out and lose the message.\n const agentCommand =\n `printf '{\"a\":1}\\\\n'; ` +\n `i=0; while [ ! -f ${gate} ]; do ` +\n `i=$((i+1)); ` +\n `if [ $i -gt 30 ]; then printf '{\"gate\":\"never\"}\\\\n'; break; fi; ` +\n `sleep 1; done; ` +\n `printf '{\"b\":2}\\\\n'`\n // Not awaited anywhere: reading the `__exit` sentinel below IS the\n // proof it finished. (`SpawnHandle.wait()` is not safe to call after the\n // fact on every provider — local-process registers a `close` listener at\n // call time, so a `wait()` issued after the process already exited never\n // resolves.)\n void handle.process.spawn(journaledCommand(agentCommand, paths))\n // `tail` on a file that does not exist yet exits immediately, and the\n // agent's spawn and the reader's spawn race. Waiting removes that race\n // WITHOUT touching the property under test: with a buffering filter on\n // the follow path the journal still exists, `tail` still runs, and the\n // reader still receives nothing.\n await waitForJournal(handle, paths)\n // The premise, pinned: the gate is genuinely absent, so the agent\n // really is blocked and its second line really is downstream of this\n // reader. Without this a pre-existing gate path would make the case\n // pass without following anything.\n expect(\n (await handle.process.exec(`test -e ${gate}`)).exitCode,\n ).not.toBe(0)\n const seen: Array<string> = []\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n // Not the assertion — see {@link READ_BACKSTOP_MS}. The gate's own\n // 30-tick cap fires well inside it, so the `{\"gate\":\"never\"}`\n // diagnostic still reaches this reader.\n signal: AbortSignal.timeout(READ_BACKSTOP_MS),\n })) {\n seen.push(line.line)\n // Releases the agent, and only from inside the stream. `touch` is\n // not portable to every BusyBox build with these flags, so this\n // creates the file with a redirect, through the shell — `handle.fs`\n // would write a path the agent's `test -f` cannot see on\n // local-process (rule 3).\n if (seen.length === 1) {\n await handle.process.exec(`: >> ${gate}`)\n }\n if (seen.length === 3) break\n }\n expect(seen).toEqual([\n '{\"a\":1}',\n '{\"b\":2}',\n exitSentinelLine(paths, 0),\n ])\n } finally {\n // Unblocks the agent even when the case failed, so no `while` loop\n // outlives it on a provider whose sandbox teardown does not reap.\n await handle.process.exec(`: >> ${gate}`).catch(() => undefined)\n await dispose()\n }\n },\n )\n\n // The follow read must obey its own AbortSignal rather than waiting for the\n // provider's `kill` to close the stream — a provider whose kill misses a\n // grandchild would otherwise hang the reader past its deadline.\n itFollows(\n config,\n 'stops a follow read when its signal aborts, without a consumer break',\n async () => {\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n try {\n expectDeclaredStrategy(handle, config)\n const paths = journalPaths(`conf-abort-${Date.now()}`)\n // Outlives the read on purpose: the journal must still be open, and the\n // agent still running, when the signal fires.\n const agent = await handle.process.spawn(\n journaledCommand(`printf '{\"a\":1}\\\\n'; sleep 30`, paths),\n )\n try {\n await waitForJournal(handle, paths)\n const seen: Array<string> = []\n // The signal fires ON the first line rather than on a stopwatch. The\n // old form was `AbortSignal.timeout(3_000)`, which asked the provider\n // to spawn a `tail` AND deliver a line inside 3s — measured failing on\n // a loaded Windows machine (git-bash `sh` + `tail`, `seen` came back\n // empty) while passing in isolation, the same load-sensitivity as the\n // `firstLineMs` bound the case above replaced. Aborting from inside the\n // stream keeps the property exactly: the agent is still running, the\n // journal is still open, and the reader must stop because its SIGNAL\n // said so.\n const stop = new AbortController()\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. It is not the assertion — `backstopped` below proves it\n // was not what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n signal: AbortSignal.any([stop.signal, backstop]),\n })) {\n seen.push(line.line)\n // No `break`, ever: the pre-fix reader honored a consumer break but\n // rode straight past its signal, so a `break` here would pass it.\n stop.abort()\n }\n expect({ seen, backstopped: backstop.aborted }).toEqual({\n seen: ['{\"a\":1}'],\n // The loop ended, and NOT because the backstop timed out — which is\n // the causal witness that \"the signal ends it at all\", with no clock\n // in the assertion.\n backstopped: false,\n })\n } finally {\n await agent.kill()\n }\n } finally {\n await dispose()\n }\n },\n )\n\n // THE CAPABILITY, not the reader's reaction to it.\n //\n // `killableProcesses` is the flag `journalReadStrategy` reads to choose\n // `'follow'`, and the promise it makes is about the SANDBOX-SIDE process:\n // `tail -f` may be spawned because the caller can reclaim it. Every other\n // case in this file asserts only that the READER stopped, which\n // `untilAborted` delivers unilaterally by abandoning the pipe — so all of\n // them pass a provider whose `kill()` is `() => Promise.resolve()`. This one\n // asks the sandbox itself.\n //\n // WHAT IT SPAWNS, AND WHY THAT SHAPE. The heartbeat loop is BACKGROUNDED\n // (`( … ) & wait`), so the long-lived work is a grandchild of the wrapper\n // shell rather than the wrapper itself. That is deliberate: it is the shape\n // of the defects this bites on. `sh -c '<cmd>'` does not reliably exec its\n // command, so a provider that signals only the wrapper leaves the real work\n // running — measured on local-process POSIX, where `sh -c 'sleep 987654321'`\n // survived `child.kill('SIGKILL')` — and Docker's `kill -SIG -\"$pid\"` group\n // form exists precisely so a backgrounded grandchild is not orphaned. A probe\n // that `exec`ed itself into the wrapper would be reclaimed by the correct and\n // the broken implementation alike, and prove nothing.\n //\n // WHY IT MEASURES WORK AND NOT EXISTENCE. `kill -0 <pid>` was the obvious\n // probe and it is WRONG here, measured: alpine's PID 1 under this provider is\n // `tail -f /dev/null`, which never `wait()`s, so a correctly killed child\n // lingers as an unreaped `[sleep]` forever and `kill -0` answers 0 for it.\n // That fails a healthy provider. `ps` is no better and is why the identity\n // here is a file rather than a nonce in the command line: on local-process\n // under Windows the shell is git-bash, whose MSYS `ps` prints only the process\n // IMAGE PATH (`/usr/bin/sleep`) and never argv — verified, `ps`, `ps -ef` and\n // `ps -W` all omit it — so `ps | grep <nonce>` would match nothing there, and\n // \"no match\" is indistinguishable from \"it is gone\": a vacuous pass on\n // exactly the provider whose kill was broken. (`pgrep` is worse: BusyBox has\n // it, git-bash does not.) A host-side census is not portable at all — Docker's\n // container-side process has no host process, and a remote provider has none\n // either.\n //\n // So the probe is a heartbeat: the process appends one byte per second to a\n // nonce-named file, through the shell. Only a RUNNING process can do that. A\n // zombie cannot, a killed process cannot, and no other test on the machine\n // writes to that path.\n //\n // BOTH OBSERVATIONS ARE ASSERTED, in one object, and the \"before\" one is not\n // decoration: a frozen-file check passes trivially against a file that never\n // grew at all, which is the exact failure shape this whole review keeps\n // turning up. `tickedBeforeKill` is what makes the detector live.\n itFollows(\n config,\n \"kills the sandbox-side process, not just the host's view of it\",\n async () => {\n expect.hasAssertions()\n const { handle, dispose } = await config.createHandle()\n const heartbeat = noncePath('killprobe-hb')\n const stop = noncePath('killprobe-stop')\n try {\n expectDeclaredStrategy(handle, config)\n // The `stop` file is how a SURVIVOR is reclaimed in teardown, since a\n // provider that fails this case cannot be trusted to kill it and the\n // suite must not leak a spinner either way. The tick cap is the second\n // net, for a teardown that never ran at all.\n const probe = await handle.process.spawn(\n `( i=0; while [ ! -f ${stop} ] && [ $i -lt ${PROBE_MAX_TICKS} ]; do ` +\n `printf '.' >> ${heartbeat}; i=$((i+1)); sleep 1; ` +\n `done ) & wait`,\n )\n // Two bytes, not one: one byte is \"it started\", two is \"it is looping\".\n const tickedBeforeKill = await waitForTicks(handle, heartbeat, 2)\n\n await probe.kill()\n await sleep(KILL_SETTLE_MS)\n const atSettle = await fileSize(handle, heartbeat)\n await sleep(HEARTBEAT_QUIET_MS)\n const afterQuietWindow = await fileSize(handle, heartbeat)\n\n expect({\n tickedBeforeKill,\n // An UNREADABLE sample counts as a tick, i.e. fails: the file was\n // provably readable a moment ago (`tickedBeforeKill`), so `null` here\n // means the probe itself broke and the quiet window proves nothing.\n // Two `null`s compare equal, which would otherwise read as \"frozen\".\n tickedAfterKill:\n atSettle === null ||\n afterQuietWindow === null ||\n atSettle !== afterQuietWindow,\n }).toEqual({ tickedBeforeKill: true, tickedAfterKill: false })\n } finally {\n await handle.process\n .exec(`: >> ${stop}; rm -f ${heartbeat}`)\n .catch(() => undefined)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
@@ -112,7 +112,7 @@ var FENCE_QUIET_MS = 25;
112
112
  */
113
113
  var READ_BACKSTOP_MS = 9e4;
114
114
  /** Long enough that nothing in this suite is ever classified as expired. */
115
- var NEVER_EXPIRES_MS = 3600 * 1e3;
115
+ var NEVER_EXPIRES_MS = 36e5;
116
116
  /**
117
117
  * A journal directory nothing else on the machine writes to, created fresh for
118
118
  * EVERY case.
@@ -241,7 +241,7 @@ function conformanceLog() {
241
241
  });
242
242
  return offset;
243
243
  })),
244
- read: () => void 0,
244
+ read: () => (async function* empty() {})(),
245
245
  close: () => {
246
246
  closes += 1;
247
247
  return Promise.resolve();
@@ -439,11 +439,12 @@ function runReaperConformance(config) {
439
439
  status: "completed",
440
440
  finishedAt: Date.now()
441
441
  });
442
- expect((await pruneJournals({
442
+ const first = await pruneJournals({
443
443
  handle,
444
444
  runs,
445
445
  dir
446
- })).deleted).toEqual([runId]);
446
+ });
447
+ expect(first.deleted).toEqual([runId]);
447
448
  const second = await pruneJournals({
448
449
  handle,
449
450
  runs,
@@ -462,7 +463,8 @@ function runReaperConformance(config) {
462
463
  kept: [],
463
464
  failures: []
464
465
  });
465
- expect((await handle.process.exec(journalCleanupCommand(paths))).exitCode).toBe(0);
466
+ const rerun = await handle.process.exec(journalCleanupCommand(paths));
467
+ expect(rerun.exitCode).toBe(0);
466
468
  } finally {
467
469
  await removeDir(handle, dir);
468
470
  await dispose();
@@ -516,7 +518,8 @@ function runReaperConformance(config) {
516
518
  it("emits stat's self-witness line for a populated directory, so the age gate is usable", { timeout: 6e4 }, async () => {
517
519
  const { handle, dispose } = await config.createHandle();
518
520
  const dir = caseDir();
519
- const paths = journalPaths(uniqueRunId("witness"), dir);
521
+ const runId = uniqueRunId("witness");
522
+ const paths = journalPaths(runId, dir);
520
523
  try {
521
524
  await runAgent(handle, paths, ["1"]);
522
525
  const listing = await mtimeListing(handle, dir);
@@ -800,9 +803,11 @@ function runReaperConformance(config) {
800
803
  expect(followed).toEqual(["{\"delta\":\"1\"}", exitSentinelLine(paths, 0)]);
801
804
  expect(await fileExists(handle, canary)).toBe(false);
802
805
  }
803
- expect(decodeJournalRead((await handle.process.exec(journalStderrReadCommand(paths))).stdout)).toBe("boom\n");
806
+ const sidecar = await handle.process.exec(journalStderrReadCommand(paths));
807
+ expect(decodeJournalRead(sidecar.stdout)).toBe("boom\n");
804
808
  expect(await fileExists(handle, canary)).toBe(false);
805
- expect((await listNames(handle, dir)).sort()).toEqual([journalName, basename(dir, paths.stderr)].sort());
809
+ const names = await listNames(handle, dir);
810
+ expect(names.sort()).toEqual([journalName, basename(dir, paths.stderr)].sort());
806
811
  expect(decodeJournalRunId(journalName)).toEqual({
807
812
  kind: "runId",
808
813
  runId
@@ -1 +1 @@
1
- {"version":3,"file":"reaper-conformance.js","names":[],"sources":["../../../src/testkit/reaper-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for the two unattended sweeps: `pruneJournals`\n * (`journal-sweep.ts`) and `reapDetachedRuns` (`reap.ts`), against a REAL\n * sandbox.\n *\n * WHY THIS EXISTS SEPARATELY FROM THE UNIT TESTS. Both sweeps are almost\n * entirely *shell* — `ls -1`, `stat -c '%Y %n'`, `rm -f`, `tail -c -N | base64`\n * — composed as strings by `journal.ts` and executed by a provider. The unit\n * suites drive fakes: an `exec` that answers from a scripted table, a\n * filesystem that is a `Map`. A fake cannot be wrong about `stat` the way a\n * BusyBox actually is, and on this feature that gap has already produced four\n * defects that every unit test passed (see `takeover-conformance.ts`'s module\n * doc for the roster). So the four properties the sweeps rest on are asserted\n * here through a real shell against real files:\n *\n * 1. **A deletion really deletes, and a keep really keeps.** Asserted with\n * `test -f` through the provider's shell, NEVER `handle.fs.exists`: on\n * local-process the two resolve `/tmp` differently, so an `fs` probe answers\n * about a path the journal was never written to (`journal.ts` rule 3). A\n * sweep that \"succeeded\" while deleting nothing passes an `fs` probe.\n * 2. **The age gate's self-witness works on THIS shell.** `journalMtimeListCommand`\n * passes the directory as `stat`'s own first operand precisely because\n * BusyBox exits 1 with EMPTY stdout on an unrecognised flag, and an empty\n * parse read as an empty directory would delete every live run's journal. The\n * docker provider's image is `alpine:3` — BusyBox 1.37, where `find -newermt`\n * and `find -printf` are unrecognised — so the docker matrix is the authority\n * on this case, not the local-process one (on Windows local-process execs\n * through git-bash, whose `find`/`stat` are GNU-flavoured).\n * 3. **The reaper never drives a live run.** The `'producing'` case asserts\n * ABSENCE — nothing appended, `close()` not called, not one `runs.update`,\n * `detachedSince` intact — because that is the shape of the defect\n * `probeRunExit` exists to prevent: entering `pipeToRunLog` to \"check\" writes\n * a terminal status and drops the run out of `listReclaimable` forever.\n * 4. **A shell-hostile runId cannot become a shell-hostile command.** The encode\n * → journal → follow → `ls` → decode → `rm` round trip runs on a runId\n * containing `/`, a space, `;`, `$( )` and an embedded `touch`, with a canary\n * file asserted absent. An ENCODING bug here is arbitrary command execution\n * inside the sandbox, not a cosmetic defect.\n *\n * **What the canary proves, exactly, and what it does not.** It detects a\n * runId reaching the shell WITHOUT `encodeRunId` — that is the mutation it\n * bites on, and it bites hard: `journaledCommand`, `journalFollowCommand`,\n * `journalExitProbeCommand`, `journalStderrReadCommand` and\n * `journalCleanupCommand` all interpolate the path, so the `;touch` executes\n * and the canary appears. It is BLIND to the loss of `journal.ts`'s\n * `shellQuote`, the second and independent layer. Measured: with `shellQuote`\n * reduced to the identity while `encodeRunId` stays, the redirect target\n * becomes `>> /tmp/…/rp-a_3btouch_20_2ftmp…ndjson` — a single shell word of\n * `[A-Za-z0-9._/-]`, because the encoder already removed every character a\n * shell can act on — so no canary fires and NOTHING in this suite, or in any\n * other real-provider suite, changes. Do not read a green run here as licence\n * to \"simplify\" `shellQuote` away.\n *\n * The quoting is pinned instead by exact-string unit tests in\n * `packages/ai-sandbox/tests/journal.test.ts`, which compare each composed\n * command to a literal containing the quotes. By name, one per command:\n * `journaledCommand` — \"redirects stdout to the journal, stderr to its own\n * file, and appends the exit sentinel\" plus \"quotes an adversarial runId so it\n * cannot inject shell metacharacters\"; `journalFollowCommand` — \"translates a\n * 0-based consumed-byte count into tail -c +N (1-based)\";\n * `journalReadCommand` — \"the bounded read drops -f and keeps the base64\n * frame, so a poll cannot hang\"; `journalExistsCommand` — \"probes through the\n * shell, never through fs.*\"; `journalStderrReadCommand` — \"reads a BOUNDED\n * tail of the sidecar, base64-framed, stderr silenced\";\n * `journalCleanupCommand`, `journalMtimeListCommand` and\n * `journalExitProbeCommand` — the first `it` under each of their `describe`s.\n * Those are the tests that go red on a dropped `shellQuote`; keep them exact.\n *\n * A provider that cannot satisfy the contract MUST declare `unsupported.reason`.\n * As in the journal and takeover suites there is deliberately no silent-skip\n * path: a conformance case that quietly returns prints as a pass, which is how\n * an unimplemented capability ships green.\n *\n * EVERY WAIT IN THIS FILE IS BOUNDED, and every journal directory is unique per\n * case — see {@link caseDir}. This suite DELETES FILES, and\n * `DEFAULT_JOURNAL_DIR` is a fixed absolute path shared with every other test\n * and, on local-process, with a developer's real runs.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { randomUUID } from 'node:crypto'\nimport { describe, expect, it } from 'vitest'\nimport { EventType, InMemoryRunStore } from '@tanstack/ai'\nimport { InMemoryLockStore } from '@tanstack/ai/locks'\nimport {\n EXIT_SENTINEL_KEY,\n decodeJournalRunId,\n exitSentinelLine,\n journalCleanupCommand,\n journalExistsCommand,\n journalListCommand,\n journalMtimeListCommand,\n journalPaths,\n journalReadCommand,\n journalStderrReadCommand,\n journaledCommand,\n parseJournalMtimeListing,\n} from '../journal'\nimport { journalReadStrategy, readJournal } from '../journal-reader'\nimport { pruneJournals } from '../journal-sweep'\nimport { probeRunExit, reapDetachedRuns } from '../reap'\nimport { readJournalNdjson } from '../runner'\nimport { chunkFingerprint, createRunScopedIdGen } from '../chunk-identity'\nimport { waitForJournal } from './journal-conformance'\nimport type { JournalPaths } from '../journal'\nimport type { SandboxHandle } from '../contracts'\nimport type { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai'\n\nexport interface ReaperConformanceConfig {\n /** Provider name, used in the describe title. */\n name: string\n /** Create a live sandbox plus its teardown. */\n createHandle: () => Promise<{\n handle: SandboxHandle\n dispose: () => Promise<void>\n }>\n /**\n * Declare that this provider cannot support the sweeps, with the reason.\n * Registers a skipped case whose title carries the reason — a NAMED skip,\n * visible in the reporter. Omit it and the suite runs.\n */\n unsupported?: { reason: string }\n /**\n * Declare that this provider's reads take the POLL strategy rather than the\n * FOLLOW one — i.e. `journalReadStrategy` answers `'poll'` for its handles.\n *\n * Only the FOLLOW half of the shell-hostile-runId case depends on it, so this\n * does not skip a case; it names itself in that case's title and the follow\n * read is omitted. The declaration is checked against the live handle there, in\n * both directions, so it cannot quietly remove coverage from a provider that\n * can in fact follow.\n */\n followUnsupported?: { reason: string }\n}\n\n/** Poll interval handed to providers that cannot follow a growing file. */\nconst POLL_INTERVAL_MS = 50\n\n/**\n * Quiescence window for the reaper's first append. Short because the agent in\n * these cases has provably stopped (the suite waited for its sentinel) — the\n * gate still runs, it just does not need to wait 5s to observe nothing.\n */\nconst FENCE_QUIET_MS = 25\n\n/**\n * Bound on a real journal read, so a reader that delivers nothing FAILS instead\n * of parking CI.\n *\n * Never an assertion, and deliberately far above anything a healthy read needs\n * (measured: 10–18s for the follow cases on both providers). Every use site\n * pairs it with a `backstopped: false` witness, so a read the CLOCK ended fails\n * naming this backstop rather than as a downstream transcript mismatch — which\n * means this number can be raised freely and must never be the thing a case is\n * tuned against.\n */\nconst READ_BACKSTOP_MS = 90_000\n\n/** Long enough that nothing in this suite is ever classified as expired. */\nconst NEVER_EXPIRES_MS = 60 * 60 * 1000\n\n/**\n * A journal directory nothing else on the machine writes to, created fresh for\n * EVERY case.\n *\n * Not `DEFAULT_JOURNAL_DIR`, and not even one directory per suite. Both sweeps\n * under test enumerate a whole directory and then DELETE from it, so a shared\n * directory would let one case's leftovers become another's input — and on\n * local-process the sandbox shell shares the host's real `/tmp`, where\n * `DEFAULT_JOURNAL_DIR` holds a developer's actual runs.\n */\nfunction caseDir(): string {\n return `/tmp/tanstack-reaper-conformance-${randomUUID()}`\n}\n\n/**\n * Unique per run, and it must be: `journalPaths` derives the filename from the\n * runId and the journal is append-only, so a reused id appends BEHIND the\n * previous run's `{\"__exit\":N}` sentinel and the new run appears to emit nothing\n * at all (see `journal.ts`).\n */\nfunction uniqueRunId(label: string): string {\n return `rp-${label}-${randomUUID()}`\n}\n\n/**\n * Single-quote a shell word, POSIX-style — the same rule `journal.ts`'s private\n * `shellQuote` applies.\n *\n * Duplicated rather than exported from production code on purpose: this exists\n * only for this suite's `rm -rf` teardown, which is not a production operation\n * and must not become one by growing an export for it.\n */\nfunction quote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`\n}\n\n/** Remove a case's journal directory and everything in it. Best effort. */\nasync function removeDir(handle: SandboxHandle, dir: string): Promise<void> {\n try {\n await handle.process.exec(`rm -rf ${quote(dir)}`)\n } catch {\n // The sandbox may already be gone, and on docker it is about to be. Nothing\n // under test depends on the directory being absent afterwards — the cases\n // that DO assert deletion assert it directly, per file.\n }\n}\n\n/**\n * Does `path` exist, according to the SANDBOX'S SHELL?\n *\n * `journalExistsCommand` rather than `handle.fs.exists`, for any path and not\n * just a journal: `journal.ts` rule 3 — on local-process `fs.*` resolves `/tmp`\n * under the sandbox root while a shell redirect hits the host's real `/tmp`, so\n * an `fs` probe would answer about a different file and every deletion\n * assertion in this suite would pass vacuously.\n */\nasync function fileExists(\n handle: SandboxHandle,\n path: string,\n): Promise<boolean> {\n const probe = await handle.process.exec(\n // Only `journal` is read by the probe, and its parameter is typed\n // `Pick<JournalPaths, 'journal'>` for exactly this reason: an arbitrary path\n // has no run behind it, so there is no nonce or sidecar to invent.\n journalExistsCommand({ journal: path }),\n )\n return probe.exitCode === 0\n}\n\n/** Filename as `ls -1` reports it, for a path inside `dir`. */\nfunction basename(dir: string, path: string): string {\n return path.slice(dir.length + 1)\n}\n\n/**\n * A real agent: a shell command printing one NDJSON line per delta, then\n * exiting.\n *\n * `printf '%s\\n' a b c` reuses the format for every operand on GNU coreutils\n * and on BusyBox alike, so this needs no loop. The JSON contains only double\n * quotes, so it is safe inside the POSIX single-quoted words this builds.\n */\nfunction emitLines(deltas: Array<string>): string {\n return `printf '%s\\\\n' ${deltas.map((delta) => `'{\"delta\":\"${delta}\"}'`).join(' ')}`\n}\n\n/**\n * Run a journaled agent to completion, so the `{\"__exit\":N}` sentinel is in the\n * journal by the time this resolves.\n *\n * `exec`, not `spawn`: `exec` waits, and a bounded wait is the only kind this\n * suite allows. (`SpawnHandle.wait()` is also not safe to call after the fact on\n * every provider — see `journal-conformance.ts`.)\n */\nasync function runAgent(\n handle: SandboxHandle,\n paths: JournalPaths,\n deltas: Array<string>,\n): Promise<void> {\n await handle.process.exec(journaledCommand(emitLines(deltas), paths))\n}\n\n/** `ls -1` output as a list of names. */\nasync function listNames(\n handle: SandboxHandle,\n dir: string,\n): Promise<Array<string>> {\n const listing = await handle.process.exec(journalListCommand(dir))\n return listing.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line !== '')\n}\n\n/** Decode the base64 frame a bounded journal read produces. */\nfunction decodeJournalRead(stdout: string): string {\n return Buffer.from(stdout.replace(/\\s+/g, ''), 'base64').toString('utf8')\n}\n\n/**\n * The `stat -c '%Y %n'` listing for `dir`, plus the raw stdout so a case can\n * assert the WITNESS LINE itself rather than only its parsed consequence.\n */\nasync function mtimeListing(\n handle: SandboxHandle,\n dir: string,\n): Promise<{ stdout: string; entries: Map<string, number> }> {\n const probe = await handle.process.exec(journalMtimeListCommand(dir))\n const parsed = parseJournalMtimeListing(probe.stdout, dir)\n if (parsed.kind !== 'listed') {\n throw new Error(\n `reaper conformance: the mtime listing for ${dir} came back unavailable — ` +\n `stat -c '%Y %n' produced no witness line. stdout: ${JSON.stringify(probe.stdout)}`,\n )\n }\n return {\n stdout: probe.stdout,\n entries: new Map(\n parsed.entries.map((entry) => [entry.name, entry.mtimeMs]),\n ),\n }\n}\n\n/**\n * Is there a `<seconds> <dir>` line — `stat`'s report on its own first operand?\n *\n * That line, not the exit status, is the evidence the mechanism ran: BusyBox\n * exits 1 both for an EMPTY directory (whose unexpanded glob it cannot stat) and\n * for an unrecognised flag, and only the witness distinguishes them.\n */\nfunction hasWitnessLine(stdout: string, dir: string): boolean {\n return stdout\n .split('\\n')\n .some((line) => /^\\d+ (?<path>.+)$/.exec(line.trim())?.[1] === dir)\n}\n\n/** Read one file's mtime out of a listing, loudly when it is missing. */\nfunction mtimeOf(entries: Map<string, number>, name: string): number {\n const mtimeMs = entries.get(name)\n if (mtimeMs === undefined) {\n throw new Error(\n `reaper conformance: ${name} has no mtime in the stat listing, so the age gate cannot be exercised`,\n )\n }\n return mtimeMs\n}\n\n/**\n * An in-process event log with real accumulated state, plus the two facts the\n * reaper assertions need: what was appended, and how many times `close()` ran.\n *\n * `close()` is the load-bearing counter. `pipeToRunLog` ALWAYS calls it, so a\n * reaper that entered the pipe to find out whether a run finished would show up\n * here as `closes() === 1` — which ends every attached client's stream — even if\n * it happened to append nothing.\n */\ninterface ConformanceLog {\n log: StreamDurability\n stored: () => Array<StreamChunk>\n closes: () => number\n}\n\nfunction conformanceLog(): ConformanceLog {\n const entries: Array<{ offset: string; chunk: StreamChunk }> = []\n let closes = 0\n return {\n log: {\n resumeFrom: () => null,\n append: (chunks) =>\n Promise.resolve(\n chunks.map((chunk) => {\n const offset = `reap:${entries.length}`\n entries.push({ offset, chunk })\n return offset\n }),\n ),\n // Nothing here tails the log — every assertion reads the appended\n // transcript, and a `read` would park until `close()` (see `align.ts`).\n read: () => (async function* empty() {})(),\n close: () => {\n closes += 1\n return Promise.resolve()\n },\n snapshot: () => Promise.resolve(entries.map((entry) => ({ ...entry }))),\n },\n stored: () => entries.map((entry) => entry.chunk),\n closes: () => closes,\n }\n}\n\n/**\n * A `RunStore` that counts its MUTATIONS, so the leave-alone case can assert\n * that a producing run's record was not written at all.\n *\n * \"Status still `'running'`\" is too weak on its own: `driverEpoch` is bumped by\n * `withRunClaim` before any status is written, so a reaper that claimed a live\n * run and then bailed would still read as `'running'`. Counting `update` sees\n * that; reading the status does not.\n */\ninterface CountingRunStore {\n runs: RunStore\n updates: () => number\n}\n\nfunction countingRunStore(inner: InMemoryRunStore): CountingRunStore {\n let updates = 0\n return {\n runs: {\n createOrResume: (...args) => inner.createOrResume(...args),\n update: (...args) => {\n updates += 1\n return inner.update(...args)\n },\n get: (...args) => inner.get(...args),\n listByThread: (...args) => inner.listByThread(...args),\n listReclaimable: (...args) => inner.listReclaimable(...args),\n findActiveRun: (...args) => inner.findActiveRun(...args),\n },\n updates: () => updates,\n }\n}\n\n/** The event a journal line translates into. `timestamp` is excluded from `chunkFingerprint`. */\nfunction contentChunk(messageId: string, delta: string): StreamChunk {\n return {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n delta,\n timestamp: Date.now(),\n }\n}\n\n/**\n * Narrow one parsed journal line into its chunk.\n *\n * Fields are validated and the chunk REBUILT from them rather than asserted into\n * shape: a cast would let a provider that mangles the bytes reach\n * `chunkFingerprint` as a structurally invalid chunk and fail somewhere\n * unrelated.\n */\nfunction toChunk(\n runId: string,\n messageId: string,\n value: unknown,\n): StreamChunk {\n if (typeof value !== 'object' || value === null || !('delta' in value)) {\n throw new Error(\n `reaper conformance: run ${runId} journal line is not an agent event: ${JSON.stringify(value)}`,\n )\n }\n const delta = value.delta\n if (typeof delta !== 'string') {\n throw new Error(\n `reaper conformance: run ${runId} journal line has a non-string delta: ${JSON.stringify(value)}`,\n )\n }\n return contentChunk(messageId, delta)\n}\n\n/** Deterministic translator: re-reading the journal reproduces the same chunks. */\nasync function* translate(\n runId: string,\n lines: AsyncIterable<unknown>,\n): AsyncIterable<StreamChunk> {\n const messageId = createRunScopedIdGen(runId)()\n for await (const line of lines) yield toChunk(runId, messageId, line)\n}\n\n/** A comparable transcript: each chunk reduced to its fingerprint. */\nfunction transcript(chunks: Array<StreamChunk>): Array<string> {\n return chunks.map(chunkFingerprint)\n}\n\n/** The chunks a run over `deltas` must deliver, exactly once and in order. */\nfunction expectedTranscript(\n runId: string,\n deltas: Array<string>,\n): Array<StreamChunk> {\n const messageId = createRunScopedIdGen(runId)()\n return deltas.map((delta) => contentChunk(messageId, delta))\n}\n\n/**\n * The reaper's `drive`: read the run's journal from byte 0 and translate it.\n *\n * The read is bounded independently of `signal` so a journal that stops growing\n * fails the case instead of hanging CI.\n *\n * Returns the drive alongside `backstopped()`, the causal witness for\n * {@link READ_BACKSTOP_MS}: the case must assert it is `false` before its\n * transcript assertions, so a read the CLOCK ended fails naming the backstop\n * instead of as a truncated-transcript diff.\n */\nfunction driveFromJournal(\n handle: SandboxHandle,\n dir: string,\n): {\n drive: (input: {\n runId: string\n threadId: string\n signal: AbortSignal\n }) => AsyncIterable<StreamChunk>\n /** True if any read this drive started was ended by the backstop clock. */\n backstopped: () => boolean\n} {\n // One entry per `drive` invocation, so a sweep that drives more than one run\n // cannot hide a backstopped read behind a healthy one.\n const backstops: Array<AbortSignal> = []\n return {\n drive: ({ runId, signal }) => {\n // Not the assertion — see {@link READ_BACKSTOP_MS}. `backstopped()` is what\n // proves the clock was not what ended the read.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n backstops.push(backstop)\n return translate(\n runId,\n readJournalNdjson(handle, {\n signal: AbortSignal.any([signal, backstop]),\n journal: { runId, dir, pollIntervalMs: POLL_INTERVAL_MS },\n }),\n )\n },\n backstopped: () => backstops.some((s) => s.aborted),\n }\n}\n\n/** A `'running'`, DETACHED record — the shape `listReclaimable` selects on. */\nasync function detachedRun(\n store: RunStore,\n runId: string,\n threadId: string,\n detachedSince: number,\n): Promise<void> {\n await store.createOrResume({ runId, threadId, startedAt: Date.now() })\n await store.update(runId, { detachedSince })\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Assert `createHandle` satisfies the sweep conformance contract. Each `it` gets\n * a fresh sandbox via `createHandle`/`dispose`, a fresh journal directory, and\n * unique runIds, so no case can observe another's files.\n */\nexport function runReaperConformance(config: ReaperConformanceConfig): void {\n describe(`reaper conformance — ${config.name}`, () => {\n if (config.unsupported) {\n it.skip(`unsupported: ${config.unsupported.reason}`, () => {\n expect(true).toBe(true)\n })\n return\n }\n\n // -----------------------------------------------------------------------\n // 1. `pruneJournals` against a real filesystem.\n // -----------------------------------------------------------------------\n it(\n \"deletes a terminal run's journal AND its .err sidecar, while a running run's journal survives the same sweep\",\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const terminalId = uniqueRunId('terminal')\n const liveId = uniqueRunId('live')\n const terminal = journalPaths(terminalId, dir)\n const live = journalPaths(liveId, dir)\n try {\n await runAgent(handle, terminal, ['1'])\n await runAgent(handle, live, ['1'])\n // Premise: all four files really exist before the sweep, otherwise\n // \"deleted\" below would be indistinguishable from \"never written\".\n expect({\n terminalJournal: await fileExists(handle, terminal.journal),\n terminalSidecar: await fileExists(handle, terminal.stderr),\n liveJournal: await fileExists(handle, live.journal),\n }).toEqual({\n terminalJournal: true,\n terminalSidecar: true,\n liveJournal: true,\n })\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId: terminalId,\n threadId: `${terminalId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(terminalId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n await runs.createOrResume({\n runId: liveId,\n threadId: `${liveId}-t`,\n startedAt: Date.now(),\n })\n\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([terminalId])\n expect(result.failures).toEqual([])\n expect(result.kept).toEqual([\n { runId: liveId, names: expect.any(Array), reason: 'non-terminal' },\n ])\n\n // The files, through the shell. A sweep that reported a deletion it did\n // not perform passes every assertion above and fails here.\n expect({\n terminalJournal: await fileExists(handle, terminal.journal),\n terminalSidecar: await fileExists(handle, terminal.stderr),\n liveJournal: await fileExists(handle, live.journal),\n liveSidecar: await fileExists(handle, live.stderr),\n }).toEqual({\n terminalJournal: false,\n terminalSidecar: false,\n liveJournal: true,\n liveSidecar: true,\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'sweeps the same terminal run twice without a failure, and rm -f of an already-absent journal exits 0',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('twice')\n const paths = journalPaths(runId, dir)\n try {\n await runAgent(handle, paths, ['1'])\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId,\n threadId: `${runId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n\n const first = await pruneJournals({ handle, runs, dir })\n expect(first.deleted).toEqual([runId])\n\n // The second sweep sees an empty directory. It must report nothing to\n // do rather than a failure — a cron runs this every tick forever.\n const second = await pruneJournals({ handle, runs, dir })\n expect({\n listed: second.listed,\n runIds: second.runIds,\n deleted: second.deleted,\n kept: second.kept,\n failures: second.failures,\n }).toEqual({\n listed: 0,\n runIds: 0,\n deleted: [],\n kept: [],\n failures: [],\n })\n\n // And the `rm -f` the sweep issues is itself idempotent on this shell.\n // Asserted directly because the sweep folds a non-zero `rm` into\n // `kept: 'delete-failed'` and would therefore hide it as a keep.\n const rerun = await handle.process.exec(journalCleanupCommand(paths))\n expect(rerun.exitCode).toBe(0)\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'leaves a filename it cannot decode alone, while still sweeping the terminal run beside it',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('undecodable')\n const paths = journalPaths(runId, dir)\n // `_1.` is not a two-hex-digit escape, so this name is `malformed` — the\n // shape a truncated or foreign file has. `decodeJournalRunId` must refuse\n // it, and the sweep must keep it WITHOUT asking the store, because a\n // plausible-but-wrong runId could answer `terminal` for someone else.\n const strayName = 'reaper-conformance-stray_1.ndjson'\n const strayPath = `${dir}/${strayName}`\n try {\n await runAgent(handle, paths, ['1'])\n await handle.process.exec(\n `printf 'not a journal\\\\n' >> ${quote(strayPath)}`,\n )\n expect(await fileExists(handle, strayPath)).toBe(true)\n expect(decodeJournalRunId(strayName).kind).toBe('malformed')\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId,\n threadId: `${runId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([runId])\n expect(result.kept).toEqual([\n { names: [strayName], reason: 'undecodable-name' },\n ])\n expect(result.failures).toEqual([])\n expect({\n strayKept: await fileExists(handle, strayPath),\n journalDeleted: !(await fileExists(handle, paths.journal)),\n }).toEqual({ strayKept: true, journalDeleted: true })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 2. The age gate on a real shell.\n // -----------------------------------------------------------------------\n it(\n \"emits stat's self-witness line for a populated directory, so the age gate is usable\",\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('witness')\n const paths = journalPaths(runId, dir)\n try {\n await runAgent(handle, paths, ['1'])\n const listing = await mtimeListing(handle, dir)\n // The witness is what makes \"no files\" distinguishable from \"the\n // mechanism is unavailable\". On BusyBox 1.37 — the docker provider's\n // `alpine:3` — `find -newermt`/`-printf` are unrecognised and exit 1\n // with empty stdout, which is exactly why the design is a witness line\n // rather than a `find` and an exit code.\n expect(hasWitnessLine(listing.stdout, dir)).toBe(true)\n expect([...listing.entries.keys()].sort()).toEqual(\n [basename(dir, paths.journal), basename(dir, paths.stderr)].sort(),\n )\n // Real epoch times, not the parser's zeroes: a `%Y` the shell did not\n // expand would parse as no entry at all, and a `stat` that printed\n // something else would land far from now.\n for (const mtimeMs of listing.entries.values()) {\n expect(Math.abs(Date.now() - mtimeMs)).toBeLessThan(120_000)\n }\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'reports an EMPTY journal directory as witness-only rather than unavailable',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n try {\n await handle.process.exec(`mkdir -p ${quote(dir)}`)\n const listing = await mtimeListing(handle, dir)\n expect(hasWitnessLine(listing.stdout, dir)).toBe(true)\n expect([...listing.entries.keys()]).toEqual([])\n\n // And the sweep agrees: an empty directory is a LISTED age gate, not an\n // unavailable one. `'unavailable'` here would silently disable orphan\n // expiry forever on this provider.\n const result = await pruneJournals({\n handle,\n runs: new InMemoryRunStore(),\n dir,\n })\n expect({\n listed: result.listed,\n ageGate: result.ageGate,\n deleted: result.deleted,\n failures: result.failures,\n }).toEqual({\n listed: 0,\n ageGate: 'listed',\n deleted: [],\n failures: [],\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'keeps an orphan younger than orphanTtlMs and sweeps the older one, in the same pass',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const olderId = uniqueRunId('older')\n const newerId = uniqueRunId('newer')\n const older = journalPaths(olderId, dir)\n const newer = journalPaths(newerId, dir)\n try {\n await runAgent(handle, older, ['1'])\n // `stat -c '%Y'` is second-granular, so the two runs must be more than\n // one second apart for their ages to be distinguishable at all.\n await sleep(2_500)\n await runAgent(handle, newer, ['1'])\n\n // The cutoff is computed from the REAL mtimes the real shell reported,\n // not from a fabricated timestamp: that is the whole point of running\n // this against a provider. `pruneJournals` keeps when the NEWEST of a\n // run's files is strictly newer than the cutoff, so placing the cutoff\n // between the two runs must expire exactly one of them.\n const listing = await mtimeListing(handle, dir)\n const newestOf = (paths: JournalPaths): number =>\n Math.max(\n mtimeOf(listing.entries, basename(dir, paths.journal)),\n mtimeOf(listing.entries, basename(dir, paths.stderr)),\n )\n const olderMtime = newestOf(older)\n const newerMtime = newestOf(newer)\n expect(newerMtime - olderMtime).toBeGreaterThanOrEqual(1_000)\n\n const now = Date.now()\n const cutoff = olderMtime + Math.floor((newerMtime - olderMtime) / 2)\n // NEITHER run is in the store, so both take the orphan arm and only the\n // age gate decides between them.\n const result = await pruneJournals({\n handle,\n runs: new InMemoryRunStore(),\n dir,\n now,\n orphanTtlMs: now - cutoff,\n })\n expect(result.ageGate).toBe('listed')\n expect(result.deleted).toEqual([olderId])\n expect(result.kept).toEqual([\n {\n runId: newerId,\n names: expect.any(Array),\n reason: 'orphan-too-recent',\n },\n ])\n expect(result.failures).toEqual([])\n expect({\n olderJournal: await fileExists(handle, older.journal),\n olderSidecar: await fileExists(handle, older.stderr),\n newerJournal: await fileExists(handle, newer.journal),\n newerSidecar: await fileExists(handle, newer.stderr),\n }).toEqual({\n olderJournal: false,\n olderSidecar: false,\n newerJournal: true,\n newerSidecar: true,\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 3. `reapDetachedRuns` end to end.\n // -----------------------------------------------------------------------\n it(\n 'finalizes a detached run whose agent reached its sentinel, and its transcript lands',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('finalize')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3']\n const paths = journalPaths(runId, dir)\n try {\n const runs = new InMemoryRunStore()\n const detachedSince = Date.now()\n await detachedRun(runs, runId, threadId, detachedSince)\n await runAgent(handle, paths, deltas)\n\n // The probe, on its own, before any sweep: this read is what makes the\n // reaper safe, and it must answer from the JOURNAL rather than from the\n // delivery log (which a detached run's dead host stopped appending to).\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'finished',\n exitCode: 0,\n })\n\n const log = conformanceLog()\n const journalDrive = driveFromJournal(handle, dir)\n const result = await reapDetachedRuns({\n runs,\n locks: new InMemoryLockStore(),\n durability: () => log.log,\n hasFinished: (record) =>\n probeRunExit({ handle, runId: record.runId, dir }),\n drive: journalDrive.drive,\n now: Date.now(),\n detachedRunTtlMs: NEVER_EXPIRES_MS,\n fenceQuietMs: FENCE_QUIET_MS,\n })\n\n // The causal witness, before anything downstream — see\n // {@link READ_BACKSTOP_MS}. The reaper's read ends at the sentinel; if\n // the clock ended it instead, the transcript below is short and the\n // failure must name the backstop rather than a missing chunk.\n expect({ backstopped: journalDrive.backstopped() }).toEqual({\n backstopped: false,\n })\n expect({\n considered: result.considered,\n probed: result.probed,\n finalized: result.outcomes.finalized,\n }).toEqual({ considered: 1, probed: 1, finalized: 1 })\n expect(result.runs).toEqual([\n { runId, outcome: 'finalized', status: 'completed', exitCode: 0 },\n ])\n // The transcript, element for element — the reaper's whole purpose is\n // that the run a nobody watched still ends up saved.\n expect(transcript(log.stored())).toEqual(\n transcript(expectedTranscript(runId, deltas)),\n )\n const record = await runs.get(runId)\n expect(record?.status).toBe('completed')\n // NEVER CLEARED: `detachedSince` is what the next sweep selects on, and\n // clearing it would reset the TTL on every pass.\n expect(record?.detachedSince).toBe(detachedSince)\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'reports a still-producing detached run as producing and leaves it completely untouched',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('producing')\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, dir)\n const store = countingRunStore(new InMemoryRunStore())\n // A REAL agent that has written a line and is genuinely still alive: no\n // sentinel can be in the journal, and driving it would truncate a healthy\n // run's transcript at line one.\n const agent = await handle.process.spawn(\n journaledCommand(`${emitLines(['1'])}; sleep 30`, paths),\n )\n try {\n const detachedSince = Date.now()\n await detachedRun(store.runs, runId, threadId, detachedSince)\n await waitForJournal(handle, paths)\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n // Producing, provably: the first line is there and the sentinel is not.\n expect(text).toContain('{\"delta\":\"1\"}')\n expect(text).not.toContain('__exit')\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'producing',\n })\n\n const log = conformanceLog()\n let driveCalled = false\n let durabilityCalls = 0\n const result = await reapDetachedRuns({\n runs: store.runs,\n locks: new InMemoryLockStore(),\n durability: () => {\n durabilityCalls += 1\n return log.log\n },\n hasFinished: (record) =>\n probeRunExit({ handle, runId: record.runId, dir }),\n drive: () => {\n driveCalled = true\n return (async function* never() {})()\n },\n now: Date.now(),\n detachedRunTtlMs: NEVER_EXPIRES_MS,\n fenceQuietMs: FENCE_QUIET_MS,\n })\n\n expect(result.runs).toEqual([{ runId, outcome: 'producing' }])\n expect({\n considered: result.considered,\n probed: result.probed,\n producing: result.outcomes.producing,\n finalized: result.outcomes.finalized,\n expired: result.outcomes.expired,\n failed: result.outcomes.failed,\n }).toEqual({\n considered: 1,\n probed: 1,\n producing: 1,\n finalized: 0,\n expired: 0,\n failed: 0,\n })\n\n // ABSENCE, asserted in one object so a regression names which\n // guarantee broke instead of failing on whichever line came first.\n // Every one of these is a way the pre-`probeRunExit` design destroyed a\n // live run: an append duplicates its prefix, a `close()` ends every\n // attached client's stream, an `update` writes `'completed'` and drops\n // the run out of `listReclaimable` forever, and a moved\n // `detachedSince` restarts its TTL.\n const record = await store.runs.get(runId)\n expect({\n driveCalled,\n durabilityCalls,\n appended: log.stored().length,\n closes: log.closes(),\n updatesAfterSetup: store.updates() - 1,\n status: record?.status,\n detachedSince: record?.detachedSince,\n driverEpoch: record?.driverEpoch,\n }).toEqual({\n driveCalled: false,\n durabilityCalls: 0,\n appended: 0,\n closes: 0,\n updatesAfterSetup: 0,\n status: 'running',\n detachedSince,\n driverEpoch: undefined,\n })\n } finally {\n // The agent outlives the sweep on purpose; reap it here so no `sleep`\n // survives the case.\n try {\n await agent.kill()\n } catch {\n // Already gone, or a provider whose sandbox teardown covers it.\n }\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 4. A shell-hostile runId, end to end. SECURITY-RELEVANT.\n // -----------------------------------------------------------------------\n it(\n 'round-trips a shell-hostile runId through encode, journal, follow, sidecar read, list, decode and delete without executing any of it' +\n (config.followUnsupported === undefined\n ? ''\n : ` (follow read omitted: ${config.followUnsupported.reason})`),\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const nonce = randomUUID().slice(0, 8)\n // The canary lives OUTSIDE `dir` so the teardown `rm -rf` cannot be what\n // makes the final assertion pass.\n const canary = `/tmp/rp-pwn-${nonce}`\n // `/` would escape the directory, the space would split the word, `;` and\n // `$( )` would start new commands, and the `touch` is a real payload with\n // an observable effect. Every one of these must survive as DATA.\n //\n // THE ORDER OF THE PAYLOAD IS DELIBERATE and was measured: the `;touch`\n // comes BEFORE the space and the `/`. With raw interpolation, the\n // journaled command's redirect target is one word, so a payload whose\n // space precedes the `;` (`rp-a b;touch …`) makes the mangled command a\n // SYNTAX ERROR — the injected `touch` never runs and the canary below\n // would be decoration that can never fire. With the `;` first, the\n // vulnerable form parses as a command LIST and the payload really\n // executes (verified against a hand-composed unquoted, unencoded command\n // on this provider: canary present). So the canary is a live detector.\n const runId = `rp-a;touch ${canary};b c/d$(x)-${nonce}`\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, dir)\n const journalName = basename(dir, paths.journal)\n try {\n // This case's agent writes to STDERR as well, so the sidecar read\n // below has real bytes to compare against: an empty sidecar is also\n // what a `journalStderrReadCommand` that read the wrong path (or\n // nothing at all) would return, and that read is the only coverage\n // that command has anywhere.\n await handle.process.exec(\n journaledCommand(\n `${emitLines(['1'])}; printf 'boom\\\\n' 1>&2`,\n paths,\n ),\n )\n // THE SECURITY ASSERTION, and deliberately the FIRST one: nothing the\n // runId contains was executed. It is stated before the cheaper\n // structural checks below on purpose — a defect that reintroduces raw\n // interpolation would also fail the filename shape, and a case that\n // short-circuited there would never prove this probe is live rather\n // than decorative. Re-asserted after the delete, because the sweep\n // composes a DIFFERENT command (`rm -f`) from the same id.\n expect(await fileExists(handle, canary)).toBe(false)\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'finished',\n exitCode: 0,\n })\n // The encoding is what bought that: the filename carries no character\n // a shell can act on, and it stays inside the journal directory.\n expect(journalName).toMatch(/^[A-Za-z0-9._-]+\\.ndjson$/)\n expect(paths.journal.startsWith(`${dir}/`)).toBe(true)\n\n // THE FOLLOW PATH, against this same hostile id.\n //\n // `journalFollowCommand` is the WORST command in the set under a\n // dropped `encodeRunId`: it interpolates the journal path THREE times\n // (`mkdir -p`, `: >> path`, `tail -c +N -f path`) and joins its prep\n // steps with `;` rather than `&&`, so `: >> /tmp/dir/rp-a;touch\n // <canary>;…` is a complete redirect followed by a command LIST — the\n // payload runs on EVERY attach, and a failing prep step does not stop\n // it. Nothing else reaches this command with a hostile runId: the\n // reaper's own probes are all bounded reads, and the takeover suite,\n // the only other real-provider consumer of the follow path, builds\n // alnum-only ids. So it is exercised here, where the hostile id and a\n // live canary already exist, for the cost of one read.\n //\n // The strategy is FORCED rather than capability-derived so this is the\n // follow command and not the bounded one, and the declaration is\n // checked against the live handle in both directions — a config that\n // does not describe the provider must fail rather than silently drop\n // this read.\n expect(journalReadStrategy(handle)).toBe(\n config.followUnsupported === undefined ? 'follow' : 'poll',\n )\n if (config.followUnsupported === undefined) {\n const followed: Array<string> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI — a `tail -f` never ends on its own, so this read has no\n // other floor. Not the assertion — `backstopped` below proves it was\n // not what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n strategy: 'follow',\n signal: backstop,\n })) {\n followed.push(line.line)\n // The agent has already reached its sentinel, so this arrives; a\n // `tail -f` never ends on its own.\n if (line.line.includes(EXIT_SENTINEL_KEY)) break\n }\n // The causal witness, first: the loop must end on the sentinel\n // `break`, not on the clock. A backstopped follow read otherwise\n // reports as a one-element-vs-two array diff that says nothing about\n // why.\n expect({ backstopped: backstop.aborted }).toEqual({\n backstopped: false,\n })\n expect(followed).toEqual([\n '{\"delta\":\"1\"}',\n exitSentinelLine(paths, 0),\n ])\n expect(await fileExists(handle, canary)).toBe(false)\n }\n\n // The stderr SIDECAR read, which no other conformance case reaches at\n // all. Same hostile id, same canary, one `exec`.\n const sidecar = await handle.process.exec(\n journalStderrReadCommand(paths),\n )\n expect(decodeJournalRead(sidecar.stdout)).toBe('boom\\n')\n expect(await fileExists(handle, canary)).toBe(false)\n\n // encode → journal → list → decode: the sweep's actual path back to a\n // runId, over a real `ls -1`.\n const names = await listNames(handle, dir)\n expect(names.sort()).toEqual(\n [journalName, basename(dir, paths.stderr)].sort(),\n )\n expect(decodeJournalRunId(journalName)).toEqual({\n kind: 'runId',\n runId,\n })\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({ runId, threadId, startedAt: Date.now() })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([runId])\n expect(result.failures).toEqual([])\n expect({\n journalDeleted: !(await fileExists(handle, paths.journal)),\n sidecarDeleted: !(await fileExists(handle, paths.stderr)),\n canaryAbsent: !(await fileExists(handle, canary)),\n }).toEqual({\n journalDeleted: true,\n sidecarDeleted: true,\n canaryAbsent: true,\n })\n } finally {\n await handle.process\n .exec(`rm -f ${quote(canary)}`)\n .catch(() => undefined)\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"reaper-conformance.js","names":[],"sources":["../../../src/testkit/reaper-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for the two unattended sweeps: `pruneJournals`\n * (`journal-sweep.ts`) and `reapDetachedRuns` (`reap.ts`), against a REAL\n * sandbox.\n *\n * WHY THIS EXISTS SEPARATELY FROM THE UNIT TESTS. Both sweeps are almost\n * entirely *shell* — `ls -1`, `stat -c '%Y %n'`, `rm -f`, `tail -c -N | base64`\n * — composed as strings by `journal.ts` and executed by a provider. The unit\n * suites drive fakes: an `exec` that answers from a scripted table, a\n * filesystem that is a `Map`. A fake cannot be wrong about `stat` the way a\n * BusyBox actually is, and on this feature that gap has already produced four\n * defects that every unit test passed (see `takeover-conformance.ts`'s module\n * doc for the roster). So the four properties the sweeps rest on are asserted\n * here through a real shell against real files:\n *\n * 1. **A deletion really deletes, and a keep really keeps.** Asserted with\n * `test -f` through the provider's shell, NEVER `handle.fs.exists`: on\n * local-process the two resolve `/tmp` differently, so an `fs` probe answers\n * about a path the journal was never written to (`journal.ts` rule 3). A\n * sweep that \"succeeded\" while deleting nothing passes an `fs` probe.\n * 2. **The age gate's self-witness works on THIS shell.** `journalMtimeListCommand`\n * passes the directory as `stat`'s own first operand precisely because\n * BusyBox exits 1 with EMPTY stdout on an unrecognised flag, and an empty\n * parse read as an empty directory would delete every live run's journal. The\n * docker provider's image is `alpine:3` — BusyBox 1.37, where `find -newermt`\n * and `find -printf` are unrecognised — so the docker matrix is the authority\n * on this case, not the local-process one (on Windows local-process execs\n * through git-bash, whose `find`/`stat` are GNU-flavoured).\n * 3. **The reaper never drives a live run.** The `'producing'` case asserts\n * ABSENCE — nothing appended, `close()` not called, not one `runs.update`,\n * `detachedSince` intact — because that is the shape of the defect\n * `probeRunExit` exists to prevent: entering `pipeToRunLog` to \"check\" writes\n * a terminal status and drops the run out of `listReclaimable` forever.\n * 4. **A shell-hostile runId cannot become a shell-hostile command.** The encode\n * → journal → follow → `ls` → decode → `rm` round trip runs on a runId\n * containing `/`, a space, `;`, `$( )` and an embedded `touch`, with a canary\n * file asserted absent. An ENCODING bug here is arbitrary command execution\n * inside the sandbox, not a cosmetic defect.\n *\n * **What the canary proves, exactly, and what it does not.** It detects a\n * runId reaching the shell WITHOUT `encodeRunId` — that is the mutation it\n * bites on, and it bites hard: `journaledCommand`, `journalFollowCommand`,\n * `journalExitProbeCommand`, `journalStderrReadCommand` and\n * `journalCleanupCommand` all interpolate the path, so the `;touch` executes\n * and the canary appears. It is BLIND to the loss of `journal.ts`'s\n * `shellQuote`, the second and independent layer. Measured: with `shellQuote`\n * reduced to the identity while `encodeRunId` stays, the redirect target\n * becomes `>> /tmp/…/rp-a_3btouch_20_2ftmp…ndjson` — a single shell word of\n * `[A-Za-z0-9._/-]`, because the encoder already removed every character a\n * shell can act on — so no canary fires and NOTHING in this suite, or in any\n * other real-provider suite, changes. Do not read a green run here as licence\n * to \"simplify\" `shellQuote` away.\n *\n * The quoting is pinned instead by exact-string unit tests in\n * `packages/ai-sandbox/tests/journal.test.ts`, which compare each composed\n * command to a literal containing the quotes. By name, one per command:\n * `journaledCommand` — \"redirects stdout to the journal, stderr to its own\n * file, and appends the exit sentinel\" plus \"quotes an adversarial runId so it\n * cannot inject shell metacharacters\"; `journalFollowCommand` — \"translates a\n * 0-based consumed-byte count into tail -c +N (1-based)\";\n * `journalReadCommand` — \"the bounded read drops -f and keeps the base64\n * frame, so a poll cannot hang\"; `journalExistsCommand` — \"probes through the\n * shell, never through fs.*\"; `journalStderrReadCommand` — \"reads a BOUNDED\n * tail of the sidecar, base64-framed, stderr silenced\";\n * `journalCleanupCommand`, `journalMtimeListCommand` and\n * `journalExitProbeCommand` — the first `it` under each of their `describe`s.\n * Those are the tests that go red on a dropped `shellQuote`; keep them exact.\n *\n * A provider that cannot satisfy the contract MUST declare `unsupported.reason`.\n * As in the journal and takeover suites there is deliberately no silent-skip\n * path: a conformance case that quietly returns prints as a pass, which is how\n * an unimplemented capability ships green.\n *\n * EVERY WAIT IN THIS FILE IS BOUNDED, and every journal directory is unique per\n * case — see {@link caseDir}. This suite DELETES FILES, and\n * `DEFAULT_JOURNAL_DIR` is a fixed absolute path shared with every other test\n * and, on local-process, with a developer's real runs.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { randomUUID } from 'node:crypto'\nimport { describe, expect, it } from 'vitest'\nimport { EventType, InMemoryRunStore } from '@tanstack/ai'\nimport { InMemoryLockStore } from '@tanstack/ai/locks'\nimport {\n EXIT_SENTINEL_KEY,\n decodeJournalRunId,\n exitSentinelLine,\n journalCleanupCommand,\n journalExistsCommand,\n journalListCommand,\n journalMtimeListCommand,\n journalPaths,\n journalReadCommand,\n journalStderrReadCommand,\n journaledCommand,\n parseJournalMtimeListing,\n} from '../journal'\nimport { journalReadStrategy, readJournal } from '../journal-reader'\nimport { pruneJournals } from '../journal-sweep'\nimport { probeRunExit, reapDetachedRuns } from '../reap'\nimport { readJournalNdjson } from '../runner'\nimport { chunkFingerprint, createRunScopedIdGen } from '../chunk-identity'\nimport { waitForJournal } from './journal-conformance'\nimport type { JournalPaths } from '../journal'\nimport type { SandboxHandle } from '../contracts'\nimport type { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai'\n\nexport interface ReaperConformanceConfig {\n /** Provider name, used in the describe title. */\n name: string\n /** Create a live sandbox plus its teardown. */\n createHandle: () => Promise<{\n handle: SandboxHandle\n dispose: () => Promise<void>\n }>\n /**\n * Declare that this provider cannot support the sweeps, with the reason.\n * Registers a skipped case whose title carries the reason — a NAMED skip,\n * visible in the reporter. Omit it and the suite runs.\n */\n unsupported?: { reason: string }\n /**\n * Declare that this provider's reads take the POLL strategy rather than the\n * FOLLOW one — i.e. `journalReadStrategy` answers `'poll'` for its handles.\n *\n * Only the FOLLOW half of the shell-hostile-runId case depends on it, so this\n * does not skip a case; it names itself in that case's title and the follow\n * read is omitted. The declaration is checked against the live handle there, in\n * both directions, so it cannot quietly remove coverage from a provider that\n * can in fact follow.\n */\n followUnsupported?: { reason: string }\n}\n\n/** Poll interval handed to providers that cannot follow a growing file. */\nconst POLL_INTERVAL_MS = 50\n\n/**\n * Quiescence window for the reaper's first append. Short because the agent in\n * these cases has provably stopped (the suite waited for its sentinel) — the\n * gate still runs, it just does not need to wait 5s to observe nothing.\n */\nconst FENCE_QUIET_MS = 25\n\n/**\n * Bound on a real journal read, so a reader that delivers nothing FAILS instead\n * of parking CI.\n *\n * Never an assertion, and deliberately far above anything a healthy read needs\n * (measured: 10–18s for the follow cases on both providers). Every use site\n * pairs it with a `backstopped: false` witness, so a read the CLOCK ended fails\n * naming this backstop rather than as a downstream transcript mismatch — which\n * means this number can be raised freely and must never be the thing a case is\n * tuned against.\n */\nconst READ_BACKSTOP_MS = 90_000\n\n/** Long enough that nothing in this suite is ever classified as expired. */\nconst NEVER_EXPIRES_MS = 60 * 60 * 1000\n\n/**\n * A journal directory nothing else on the machine writes to, created fresh for\n * EVERY case.\n *\n * Not `DEFAULT_JOURNAL_DIR`, and not even one directory per suite. Both sweeps\n * under test enumerate a whole directory and then DELETE from it, so a shared\n * directory would let one case's leftovers become another's input — and on\n * local-process the sandbox shell shares the host's real `/tmp`, where\n * `DEFAULT_JOURNAL_DIR` holds a developer's actual runs.\n */\nfunction caseDir(): string {\n return `/tmp/tanstack-reaper-conformance-${randomUUID()}`\n}\n\n/**\n * Unique per run, and it must be: `journalPaths` derives the filename from the\n * runId and the journal is append-only, so a reused id appends BEHIND the\n * previous run's `{\"__exit\":N}` sentinel and the new run appears to emit nothing\n * at all (see `journal.ts`).\n */\nfunction uniqueRunId(label: string): string {\n return `rp-${label}-${randomUUID()}`\n}\n\n/**\n * Single-quote a shell word, POSIX-style — the same rule `journal.ts`'s private\n * `shellQuote` applies.\n *\n * Duplicated rather than exported from production code on purpose: this exists\n * only for this suite's `rm -rf` teardown, which is not a production operation\n * and must not become one by growing an export for it.\n */\nfunction quote(value: string): string {\n return `'${value.replaceAll(\"'\", `'\\\\''`)}'`\n}\n\n/** Remove a case's journal directory and everything in it. Best effort. */\nasync function removeDir(handle: SandboxHandle, dir: string): Promise<void> {\n try {\n await handle.process.exec(`rm -rf ${quote(dir)}`)\n } catch {\n // The sandbox may already be gone, and on docker it is about to be. Nothing\n // under test depends on the directory being absent afterwards — the cases\n // that DO assert deletion assert it directly, per file.\n }\n}\n\n/**\n * Does `path` exist, according to the SANDBOX'S SHELL?\n *\n * `journalExistsCommand` rather than `handle.fs.exists`, for any path and not\n * just a journal: `journal.ts` rule 3 — on local-process `fs.*` resolves `/tmp`\n * under the sandbox root while a shell redirect hits the host's real `/tmp`, so\n * an `fs` probe would answer about a different file and every deletion\n * assertion in this suite would pass vacuously.\n */\nasync function fileExists(\n handle: SandboxHandle,\n path: string,\n): Promise<boolean> {\n const probe = await handle.process.exec(\n // Only `journal` is read by the probe, and its parameter is typed\n // `Pick<JournalPaths, 'journal'>` for exactly this reason: an arbitrary path\n // has no run behind it, so there is no nonce or sidecar to invent.\n journalExistsCommand({ journal: path }),\n )\n return probe.exitCode === 0\n}\n\n/** Filename as `ls -1` reports it, for a path inside `dir`. */\nfunction basename(dir: string, path: string): string {\n return path.slice(dir.length + 1)\n}\n\n/**\n * A real agent: a shell command printing one NDJSON line per delta, then\n * exiting.\n *\n * `printf '%s\\n' a b c` reuses the format for every operand on GNU coreutils\n * and on BusyBox alike, so this needs no loop. The JSON contains only double\n * quotes, so it is safe inside the POSIX single-quoted words this builds.\n */\nfunction emitLines(deltas: Array<string>): string {\n return `printf '%s\\\\n' ${deltas.map((delta) => `'{\"delta\":\"${delta}\"}'`).join(' ')}`\n}\n\n/**\n * Run a journaled agent to completion, so the `{\"__exit\":N}` sentinel is in the\n * journal by the time this resolves.\n *\n * `exec`, not `spawn`: `exec` waits, and a bounded wait is the only kind this\n * suite allows. (`SpawnHandle.wait()` is also not safe to call after the fact on\n * every provider — see `journal-conformance.ts`.)\n */\nasync function runAgent(\n handle: SandboxHandle,\n paths: JournalPaths,\n deltas: Array<string>,\n): Promise<void> {\n await handle.process.exec(journaledCommand(emitLines(deltas), paths))\n}\n\n/** `ls -1` output as a list of names. */\nasync function listNames(\n handle: SandboxHandle,\n dir: string,\n): Promise<Array<string>> {\n const listing = await handle.process.exec(journalListCommand(dir))\n return listing.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line !== '')\n}\n\n/** Decode the base64 frame a bounded journal read produces. */\nfunction decodeJournalRead(stdout: string): string {\n return Buffer.from(stdout.replace(/\\s+/g, ''), 'base64').toString('utf8')\n}\n\n/**\n * The `stat -c '%Y %n'` listing for `dir`, plus the raw stdout so a case can\n * assert the WITNESS LINE itself rather than only its parsed consequence.\n */\nasync function mtimeListing(\n handle: SandboxHandle,\n dir: string,\n): Promise<{ stdout: string; entries: Map<string, number> }> {\n const probe = await handle.process.exec(journalMtimeListCommand(dir))\n const parsed = parseJournalMtimeListing(probe.stdout, dir)\n if (parsed.kind !== 'listed') {\n throw new Error(\n `reaper conformance: the mtime listing for ${dir} came back unavailable — ` +\n `stat -c '%Y %n' produced no witness line. stdout: ${JSON.stringify(probe.stdout)}`,\n )\n }\n return {\n stdout: probe.stdout,\n entries: new Map(\n parsed.entries.map((entry) => [entry.name, entry.mtimeMs]),\n ),\n }\n}\n\n/**\n * Is there a `<seconds> <dir>` line — `stat`'s report on its own first operand?\n *\n * That line, not the exit status, is the evidence the mechanism ran: BusyBox\n * exits 1 both for an EMPTY directory (whose unexpanded glob it cannot stat) and\n * for an unrecognised flag, and only the witness distinguishes them.\n */\nfunction hasWitnessLine(stdout: string, dir: string): boolean {\n return stdout\n .split('\\n')\n .some((line) => /^\\d+ (?<path>.+)$/.exec(line.trim())?.[1] === dir)\n}\n\n/** Read one file's mtime out of a listing, loudly when it is missing. */\nfunction mtimeOf(entries: Map<string, number>, name: string): number {\n const mtimeMs = entries.get(name)\n if (mtimeMs === undefined) {\n throw new Error(\n `reaper conformance: ${name} has no mtime in the stat listing, so the age gate cannot be exercised`,\n )\n }\n return mtimeMs\n}\n\n/**\n * An in-process event log with real accumulated state, plus the two facts the\n * reaper assertions need: what was appended, and how many times `close()` ran.\n *\n * `close()` is the load-bearing counter. `pipeToRunLog` ALWAYS calls it, so a\n * reaper that entered the pipe to find out whether a run finished would show up\n * here as `closes() === 1` — which ends every attached client's stream — even if\n * it happened to append nothing.\n */\ninterface ConformanceLog {\n log: StreamDurability\n stored: () => Array<StreamChunk>\n closes: () => number\n}\n\nfunction conformanceLog(): ConformanceLog {\n const entries: Array<{ offset: string; chunk: StreamChunk }> = []\n let closes = 0\n return {\n log: {\n resumeFrom: () => null,\n append: (chunks) =>\n Promise.resolve(\n chunks.map((chunk) => {\n const offset = `reap:${entries.length}`\n entries.push({ offset, chunk })\n return offset\n }),\n ),\n // Nothing here tails the log — every assertion reads the appended\n // transcript, and a `read` would park until `close()` (see `align.ts`).\n read: () => (async function* empty() {})(),\n close: () => {\n closes += 1\n return Promise.resolve()\n },\n snapshot: () => Promise.resolve(entries.map((entry) => ({ ...entry }))),\n },\n stored: () => entries.map((entry) => entry.chunk),\n closes: () => closes,\n }\n}\n\n/**\n * A `RunStore` that counts its MUTATIONS, so the leave-alone case can assert\n * that a producing run's record was not written at all.\n *\n * \"Status still `'running'`\" is too weak on its own: `driverEpoch` is bumped by\n * `withRunClaim` before any status is written, so a reaper that claimed a live\n * run and then bailed would still read as `'running'`. Counting `update` sees\n * that; reading the status does not.\n */\ninterface CountingRunStore {\n runs: RunStore\n updates: () => number\n}\n\nfunction countingRunStore(inner: InMemoryRunStore): CountingRunStore {\n let updates = 0\n return {\n runs: {\n createOrResume: (...args) => inner.createOrResume(...args),\n update: (...args) => {\n updates += 1\n return inner.update(...args)\n },\n get: (...args) => inner.get(...args),\n listByThread: (...args) => inner.listByThread(...args),\n listReclaimable: (...args) => inner.listReclaimable(...args),\n findActiveRun: (...args) => inner.findActiveRun(...args),\n },\n updates: () => updates,\n }\n}\n\n/** The event a journal line translates into. `timestamp` is excluded from `chunkFingerprint`. */\nfunction contentChunk(messageId: string, delta: string): StreamChunk {\n return {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n delta,\n timestamp: Date.now(),\n }\n}\n\n/**\n * Narrow one parsed journal line into its chunk.\n *\n * Fields are validated and the chunk REBUILT from them rather than asserted into\n * shape: a cast would let a provider that mangles the bytes reach\n * `chunkFingerprint` as a structurally invalid chunk and fail somewhere\n * unrelated.\n */\nfunction toChunk(\n runId: string,\n messageId: string,\n value: unknown,\n): StreamChunk {\n if (typeof value !== 'object' || value === null || !('delta' in value)) {\n throw new Error(\n `reaper conformance: run ${runId} journal line is not an agent event: ${JSON.stringify(value)}`,\n )\n }\n const delta = value.delta\n if (typeof delta !== 'string') {\n throw new Error(\n `reaper conformance: run ${runId} journal line has a non-string delta: ${JSON.stringify(value)}`,\n )\n }\n return contentChunk(messageId, delta)\n}\n\n/** Deterministic translator: re-reading the journal reproduces the same chunks. */\nasync function* translate(\n runId: string,\n lines: AsyncIterable<unknown>,\n): AsyncIterable<StreamChunk> {\n const messageId = createRunScopedIdGen(runId)()\n for await (const line of lines) yield toChunk(runId, messageId, line)\n}\n\n/** A comparable transcript: each chunk reduced to its fingerprint. */\nfunction transcript(chunks: Array<StreamChunk>): Array<string> {\n return chunks.map(chunkFingerprint)\n}\n\n/** The chunks a run over `deltas` must deliver, exactly once and in order. */\nfunction expectedTranscript(\n runId: string,\n deltas: Array<string>,\n): Array<StreamChunk> {\n const messageId = createRunScopedIdGen(runId)()\n return deltas.map((delta) => contentChunk(messageId, delta))\n}\n\n/**\n * The reaper's `drive`: read the run's journal from byte 0 and translate it.\n *\n * The read is bounded independently of `signal` so a journal that stops growing\n * fails the case instead of hanging CI.\n *\n * Returns the drive alongside `backstopped()`, the causal witness for\n * {@link READ_BACKSTOP_MS}: the case must assert it is `false` before its\n * transcript assertions, so a read the CLOCK ended fails naming the backstop\n * instead of as a truncated-transcript diff.\n */\nfunction driveFromJournal(\n handle: SandboxHandle,\n dir: string,\n): {\n drive: (input: {\n runId: string\n threadId: string\n signal: AbortSignal\n }) => AsyncIterable<StreamChunk>\n /** True if any read this drive started was ended by the backstop clock. */\n backstopped: () => boolean\n} {\n // One entry per `drive` invocation, so a sweep that drives more than one run\n // cannot hide a backstopped read behind a healthy one.\n const backstops: Array<AbortSignal> = []\n return {\n drive: ({ runId, signal }) => {\n // Not the assertion — see {@link READ_BACKSTOP_MS}. `backstopped()` is what\n // proves the clock was not what ended the read.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n backstops.push(backstop)\n return translate(\n runId,\n readJournalNdjson(handle, {\n signal: AbortSignal.any([signal, backstop]),\n journal: { runId, dir, pollIntervalMs: POLL_INTERVAL_MS },\n }),\n )\n },\n backstopped: () => backstops.some((s) => s.aborted),\n }\n}\n\n/** A `'running'`, DETACHED record — the shape `listReclaimable` selects on. */\nasync function detachedRun(\n store: RunStore,\n runId: string,\n threadId: string,\n detachedSince: number,\n): Promise<void> {\n await store.createOrResume({ runId, threadId, startedAt: Date.now() })\n await store.update(runId, { detachedSince })\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Assert `createHandle` satisfies the sweep conformance contract. Each `it` gets\n * a fresh sandbox via `createHandle`/`dispose`, a fresh journal directory, and\n * unique runIds, so no case can observe another's files.\n */\nexport function runReaperConformance(config: ReaperConformanceConfig): void {\n describe(`reaper conformance — ${config.name}`, () => {\n if (config.unsupported) {\n it.skip(`unsupported: ${config.unsupported.reason}`, () => {\n expect(true).toBe(true)\n })\n return\n }\n\n // -----------------------------------------------------------------------\n // 1. `pruneJournals` against a real filesystem.\n // -----------------------------------------------------------------------\n it(\n \"deletes a terminal run's journal AND its .err sidecar, while a running run's journal survives the same sweep\",\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const terminalId = uniqueRunId('terminal')\n const liveId = uniqueRunId('live')\n const terminal = journalPaths(terminalId, dir)\n const live = journalPaths(liveId, dir)\n try {\n await runAgent(handle, terminal, ['1'])\n await runAgent(handle, live, ['1'])\n // Premise: all four files really exist before the sweep, otherwise\n // \"deleted\" below would be indistinguishable from \"never written\".\n expect({\n terminalJournal: await fileExists(handle, terminal.journal),\n terminalSidecar: await fileExists(handle, terminal.stderr),\n liveJournal: await fileExists(handle, live.journal),\n }).toEqual({\n terminalJournal: true,\n terminalSidecar: true,\n liveJournal: true,\n })\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId: terminalId,\n threadId: `${terminalId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(terminalId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n await runs.createOrResume({\n runId: liveId,\n threadId: `${liveId}-t`,\n startedAt: Date.now(),\n })\n\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([terminalId])\n expect(result.failures).toEqual([])\n expect(result.kept).toEqual([\n { runId: liveId, names: expect.any(Array), reason: 'non-terminal' },\n ])\n\n // The files, through the shell. A sweep that reported a deletion it did\n // not perform passes every assertion above and fails here.\n expect({\n terminalJournal: await fileExists(handle, terminal.journal),\n terminalSidecar: await fileExists(handle, terminal.stderr),\n liveJournal: await fileExists(handle, live.journal),\n liveSidecar: await fileExists(handle, live.stderr),\n }).toEqual({\n terminalJournal: false,\n terminalSidecar: false,\n liveJournal: true,\n liveSidecar: true,\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'sweeps the same terminal run twice without a failure, and rm -f of an already-absent journal exits 0',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('twice')\n const paths = journalPaths(runId, dir)\n try {\n await runAgent(handle, paths, ['1'])\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId,\n threadId: `${runId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n\n const first = await pruneJournals({ handle, runs, dir })\n expect(first.deleted).toEqual([runId])\n\n // The second sweep sees an empty directory. It must report nothing to\n // do rather than a failure — a cron runs this every tick forever.\n const second = await pruneJournals({ handle, runs, dir })\n expect({\n listed: second.listed,\n runIds: second.runIds,\n deleted: second.deleted,\n kept: second.kept,\n failures: second.failures,\n }).toEqual({\n listed: 0,\n runIds: 0,\n deleted: [],\n kept: [],\n failures: [],\n })\n\n // And the `rm -f` the sweep issues is itself idempotent on this shell.\n // Asserted directly because the sweep folds a non-zero `rm` into\n // `kept: 'delete-failed'` and would therefore hide it as a keep.\n const rerun = await handle.process.exec(journalCleanupCommand(paths))\n expect(rerun.exitCode).toBe(0)\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'leaves a filename it cannot decode alone, while still sweeping the terminal run beside it',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('undecodable')\n const paths = journalPaths(runId, dir)\n // `_1.` is not a two-hex-digit escape, so this name is `malformed` — the\n // shape a truncated or foreign file has. `decodeJournalRunId` must refuse\n // it, and the sweep must keep it WITHOUT asking the store, because a\n // plausible-but-wrong runId could answer `terminal` for someone else.\n const strayName = 'reaper-conformance-stray_1.ndjson'\n const strayPath = `${dir}/${strayName}`\n try {\n await runAgent(handle, paths, ['1'])\n await handle.process.exec(\n `printf 'not a journal\\\\n' >> ${quote(strayPath)}`,\n )\n expect(await fileExists(handle, strayPath)).toBe(true)\n expect(decodeJournalRunId(strayName).kind).toBe('malformed')\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({\n runId,\n threadId: `${runId}-t`,\n startedAt: Date.now(),\n })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([runId])\n expect(result.kept).toEqual([\n { names: [strayName], reason: 'undecodable-name' },\n ])\n expect(result.failures).toEqual([])\n expect({\n strayKept: await fileExists(handle, strayPath),\n journalDeleted: !(await fileExists(handle, paths.journal)),\n }).toEqual({ strayKept: true, journalDeleted: true })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 2. The age gate on a real shell.\n // -----------------------------------------------------------------------\n it(\n \"emits stat's self-witness line for a populated directory, so the age gate is usable\",\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('witness')\n const paths = journalPaths(runId, dir)\n try {\n await runAgent(handle, paths, ['1'])\n const listing = await mtimeListing(handle, dir)\n // The witness is what makes \"no files\" distinguishable from \"the\n // mechanism is unavailable\". On BusyBox 1.37 — the docker provider's\n // `alpine:3` — `find -newermt`/`-printf` are unrecognised and exit 1\n // with empty stdout, which is exactly why the design is a witness line\n // rather than a `find` and an exit code.\n expect(hasWitnessLine(listing.stdout, dir)).toBe(true)\n expect([...listing.entries.keys()].sort()).toEqual(\n [basename(dir, paths.journal), basename(dir, paths.stderr)].sort(),\n )\n // Real epoch times, not the parser's zeroes: a `%Y` the shell did not\n // expand would parse as no entry at all, and a `stat` that printed\n // something else would land far from now.\n for (const mtimeMs of listing.entries.values()) {\n expect(Math.abs(Date.now() - mtimeMs)).toBeLessThan(120_000)\n }\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'reports an EMPTY journal directory as witness-only rather than unavailable',\n { timeout: 60_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n try {\n await handle.process.exec(`mkdir -p ${quote(dir)}`)\n const listing = await mtimeListing(handle, dir)\n expect(hasWitnessLine(listing.stdout, dir)).toBe(true)\n expect([...listing.entries.keys()]).toEqual([])\n\n // And the sweep agrees: an empty directory is a LISTED age gate, not an\n // unavailable one. `'unavailable'` here would silently disable orphan\n // expiry forever on this provider.\n const result = await pruneJournals({\n handle,\n runs: new InMemoryRunStore(),\n dir,\n })\n expect({\n listed: result.listed,\n ageGate: result.ageGate,\n deleted: result.deleted,\n failures: result.failures,\n }).toEqual({\n listed: 0,\n ageGate: 'listed',\n deleted: [],\n failures: [],\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'keeps an orphan younger than orphanTtlMs and sweeps the older one, in the same pass',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const olderId = uniqueRunId('older')\n const newerId = uniqueRunId('newer')\n const older = journalPaths(olderId, dir)\n const newer = journalPaths(newerId, dir)\n try {\n await runAgent(handle, older, ['1'])\n // `stat -c '%Y'` is second-granular, so the two runs must be more than\n // one second apart for their ages to be distinguishable at all.\n await sleep(2_500)\n await runAgent(handle, newer, ['1'])\n\n // The cutoff is computed from the REAL mtimes the real shell reported,\n // not from a fabricated timestamp: that is the whole point of running\n // this against a provider. `pruneJournals` keeps when the NEWEST of a\n // run's files is strictly newer than the cutoff, so placing the cutoff\n // between the two runs must expire exactly one of them.\n const listing = await mtimeListing(handle, dir)\n const newestOf = (paths: JournalPaths): number =>\n Math.max(\n mtimeOf(listing.entries, basename(dir, paths.journal)),\n mtimeOf(listing.entries, basename(dir, paths.stderr)),\n )\n const olderMtime = newestOf(older)\n const newerMtime = newestOf(newer)\n expect(newerMtime - olderMtime).toBeGreaterThanOrEqual(1_000)\n\n const now = Date.now()\n const cutoff = olderMtime + Math.floor((newerMtime - olderMtime) / 2)\n // NEITHER run is in the store, so both take the orphan arm and only the\n // age gate decides between them.\n const result = await pruneJournals({\n handle,\n runs: new InMemoryRunStore(),\n dir,\n now,\n orphanTtlMs: now - cutoff,\n })\n expect(result.ageGate).toBe('listed')\n expect(result.deleted).toEqual([olderId])\n expect(result.kept).toEqual([\n {\n runId: newerId,\n names: expect.any(Array),\n reason: 'orphan-too-recent',\n },\n ])\n expect(result.failures).toEqual([])\n expect({\n olderJournal: await fileExists(handle, older.journal),\n olderSidecar: await fileExists(handle, older.stderr),\n newerJournal: await fileExists(handle, newer.journal),\n newerSidecar: await fileExists(handle, newer.stderr),\n }).toEqual({\n olderJournal: false,\n olderSidecar: false,\n newerJournal: true,\n newerSidecar: true,\n })\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 3. `reapDetachedRuns` end to end.\n // -----------------------------------------------------------------------\n it(\n 'finalizes a detached run whose agent reached its sentinel, and its transcript lands',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('finalize')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3']\n const paths = journalPaths(runId, dir)\n try {\n const runs = new InMemoryRunStore()\n const detachedSince = Date.now()\n await detachedRun(runs, runId, threadId, detachedSince)\n await runAgent(handle, paths, deltas)\n\n // The probe, on its own, before any sweep: this read is what makes the\n // reaper safe, and it must answer from the JOURNAL rather than from the\n // delivery log (which a detached run's dead host stopped appending to).\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'finished',\n exitCode: 0,\n })\n\n const log = conformanceLog()\n const journalDrive = driveFromJournal(handle, dir)\n const result = await reapDetachedRuns({\n runs,\n locks: new InMemoryLockStore(),\n durability: () => log.log,\n hasFinished: (record) =>\n probeRunExit({ handle, runId: record.runId, dir }),\n drive: journalDrive.drive,\n now: Date.now(),\n detachedRunTtlMs: NEVER_EXPIRES_MS,\n fenceQuietMs: FENCE_QUIET_MS,\n })\n\n // The causal witness, before anything downstream — see\n // {@link READ_BACKSTOP_MS}. The reaper's read ends at the sentinel; if\n // the clock ended it instead, the transcript below is short and the\n // failure must name the backstop rather than a missing chunk.\n expect({ backstopped: journalDrive.backstopped() }).toEqual({\n backstopped: false,\n })\n expect({\n considered: result.considered,\n probed: result.probed,\n finalized: result.outcomes.finalized,\n }).toEqual({ considered: 1, probed: 1, finalized: 1 })\n expect(result.runs).toEqual([\n { runId, outcome: 'finalized', status: 'completed', exitCode: 0 },\n ])\n // The transcript, element for element — the reaper's whole purpose is\n // that the run a nobody watched still ends up saved.\n expect(transcript(log.stored())).toEqual(\n transcript(expectedTranscript(runId, deltas)),\n )\n const record = await runs.get(runId)\n expect(record?.status).toBe('completed')\n // NEVER CLEARED: `detachedSince` is what the next sweep selects on, and\n // clearing it would reset the TTL on every pass.\n expect(record?.detachedSince).toBe(detachedSince)\n } finally {\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n it(\n 'reports a still-producing detached run as producing and leaves it completely untouched',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const runId = uniqueRunId('producing')\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, dir)\n const store = countingRunStore(new InMemoryRunStore())\n // A REAL agent that has written a line and is genuinely still alive: no\n // sentinel can be in the journal, and driving it would truncate a healthy\n // run's transcript at line one.\n const agent = await handle.process.spawn(\n journaledCommand(`${emitLines(['1'])}; sleep 30`, paths),\n )\n try {\n const detachedSince = Date.now()\n await detachedRun(store.runs, runId, threadId, detachedSince)\n await waitForJournal(handle, paths)\n const read = await handle.process.exec(journalReadCommand(paths, 0))\n const text = decodeJournalRead(read.stdout)\n // Producing, provably: the first line is there and the sentinel is not.\n expect(text).toContain('{\"delta\":\"1\"}')\n expect(text).not.toContain('__exit')\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'producing',\n })\n\n const log = conformanceLog()\n let driveCalled = false\n let durabilityCalls = 0\n const result = await reapDetachedRuns({\n runs: store.runs,\n locks: new InMemoryLockStore(),\n durability: () => {\n durabilityCalls += 1\n return log.log\n },\n hasFinished: (record) =>\n probeRunExit({ handle, runId: record.runId, dir }),\n drive: () => {\n driveCalled = true\n return (async function* never() {})()\n },\n now: Date.now(),\n detachedRunTtlMs: NEVER_EXPIRES_MS,\n fenceQuietMs: FENCE_QUIET_MS,\n })\n\n expect(result.runs).toEqual([{ runId, outcome: 'producing' }])\n expect({\n considered: result.considered,\n probed: result.probed,\n producing: result.outcomes.producing,\n finalized: result.outcomes.finalized,\n expired: result.outcomes.expired,\n failed: result.outcomes.failed,\n }).toEqual({\n considered: 1,\n probed: 1,\n producing: 1,\n finalized: 0,\n expired: 0,\n failed: 0,\n })\n\n // ABSENCE, asserted in one object so a regression names which\n // guarantee broke instead of failing on whichever line came first.\n // Every one of these is a way the pre-`probeRunExit` design destroyed a\n // live run: an append duplicates its prefix, a `close()` ends every\n // attached client's stream, an `update` writes `'completed'` and drops\n // the run out of `listReclaimable` forever, and a moved\n // `detachedSince` restarts its TTL.\n const record = await store.runs.get(runId)\n expect({\n driveCalled,\n durabilityCalls,\n appended: log.stored().length,\n closes: log.closes(),\n updatesAfterSetup: store.updates() - 1,\n status: record?.status,\n detachedSince: record?.detachedSince,\n driverEpoch: record?.driverEpoch,\n }).toEqual({\n driveCalled: false,\n durabilityCalls: 0,\n appended: 0,\n closes: 0,\n updatesAfterSetup: 0,\n status: 'running',\n detachedSince,\n driverEpoch: undefined,\n })\n } finally {\n // The agent outlives the sweep on purpose; reap it here so no `sleep`\n // survives the case.\n try {\n await agent.kill()\n } catch {\n // Already gone, or a provider whose sandbox teardown covers it.\n }\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n\n // -----------------------------------------------------------------------\n // 4. A shell-hostile runId, end to end. SECURITY-RELEVANT.\n // -----------------------------------------------------------------------\n it(\n 'round-trips a shell-hostile runId through encode, journal, follow, sidecar read, list, decode and delete without executing any of it' +\n (config.followUnsupported === undefined\n ? ''\n : ` (follow read omitted: ${config.followUnsupported.reason})`),\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const dir = caseDir()\n const nonce = randomUUID().slice(0, 8)\n // The canary lives OUTSIDE `dir` so the teardown `rm -rf` cannot be what\n // makes the final assertion pass.\n const canary = `/tmp/rp-pwn-${nonce}`\n // `/` would escape the directory, the space would split the word, `;` and\n // `$( )` would start new commands, and the `touch` is a real payload with\n // an observable effect. Every one of these must survive as DATA.\n //\n // THE ORDER OF THE PAYLOAD IS DELIBERATE and was measured: the `;touch`\n // comes BEFORE the space and the `/`. With raw interpolation, the\n // journaled command's redirect target is one word, so a payload whose\n // space precedes the `;` (`rp-a b;touch …`) makes the mangled command a\n // SYNTAX ERROR — the injected `touch` never runs and the canary below\n // would be decoration that can never fire. With the `;` first, the\n // vulnerable form parses as a command LIST and the payload really\n // executes (verified against a hand-composed unquoted, unencoded command\n // on this provider: canary present). So the canary is a live detector.\n const runId = `rp-a;touch ${canary};b c/d$(x)-${nonce}`\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, dir)\n const journalName = basename(dir, paths.journal)\n try {\n // This case's agent writes to STDERR as well, so the sidecar read\n // below has real bytes to compare against: an empty sidecar is also\n // what a `journalStderrReadCommand` that read the wrong path (or\n // nothing at all) would return, and that read is the only coverage\n // that command has anywhere.\n await handle.process.exec(\n journaledCommand(\n `${emitLines(['1'])}; printf 'boom\\\\n' 1>&2`,\n paths,\n ),\n )\n // THE SECURITY ASSERTION, and deliberately the FIRST one: nothing the\n // runId contains was executed. It is stated before the cheaper\n // structural checks below on purpose — a defect that reintroduces raw\n // interpolation would also fail the filename shape, and a case that\n // short-circuited there would never prove this probe is live rather\n // than decorative. Re-asserted after the delete, because the sweep\n // composes a DIFFERENT command (`rm -f`) from the same id.\n expect(await fileExists(handle, canary)).toBe(false)\n expect(await probeRunExit({ handle, runId, dir })).toEqual({\n state: 'finished',\n exitCode: 0,\n })\n // The encoding is what bought that: the filename carries no character\n // a shell can act on, and it stays inside the journal directory.\n expect(journalName).toMatch(/^[A-Za-z0-9._-]+\\.ndjson$/)\n expect(paths.journal.startsWith(`${dir}/`)).toBe(true)\n\n // THE FOLLOW PATH, against this same hostile id.\n //\n // `journalFollowCommand` is the WORST command in the set under a\n // dropped `encodeRunId`: it interpolates the journal path THREE times\n // (`mkdir -p`, `: >> path`, `tail -c +N -f path`) and joins its prep\n // steps with `;` rather than `&&`, so `: >> /tmp/dir/rp-a;touch\n // <canary>;…` is a complete redirect followed by a command LIST — the\n // payload runs on EVERY attach, and a failing prep step does not stop\n // it. Nothing else reaches this command with a hostile runId: the\n // reaper's own probes are all bounded reads, and the takeover suite,\n // the only other real-provider consumer of the follow path, builds\n // alnum-only ids. So it is exercised here, where the hostile id and a\n // live canary already exist, for the cost of one read.\n //\n // The strategy is FORCED rather than capability-derived so this is the\n // follow command and not the bounded one, and the declaration is\n // checked against the live handle in both directions — a config that\n // does not describe the provider must fail rather than silently drop\n // this read.\n expect(journalReadStrategy(handle)).toBe(\n config.followUnsupported === undefined ? 'follow' : 'poll',\n )\n if (config.followUnsupported === undefined) {\n const followed: Array<string> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI — a `tail -f` never ends on its own, so this read has no\n // other floor. Not the assertion — `backstopped` below proves it was\n // not what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const line of readJournal(handle, {\n paths,\n fromByte: 0,\n strategy: 'follow',\n signal: backstop,\n })) {\n followed.push(line.line)\n // The agent has already reached its sentinel, so this arrives; a\n // `tail -f` never ends on its own.\n if (line.line.includes(EXIT_SENTINEL_KEY)) break\n }\n // The causal witness, first: the loop must end on the sentinel\n // `break`, not on the clock. A backstopped follow read otherwise\n // reports as a one-element-vs-two array diff that says nothing about\n // why.\n expect({ backstopped: backstop.aborted }).toEqual({\n backstopped: false,\n })\n expect(followed).toEqual([\n '{\"delta\":\"1\"}',\n exitSentinelLine(paths, 0),\n ])\n expect(await fileExists(handle, canary)).toBe(false)\n }\n\n // The stderr SIDECAR read, which no other conformance case reaches at\n // all. Same hostile id, same canary, one `exec`.\n const sidecar = await handle.process.exec(\n journalStderrReadCommand(paths),\n )\n expect(decodeJournalRead(sidecar.stdout)).toBe('boom\\n')\n expect(await fileExists(handle, canary)).toBe(false)\n\n // encode → journal → list → decode: the sweep's actual path back to a\n // runId, over a real `ls -1`.\n const names = await listNames(handle, dir)\n expect(names.sort()).toEqual(\n [journalName, basename(dir, paths.stderr)].sort(),\n )\n expect(decodeJournalRunId(journalName)).toEqual({\n kind: 'runId',\n runId,\n })\n\n const runs = new InMemoryRunStore()\n await runs.createOrResume({ runId, threadId, startedAt: Date.now() })\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n const result = await pruneJournals({ handle, runs, dir })\n expect(result.deleted).toEqual([runId])\n expect(result.failures).toEqual([])\n expect({\n journalDeleted: !(await fileExists(handle, paths.journal)),\n sidecarDeleted: !(await fileExists(handle, paths.stderr)),\n canaryAbsent: !(await fileExists(handle, canary)),\n }).toEqual({\n journalDeleted: true,\n sidecarDeleted: true,\n canaryAbsent: true,\n })\n } finally {\n await handle.process\n .exec(`rm -f ${quote(canary)}`)\n .catch(() => undefined)\n await removeDir(handle, dir)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
@@ -141,7 +141,7 @@ function conformanceLog() {
141
141
  });
142
142
  return offset;
143
143
  })),
144
- read: () => void 0,
144
+ read: () => (async function* empty() {})(),
145
145
  close: () => {
146
146
  closes += 1;
147
147
  return Promise.resolve();
@@ -352,7 +352,7 @@ function driverFor(input) {
352
352
  journal: journalOptions(durability, runId)
353
353
  });
354
354
  const gated = input.beforeFirstChunk;
355
- return alignedIfAttaching(translate(runId, gated === void 0 ? lines : (async function* afterGate() {
355
+ const source = gated === void 0 ? lines : (async function* afterGate() {
356
356
  let first = true;
357
357
  for await (const value of lines) {
358
358
  if (first) {
@@ -361,7 +361,8 @@ function driverFor(input) {
361
361
  }
362
362
  yield value;
363
363
  }
364
- })()), durability);
364
+ })();
365
+ return alignedIfAttaching(translate(runId, source), durability);
365
366
  }
366
367
  }),
367
368
  backstopped: () => backstops.some((s) => s.aborted)