@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.
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/journal-sweep.js +3 -2
- package/dist/esm/journal-sweep.js.map +1 -1
- package/dist/esm/middleware.js +2 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/reap.js +2 -1
- package/dist/esm/reap.js.map +1 -1
- package/dist/esm/sandbox.js +1 -1
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/shell.js +5 -4
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.js +4 -2
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/testkit/journal-conformance.js +6 -3
- package/dist/esm/testkit/journal-conformance.js.map +1 -1
- package/dist/esm/testkit/reaper-conformance.js +13 -8
- package/dist/esm/testkit/reaper-conformance.js.map +1 -1
- package/dist/esm/testkit/takeover-conformance.js +4 -3
- package/dist/esm/testkit/takeover-conformance.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"takeover-conformance.js","names":[],"sources":["../../../src/testkit/takeover-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for TAKEOVER: a second driver picking up a run whose\n * first driver died, against a REAL sandbox.\n *\n * WHY THIS EXISTS SEPARATELY FROM THE UNIT TESTS. Every takeover unit test in\n * this package drives fakes — a scripted `spawn`, a `test -f` that answers from\n * a boolean, a log that is an array. Fakes model what we believe the shell and\n * the filesystem do, and on this feature that belief has been wrong three times:\n * `base64` delivers zero bytes on a live pipe, `tail -f` on a missing file exits\n * instead of waiting, and a provider's `kill` does not always reap a grandchild.\n * Each one passed every fake. So the four properties a takeover actually rests\n * on are asserted here through a provider's real `spawn`/`exec` against a real\n * journal file:\n *\n * 1. **The delivered sequence is the run's sequence, with no duplicated\n * prefix.** Asserted as a TRANSCRIPT, never as \"chunks arrived\": a takeover\n * that replays the whole journal and re-appends everything satisfies the weak\n * assertion while showing the user the entire run twice. That is the exact\n * failure `alignToStoredLog` exists to prevent, and the only assertion that\n * can see it is one that compares the stored log to the expected sequence\n * element for element.\n * 2. **The attach preflight decides, or fails, but never hangs.** It probes with\n * the provider's real `exec` (`test -f`), which is the layer where a fake's\n * assumptions break, and its three verdicts (`unknown-run`, `terminal-run`,\n * `journal-timeout`) plus the legitimate late-journal race are all timing\n * against a real filesystem.\n * 3. **The epoch fence and its latch hold under real concurrency.** Two drivers\n * reading one real journal at once: the second wins, the first appends\n * NOTHING — not even `pipeToRunLog`'s recovery `RUN_ERROR` — and cannot\n * terminalize the record out from under the live successor.\n * 4. **A terminal run's journal is deleted, and a later attach says so.** The\n * deletion is a real `rm` of real files, and the follow-up attach must report\n * `terminal-run` rather than tailing the file that `journalFollowCommand`\n * would helpfully re-create.\n *\n * WHAT IS REAL HERE. The provider (its `spawn`, `exec`, and shell), the journal\n * (a real NDJSON file the agent's stdout is redirected into), the agent (a real\n * process writing real lines with a real pause in the middle), the reader\n * (`readJournalNdjson`, including the follow/poll strategy split and the attach\n * preflight), the alignment (`alignedIfAttaching` over the real\n * `resolveSandboxDurability` output), the claim and BOTH fences\n * (`sandboxRunDriver`), and the run record (`InMemoryRunStore`). The event log is\n * in-process, exactly as the recommended `memoryStream` backend is.\n *\n * A provider that cannot satisfy the contract MUST declare `unsupported.reason`.\n * As in the journal suite there is deliberately no silent-skip path: a\n * conformance case that quietly returns prints as a pass, which is how an\n * unimplemented capability ships green.\n *\n * FOUND BY THIS SUITE, FIXED IN THE PROVIDER, STILL NOT ASSERTED HERE. On\n * local-process under Windows (git-bash `sh`), the follow read's `tail`\n * grandchild used to SURVIVE `proc.kill()`: `LocalProcessHandle.killTree` ran\n * `taskkill /PID <sh> /T /F` and returned as soon as `spawnSync` reported no\n * `error`. Two things were wrong. It never checked taskkill's exit status — and\n * that alone would not have caught it, because MSYS's fork emulation leaves the\n * `tail.exe` pointing at an intermediate shell that has already exited, so\n * `taskkill /T` (live parent links only) cannot reach it and still exits `0`.\n * Measured by counting `tail.exe` before and after a run: this suite leaked 4 per\n * run and the shipped journal suite 2, accumulating for the life of the machine.\n * It was a provider defect, not a takeover defect — every case here still\n * delivered the right transcript, because `untilAborted` (see\n * `journal-reader.ts`) stops honoring the pipe once the signal fires rather than\n * waiting for the kill, which is exactly why it never failed a test.\n * `killTree` now resolves the tree through MSYS's own process table and verifies\n * the survivors are gone (0 per run), covered in\n * `ai-sandbox-local-process/tests/kill-tree.test.ts`.\n * Deliberately still NOT asserted in this suite: a per-provider process census is\n * not portable (Docker's `tail` dies with its container), and a conformance case\n * that counted host processes would fail for reasons unrelated to takeover.\n *\n * EVERY WAIT IN THIS FILE IS BOUNDED. A hang stalls CI instead of failing it, so\n * each journal read carries a timeout signal, each poll loop carries a deadline\n * and a message naming what never happened, and each case carries an explicit\n * per-test timeout.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { describe, expect, it } from 'vitest'\nimport { EventType, InMemoryRunStore } from '@tanstack/ai'\nimport { InMemoryLockStore } from '@tanstack/ai/locks'\nimport {\n journalCleanupCommand,\n journalExistsCommand,\n journalPaths,\n journaledCommand,\n} from '../journal'\nimport { readJournalNdjson, startJournaledAgent } from '../runner'\nimport {\n JournalAttachUnavailableError,\n awaitAttachableJournal,\n} from '../attach-preflight'\nimport {\n alignedIfAttaching,\n journalOptionsFor,\n resolveSandboxDurability,\n} from '../durability'\nimport { sandboxRunDriver } from '../driver'\nimport { fenceDurability, withRunClaim } from '../claim'\nimport { chunkFingerprint, createRunScopedIdGen } from '../chunk-identity'\nimport type { SandboxRunDurability } from '../durability'\nimport type { JournalOptions } from '../runner'\nimport type { SandboxHandle } from '../contracts'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai'\n\nexport interface TakeoverConformanceConfig {\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 takeover, 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\n/**\n * Journal directory for this suite, deliberately NOT\n * {@link DEFAULT_JOURNAL_DIR}: on local-process the sandbox shell shares the\n * host's real `/tmp`, so conformance runs must not write where an application's\n * runs live.\n */\nconst CONFORMANCE_JOURNAL_DIR = '/tmp/tanstack-takeover-conformance'\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 successor's first append. Short because the\n * predecessor in these cases has provably stopped (the suite sequenced it) —\n * the 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/**\n * Unique per case, and it must be: `journalPaths` derives the file name 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`). The counter covers two cases created inside the\n * same millisecond; the random suffix covers two suites sharing one `/tmp`.\n */\nlet caseCounter = 0\nfunction uniqueRunId(label: string): string {\n caseCounter += 1\n const suffix = Math.random().toString(36).slice(2, 8)\n return `tko-${label}-${Date.now()}-${caseCounter}-${suffix}`\n}\n\n/**\n * An in-process event log with real accumulated state, plus the two facts the\n * assertions need: what is stored (in append order) and how many times `close()`\n * ran.\n *\n * `snapshot()` returns fresh objects, per the `StreamDurability` contract, so a\n * caller cannot reach the stored log through the result.\n */\ninterface ConformanceLog {\n log: StreamDurability\n /** Stored chunks, in append order. The transcript under test. */\n stored: () => Array<StreamChunk>\n /** `close()` calls — proof that `close` is NOT fenced. */\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 = `conf:${entries.length}`\n entries.push({ offset, chunk })\n return offset\n }),\n ),\n // Nothing in this suite tails the log — every assertion reads the stored\n // transcript with `snapshot()`, which is also what `alignToStoredLog`\n // uses, 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 lock that grants every request immediately and never reports a loss.\n *\n * `InMemoryLockStore` SERIALIZES claims within one process, so a second attach\n * waits for the first to finish and the two drivers are never concurrent — which\n * means the epoch fence can never be observed there. `claim.ts` says exactly\n * that: in one process only layer 2, the `driverEpoch` fence, is provable. This\n * models a lease-less lock so the two drives overlap and layer 2 does the work.\n */\nconst permissiveLocks: LockStore = {\n withLock: (_key, fn) => fn(new AbortController().signal),\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 is REBUILT from them rather than asserted\n * into shape: a cast would let a provider that mangles the bytes (a folded\n * stderr diagnostic, a truncated line) reach `chunkFingerprint` as a\n * structurally invalid chunk and fail somewhere 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 `takeover 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 `takeover conformance: run ${runId} journal line has a non-string delta: ${JSON.stringify(value)}`,\n )\n }\n return contentChunk(messageId, delta)\n}\n\n/**\n * The translator. Deterministic by construction, which is what makes alignment\n * possible at all: the message id comes from {@link createRunScopedIdGen}, so\n * re-translating the same journal from byte 0 reproduces byte-identical chunks\n * (modulo `timestamp`, the one field `chunkFingerprint` excludes).\n */\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/**\n * A comparable transcript: each chunk reduced to its {@link chunkFingerprint}.\n *\n * The fingerprint, not the chunk object, and for the same reason alignment uses\n * it — `timestamp` is wall-clock and unreproducible, so a raw `toEqual` on\n * chunks would fail on the one field the feature deliberately ignores. Every\n * other field participates, so a duplicated prefix, a dropped chunk, or a\n * reordered one still fails.\n */\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 * A real agent: a shell command that prints one NDJSON line per delta, with an\n * optional real pause partway through, then exits.\n *\n * `printf '%s\\n' a b c` reuses the format for every operand on GNU coreutils and\n * on busybox alike, so this needs no loop. The JSON contains only double quotes,\n * so it is safe inside the POSIX single-quoted words this builds.\n */\nfunction agentCommand(deltas: Array<string>, pauseAfter: number): string {\n const line = (delta: string): string => `'{\"delta\":\"${delta}\"}'`\n const head = deltas.slice(0, pauseAfter)\n const tail = deltas.slice(pauseAfter)\n const parts = [`printf '%s\\\\n' ${head.map(line).join(' ')}`]\n if (tail.length > 0) {\n // A real sleep, so the takeover below happens while the agent is genuinely\n // still writing rather than against a finished file.\n parts.push('sleep 2', `printf '%s\\\\n' ${tail.map(line).join(' ')}`)\n }\n return parts.join('; ')\n}\n\n/** Resolve durability through the production resolver, fresh or attaching. */\nfunction durabilityFor(\n runs: RunStore,\n log: StreamDurability,\n attach: boolean,\n): SandboxRunDurability {\n const resolved = resolveSandboxDurability({\n runs,\n durability: {\n adapter: log,\n journal: CONFORMANCE_JOURNAL_DIR,\n attach,\n pollIntervalMs: POLL_INTERVAL_MS,\n },\n })\n if (resolved === undefined) {\n throw new Error(\n 'takeover conformance: resolveSandboxDurability returned undefined for a fully wired run',\n )\n }\n return resolved\n}\n\n/**\n * The reader's journal options for a resolved durability.\n *\n * `journalOptionsFor` answers `undefined` for a NON-durable run, which cannot\n * happen here — every run in this suite is fully wired. Narrowing it with a\n * thrown error rather than a non-null assertion keeps the impossible case loud\n * if the resolver's contract ever changes.\n */\nfunction journalOptions(\n durability: SandboxRunDurability,\n runId: string,\n): JournalOptions {\n const options = journalOptionsFor(durability, runId)\n if (options === undefined) {\n throw new Error(\n `takeover conformance: journalOptionsFor answered undefined for durable run ${runId}`,\n )\n }\n return options\n}\n\n/** A `'running'` record for `runId`, ready to be claimed. */\nasync function runningRun(\n runId: string,\n threadId: string,\n): Promise<InMemoryRunStore> {\n const runs = new InMemoryRunStore()\n await runs.createOrResume({ runId, threadId, startedAt: Date.now() })\n return runs\n}\n\n/**\n * Wrap a handle so the `process.exec` calls ONE operation makes can be counted.\n *\n * This is how the attach preflight's fail-fast cases are anchored, and the reason\n * they are not anchored on elapsed time. `awaitAttachableJournal` runs exactly one\n * `test -f` before it consults the run store, so a decision made from the record\n * costs one `exec` and a decision made by waiting costs one per\n * `probeIntervalMs`. The count separates those two behaviors exactly; elapsed time\n * does not, because a single `exec` is a provider round-trip whose latency the\n * suite does not control — a `docker exec` on a loaded daemon has been measured at\n * 9.6s, which fails a `< 4_000ms` bound while the preflight under test did\n * precisely the right thing. A timing bound that goes red on a busy machine\n * teaches people to ignore the suite.\n *\n * The spread copies the handle's own methods, so everything except `exec` is the\n * provider's; the wrapper delegates rather than reimplementing.\n */\nfunction countingExec(handle: SandboxHandle): {\n handle: SandboxHandle\n execs: () => number\n} {\n let execs = 0\n return {\n handle: {\n ...handle,\n process: {\n ...handle.process,\n exec: (command, options) => {\n execs += 1\n return handle.process.exec(command, options)\n },\n },\n },\n execs: () => execs,\n }\n}\n\n/** Poll `check` until it answers true, or fail with a message naming what never happened. */\nasync function waitUntil(\n check: () => Promise<boolean>,\n options: { timeoutMs: number; message: string },\n): Promise<void> {\n const deadline = Date.now() + options.timeoutMs\n for (;;) {\n if (await check()) return\n if (Date.now() > deadline) {\n throw new Error(\n `takeover conformance: ${options.message} within ${options.timeoutMs}ms`,\n )\n }\n await sleep(25)\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\ninterface Gate {\n promise: Promise<void>\n open: () => void\n}\n\n/** A one-shot gate, for sequencing two concurrent drivers deterministically. */\nfunction gate(): Gate {\n let open = (): void => {}\n const promise = new Promise<void>((resolve) => {\n open = () => resolve()\n })\n return { promise, open }\n}\n\n/**\n * Build the driver a host would build for one run.\n *\n * `drive` is the real journal path: read the run's journal from byte 0 (through\n * the attach preflight when attaching), translate, and align against the stored\n * log — `alignedIfAttaching`, so alignment runs on an attach and only on an\n * attach.\n *\n * Returns the driver alongside `backstopped()`, the causal witness for\n * {@link READ_BACKSTOP_MS}: every case that drives this must assert it is\n * `false` before its transcript assertions, so a read the CLOCK ended fails\n * naming the backstop instead of as a truncated-transcript diff.\n */\nfunction driverFor(input: {\n handle: SandboxHandle\n runs: RunStore\n locks: LockStore\n log: StreamDurability\n runId: string\n attach: boolean\n /** Awaited before the FIRST translated chunk is yielded, never after. */\n beforeFirstChunk?: () => Promise<void>\n}): {\n driver: ReturnType<typeof sandboxRunDriver>\n /** True if any read this driver started was ended by the backstop clock. */\n backstopped: () => boolean\n} {\n const durability = durabilityFor(input.runs, input.log, input.attach)\n // One entry per `drive` invocation, so a re-drive cannot hide a backstopped\n // read behind a healthy one.\n const backstops: Array<AbortSignal> = []\n const driver = sandboxRunDriver({\n request: new Request(\n `http://takeover.local/attach?runId=${encodeURIComponent(input.runId)}&offset=-1`,\n ),\n runs: input.runs,\n locks: input.locks,\n durability: () => input.log,\n fenceQuietMs: FENCE_QUIET_MS,\n drive: ({ runId, signal }) => {\n // The read is bounded independently of `signal`: an `InMemoryLockStore`\n // hands out a signal it never aborts, so a journal that stops growing\n // would otherwise park this read forever and turn a broken takeover into a\n // hung CI job instead of a failing assertion.\n //\n // Not the assertion — see {@link READ_BACKSTOP_MS}. `backstopped()` below\n // is what proves the clock was not what ended the read.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n backstops.push(backstop)\n const bounded = AbortSignal.any([signal, backstop])\n const lines = readJournalNdjson(input.handle, {\n signal: bounded,\n journal: journalOptions(durability, runId),\n })\n const gated = input.beforeFirstChunk\n const source =\n gated === undefined\n ? lines\n : (async function* afterGate() {\n let first = true\n for await (const value of lines) {\n if (first) {\n first = false\n await gated()\n }\n yield value\n }\n })()\n return alignedIfAttaching(translate(runId, source), durability)\n },\n })\n return { driver, backstopped: () => backstops.some((s) => s.aborted) }\n}\n\n/** Exactly what core's `startRunDriver` does: claim, then pipe the drive. */\nfunction takeOver(\n driver: ReturnType<typeof sandboxRunDriver>,\n input: { runs: RunStore; runId: string; threadId: string },\n): Promise<unknown> {\n const { runs, runId, threadId } = input\n return driver.claim({ runs, locks: driver.locks, runId }, (claim) =>\n driver.pipe(driver.drive({ runId, threadId, signal: claim.signal }), {\n runId,\n threadId,\n signal: claim.signal,\n }),\n )\n}\n\n/** Best-effort removal of a case's journal files, through the shell (rule 3). */\nasync function cleanup(handle: SandboxHandle, runId: string): Promise<void> {\n try {\n await handle.process.exec(\n journalCleanupCommand(journalPaths(runId, CONFORMANCE_JOURNAL_DIR)),\n )\n } catch {\n // The sandbox may already be gone. Nothing under test depends on the files\n // being absent afterwards — the cases that DO assert deletion assert it\n // directly.\n }\n}\n\n/**\n * Assert `createHandle` satisfies the takeover conformance contract. Each `it`\n * gets a fresh sandbox via `createHandle`/`dispose`, and a unique `runId`, so no\n * case can observe another's journal.\n */\nexport function runTakeoverConformance(\n config: TakeoverConformanceConfig,\n): void {\n describe(`takeover 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. A real takeover, end to end.\n // ---------------------------------------------------------------------\n it(\n 'delivers the run sequence exactly once when a second driver takes over mid-stream',\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('e2e')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3', '4', '5', '6']\n const prefixLength = 3\n const expected = expectedTranscript(runId, deltas)\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n try {\n const fresh = durabilityFor(runs, log.log, false)\n\n // THE HOST THAT DIES. A real claim, a real fence, a real journal read\n // of a real agent — and then it stops after `prefixLength` chunks\n // without closing the log and without terminalizing the record, which\n // is what a host vanishing looks like from the outside.\n const deliveredByFirst: Array<StreamChunk> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. Not the assertion — `backstopped` below proves it was not\n // what ended the loop.\n const firstBackstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n await withRunClaim(\n { runs, locks: new InMemoryLockStore(), runId },\n async (claim) => {\n const fenced = fenceDurability(log.log, claim, { runs })\n await startJournaledAgent(\n handle,\n agentCommand(deltas, prefixLength),\n { journal: journalOptions(fresh, runId) },\n )\n const lines = readJournalNdjson(handle, {\n signal: firstBackstop,\n journal: journalOptions(fresh, runId),\n })\n for await (const chunk of translate(runId, lines)) {\n await fenced.append([chunk])\n deliveredByFirst.push(chunk)\n // Breaking ends the reader's `tail` before this host walks away;\n // the AGENT keeps running, which is the whole premise.\n if (deliveredByFirst.length === prefixLength) break\n }\n },\n )\n // The causal witness for the dying host's read: it must stop because\n // the consumer broke at `prefixLength`, not because the clock ran out.\n // A backstopped read here delivers a short prefix and the takeover the\n // case exists to exercise would start from the wrong offset.\n expect({ backstopped: firstBackstop.aborted }).toEqual({\n backstopped: false,\n })\n expect(transcript(deliveredByFirst)).toEqual(\n transcript(expected.slice(0, prefixLength)),\n )\n\n // THE SUCCESSOR. Same runId, same journal, a fresh claim.\n const successor = driverFor({\n handle,\n runs,\n locks: new InMemoryLockStore(),\n log: log.log,\n runId,\n attach: true,\n })\n const record = await takeOver(successor.driver, {\n runs,\n runId,\n threadId,\n })\n\n // THE CAUSAL WITNESS, first — see {@link READ_BACKSTOP_MS}. The\n // transcript assertions below can only speak about chunks that\n // arrived; this one says the successor's read ended because the\n // journal ended, not because the clock did. Without it a backstopped\n // read reports as a confusing short-transcript diff.\n expect({ backstopped: successor.backstopped() }).toEqual({\n backstopped: false,\n })\n\n // THE TRANSCRIPT, element for element. This is the assertion that can\n // see the failure the feature exists to prevent: a takeover that\n // replays the journal from byte 0 without aligning re-appends the\n // prefix, so `stored` would be 9 entries beginning `1,2,3,1,2,3,…` —\n // and the user would watch the first half of the run twice. \"Chunks\n // arrived\" passes against that; this does not.\n expect(transcript(log.stored())).toEqual(transcript(expected))\n // Stated separately so a failure reads as what it is rather than as a\n // 9-vs-6 array diff.\n expect(log.stored()).toHaveLength(deltas.length)\n expect(transcript(log.stored().slice(prefixLength))).toEqual(\n transcript(expected.slice(prefixLength)),\n )\n\n const finalRecord = await runs.get(runId)\n expect(finalRecord?.status).toBe('completed')\n // The successor's claim, not the predecessor's: a hardcoded epoch\n // would read 1 here and every takeover would be fenced out.\n expect(finalRecord?.driverEpoch).toBe(2)\n expect(record).not.toBeUndefined()\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 2. The attach preflight, against a real filesystem.\n // ---------------------------------------------------------------------\n it(\n 'fails an attach to an unknown runId with unknown-run, without waiting it out',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('unknown')\n try {\n expect.hasAssertions()\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs: new InMemoryRunStore(),\n // Generous on purpose: were the store verdict skipped, this would\n // poll for the full 8s and the probe count below would catch it.\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('unknown-run')\n // Decided from the RECORD, not by waiting it out: one `test -f`, then\n // the store. A preflight that polled to the deadline would run ~80\n // probes here. See `countingExec` for why this is not a stopwatch.\n expect(probes.execs()).toBe(1)\n } finally {\n await dispose()\n }\n },\n )\n\n it(\n 'fails an attach to a terminal run whose journal is gone with terminal-run',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('terminal')\n const threadId = `${runId}-t`\n try {\n expect.hasAssertions()\n const runs = await runningRun(runId, threadId)\n await runs.update(runId, { status: 'completed', finishedAt: 2 })\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs,\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('terminal-run')\n // One `test -f`, then the record. Not a stopwatch — `countingExec`.\n expect(probes.execs()).toBe(1)\n } finally {\n await dispose()\n }\n },\n )\n\n it(\n 'waits for a live run whose journal appears late — the legitimate race',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('race')\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, CONFORMANCE_JOURNAL_DIR)\n try {\n const runs = await runningRun(runId, threadId)\n // A real driver writing its real first line ~400ms after the attach\n // starts probing. This is the NORMAL case — `journalFollowCommand`'s\n // `: >> file` exists for it — so failing fast here would reintroduce\n // the defect that fix cured.\n const writer = sleep(400).then(() =>\n handle.process.exec(\n journaledCommand(`printf '{\"delta\":\"1\"}\\\\n'`, paths),\n ),\n )\n try {\n await awaitAttachableJournal(handle, {\n paths,\n runId,\n runs,\n // Comfortably longer than the write above; the per-test timeout is\n // what turns a never-resolving wait into a failure.\n waitMs: 20_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n })\n } finally {\n await writer\n }\n // Resolving at all is the assertion; this pins the premise that it\n // resolved because the file really is there now.\n expect(\n (await handle.process.exec(journalExistsCommand(paths))).exitCode,\n ).toBe(0)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n it(\n 'bounds the wait for a live run whose journal never appears, with journal-timeout',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('timeout')\n const threadId = `${runId}-t`\n try {\n expect.hasAssertions()\n const runs = await runningRun(runId, threadId)\n const error = await awaitAttachableJournal(handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs,\n waitMs: 600,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('journal-timeout')\n // The three assertions above ARE the proof the bound was applied: an\n // unbounded wait never produces a `JournalAttachUnavailableError` at\n // all, and `'600ms'` in the message is the configured bound reported\n // back. No stopwatch assertion here on purpose — the case's own\n // `{ timeout: 120_000 }` already converts an unbounded wait into a\n // failure, and a wall-clock ceiling would red a CORRECT implementation\n // on a machine where one `docker exec` was measured at 95s.\n expect(error.message).toContain('600ms')\n } finally {\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 3. The epoch fence and the shared latch, under real concurrency.\n // ---------------------------------------------------------------------\n it(\n 'lets the second of two concurrent drivers win, and the loser appends nothing at all',\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('fence')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3']\n const expected = expectedTranscript(runId, deltas)\n try {\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n // One agent, one journal, two drivers reading it concurrently.\n await startJournaledAgent(\n handle,\n agentCommand(deltas, deltas.length),\n {\n journal: journalOptions(\n durabilityFor(runs, log.log, false),\n runId,\n ),\n },\n )\n\n // The LOSER: the original host, so it does not align (there is nothing\n // stored when it starts). Gated before its first chunk reaches the\n // log, which is where the fence has to catch it — after the successor\n // has claimed and finished. The gate sits INSIDE the source stream, so\n // the loser's alignment snapshot (were it attaching) and its first\n // append both happen after the release, exactly as a host paused by a\n // GC or a VM suspend would.\n const released = gate()\n const losingDriver = driverFor({\n handle,\n runs,\n locks: permissiveLocks,\n log: log.log,\n runId,\n attach: false,\n beforeFirstChunk: () => released.promise,\n })\n const loser = takeOver(losingDriver.driver, {\n runs,\n runId,\n threadId,\n })\n await waitUntil(\n async () => ((await runs.get(runId))?.driverEpoch ?? 0) >= 1,\n {\n timeoutMs: 30_000,\n message: `the first driver never claimed run ${runId}`,\n },\n )\n\n // The WINNER: claims at a higher epoch and drives the run to the end.\n const winner = driverFor({\n handle,\n runs,\n locks: permissiveLocks,\n log: log.log,\n runId,\n attach: true,\n })\n await takeOver(winner.driver, { runs, runId, threadId })\n // The causal witness, before the transcript — see\n // {@link READ_BACKSTOP_MS}. The winner drives the run to its sentinel,\n // so a clock-ended read here must say so rather than surface as a\n // missing chunk.\n expect({ backstopped: winner.backstopped() }).toEqual({\n backstopped: false,\n })\n expect(transcript(log.stored())).toEqual(transcript(expected))\n\n // Now let the superseded host try to write.\n released.open()\n await loser\n // The causal witness, and here it is load-bearing rather than merely\n // diagnostic: the loser's gate is awaited from INSIDE its read, so a\n // backstopped read would abandon the stream during the wait, the loser\n // would never attempt an append at all, and every \"nothing lands\"\n // assertion below would pass vacuously without the fence ever running.\n expect({ backstopped: losingDriver.backstopped() }).toEqual({\n backstopped: false,\n })\n\n // NOTHING lands — not the run's chunks a second time, and not\n // `pipeToRunLog`'s recovery `RUN_ERROR` either. That log belongs to the\n // WINNER: a terminal `RUN_ERROR` from a dead host would fail the stream\n // for every client attached to the live, healthy run.\n expect(transcript(log.stored())).toEqual(transcript(expected))\n expect(\n log.stored().some((chunk) => chunk.type === EventType.RUN_ERROR),\n ).toBe(false)\n // And nothing lands on the RECORD either: `isTerminalRunStatus` must\n // not answer for the loser's view of a run the winner completed.\n const record = await runs.get(runId)\n expect(record?.status).toBe('completed')\n expect(record?.error).toBeUndefined()\n expect(record?.driverEpoch).toBe(2)\n // `close()` is deliberately OUTSIDE both fences: it runs on the very\n // teardown caused by losing the claim, and a fenced close would wedge\n // the record at `'running'` with every live tailer parked forever. Two\n // drivers, two closes.\n expect(log.closes()).toBe(2)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 4. Journal cleanup on a terminal run, and the attach that follows it.\n // ---------------------------------------------------------------------\n it(\n \"deletes a terminal run's journal, and a later attach reports terminal-run instead of hanging\",\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('cleanup')\n const threadId = `${runId}-t`\n const deltas = ['1', '2']\n const paths = journalPaths(runId, CONFORMANCE_JOURNAL_DIR)\n try {\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n const fresh = durabilityFor(runs, log.log, false)\n await startJournaledAgent(\n handle,\n agentCommand(deltas, deltas.length),\n { journal: journalOptions(fresh, runId) },\n )\n const seen: Array<StreamChunk> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. Not the assertion — `backstopped` below proves it was not\n // what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const chunk of translate(\n runId,\n readJournalNdjson(handle, {\n signal: backstop,\n journal: journalOptions(fresh, runId),\n }),\n )) {\n seen.push(chunk)\n }\n // The causal witness, first: this loop has no `break`, so the ONLY\n // honest reasons for it to end are the sentinel or the backstop. A\n // clock-ended read must say so rather than report a short transcript.\n expect({ backstopped: backstop.aborted }).toEqual({\n backstopped: false,\n })\n // Reaching the sentinel is what makes the run terminal, and it is the\n // precondition for the deletion below.\n expect(transcript(seen)).toEqual(\n transcript(expectedTranscript(runId, deltas)),\n )\n\n // Real files, really gone — asserted through the shell, never\n // `handle.fs.exists`: on local-process the two resolve `/tmp`\n // differently, so an `fs` probe would answer about a path the journal\n // was never written to (`journal.ts` rule 3).\n // Named rather than two bare `.not.toBe(0)` assertions on an exit\n // code, so a regression reports WHICH file survived instead of\n // `expected +0 not to be +0`.\n const journalProbe = await handle.process.exec(\n journalExistsCommand(paths),\n )\n const stderrProbe = await handle.process.exec(\n journalExistsCommand({ ...paths, journal: paths.stderr }),\n )\n expect({\n journalDeleted: journalProbe.exitCode !== 0,\n stderrSidecarDeleted: stderrProbe.exitCode !== 0,\n }).toEqual({ journalDeleted: true, stderrSidecarDeleted: true })\n\n // The run is over, so the record says so — and the attach that follows\n // must answer from the record rather than tail the journal, which\n // `journalFollowCommand` would obligingly re-create as an empty file\n // that no sentinel can ever arrive in.\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths,\n runId,\n runs,\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('terminal-run')\n // The journal really is gone (asserted above), so the preflight takes\n // the record arm: one `test -f`, then the store, no wait. This is the\n // bound that failed as `expected 9652 to be less than 4000` under\n // parallel Docker load, where the 9.6s was one `docker exec`\n // round-trip and not the preflight — see `countingExec`.\n expect(probes.execs()).toBe(1)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"takeover-conformance.js","names":[],"sources":["../../../src/testkit/takeover-conformance.ts"],"sourcesContent":["/**\n * Provider conformance for TAKEOVER: a second driver picking up a run whose\n * first driver died, against a REAL sandbox.\n *\n * WHY THIS EXISTS SEPARATELY FROM THE UNIT TESTS. Every takeover unit test in\n * this package drives fakes — a scripted `spawn`, a `test -f` that answers from\n * a boolean, a log that is an array. Fakes model what we believe the shell and\n * the filesystem do, and on this feature that belief has been wrong three times:\n * `base64` delivers zero bytes on a live pipe, `tail -f` on a missing file exits\n * instead of waiting, and a provider's `kill` does not always reap a grandchild.\n * Each one passed every fake. So the four properties a takeover actually rests\n * on are asserted here through a provider's real `spawn`/`exec` against a real\n * journal file:\n *\n * 1. **The delivered sequence is the run's sequence, with no duplicated\n * prefix.** Asserted as a TRANSCRIPT, never as \"chunks arrived\": a takeover\n * that replays the whole journal and re-appends everything satisfies the weak\n * assertion while showing the user the entire run twice. That is the exact\n * failure `alignToStoredLog` exists to prevent, and the only assertion that\n * can see it is one that compares the stored log to the expected sequence\n * element for element.\n * 2. **The attach preflight decides, or fails, but never hangs.** It probes with\n * the provider's real `exec` (`test -f`), which is the layer where a fake's\n * assumptions break, and its three verdicts (`unknown-run`, `terminal-run`,\n * `journal-timeout`) plus the legitimate late-journal race are all timing\n * against a real filesystem.\n * 3. **The epoch fence and its latch hold under real concurrency.** Two drivers\n * reading one real journal at once: the second wins, the first appends\n * NOTHING — not even `pipeToRunLog`'s recovery `RUN_ERROR` — and cannot\n * terminalize the record out from under the live successor.\n * 4. **A terminal run's journal is deleted, and a later attach says so.** The\n * deletion is a real `rm` of real files, and the follow-up attach must report\n * `terminal-run` rather than tailing the file that `journalFollowCommand`\n * would helpfully re-create.\n *\n * WHAT IS REAL HERE. The provider (its `spawn`, `exec`, and shell), the journal\n * (a real NDJSON file the agent's stdout is redirected into), the agent (a real\n * process writing real lines with a real pause in the middle), the reader\n * (`readJournalNdjson`, including the follow/poll strategy split and the attach\n * preflight), the alignment (`alignedIfAttaching` over the real\n * `resolveSandboxDurability` output), the claim and BOTH fences\n * (`sandboxRunDriver`), and the run record (`InMemoryRunStore`). The event log is\n * in-process, exactly as the recommended `memoryStream` backend is.\n *\n * A provider that cannot satisfy the contract MUST declare `unsupported.reason`.\n * As in the journal suite there is deliberately no silent-skip path: a\n * conformance case that quietly returns prints as a pass, which is how an\n * unimplemented capability ships green.\n *\n * FOUND BY THIS SUITE, FIXED IN THE PROVIDER, STILL NOT ASSERTED HERE. On\n * local-process under Windows (git-bash `sh`), the follow read's `tail`\n * grandchild used to SURVIVE `proc.kill()`: `LocalProcessHandle.killTree` ran\n * `taskkill /PID <sh> /T /F` and returned as soon as `spawnSync` reported no\n * `error`. Two things were wrong. It never checked taskkill's exit status — and\n * that alone would not have caught it, because MSYS's fork emulation leaves the\n * `tail.exe` pointing at an intermediate shell that has already exited, so\n * `taskkill /T` (live parent links only) cannot reach it and still exits `0`.\n * Measured by counting `tail.exe` before and after a run: this suite leaked 4 per\n * run and the shipped journal suite 2, accumulating for the life of the machine.\n * It was a provider defect, not a takeover defect — every case here still\n * delivered the right transcript, because `untilAborted` (see\n * `journal-reader.ts`) stops honoring the pipe once the signal fires rather than\n * waiting for the kill, which is exactly why it never failed a test.\n * `killTree` now resolves the tree through MSYS's own process table and verifies\n * the survivors are gone (0 per run), covered in\n * `ai-sandbox-local-process/tests/kill-tree.test.ts`.\n * Deliberately still NOT asserted in this suite: a per-provider process census is\n * not portable (Docker's `tail` dies with its container), and a conformance case\n * that counted host processes would fail for reasons unrelated to takeover.\n *\n * EVERY WAIT IN THIS FILE IS BOUNDED. A hang stalls CI instead of failing it, so\n * each journal read carries a timeout signal, each poll loop carries a deadline\n * and a message naming what never happened, and each case carries an explicit\n * per-test timeout.\n *\n * Vitest is an OPTIONAL peer dependency: this module is imported only from test\n * files, which already run under Vitest.\n */\nimport { describe, expect, it } from 'vitest'\nimport { EventType, InMemoryRunStore } from '@tanstack/ai'\nimport { InMemoryLockStore } from '@tanstack/ai/locks'\nimport {\n journalCleanupCommand,\n journalExistsCommand,\n journalPaths,\n journaledCommand,\n} from '../journal'\nimport { readJournalNdjson, startJournaledAgent } from '../runner'\nimport {\n JournalAttachUnavailableError,\n awaitAttachableJournal,\n} from '../attach-preflight'\nimport {\n alignedIfAttaching,\n journalOptionsFor,\n resolveSandboxDurability,\n} from '../durability'\nimport { sandboxRunDriver } from '../driver'\nimport { fenceDurability, withRunClaim } from '../claim'\nimport { chunkFingerprint, createRunScopedIdGen } from '../chunk-identity'\nimport type { SandboxRunDurability } from '../durability'\nimport type { JournalOptions } from '../runner'\nimport type { SandboxHandle } from '../contracts'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type { RunStore, StreamChunk, StreamDurability } from '@tanstack/ai'\n\nexport interface TakeoverConformanceConfig {\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 takeover, 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\n/**\n * Journal directory for this suite, deliberately NOT\n * {@link DEFAULT_JOURNAL_DIR}: on local-process the sandbox shell shares the\n * host's real `/tmp`, so conformance runs must not write where an application's\n * runs live.\n */\nconst CONFORMANCE_JOURNAL_DIR = '/tmp/tanstack-takeover-conformance'\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 successor's first append. Short because the\n * predecessor in these cases has provably stopped (the suite sequenced it) —\n * the 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/**\n * Unique per case, and it must be: `journalPaths` derives the file name 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`). The counter covers two cases created inside the\n * same millisecond; the random suffix covers two suites sharing one `/tmp`.\n */\nlet caseCounter = 0\nfunction uniqueRunId(label: string): string {\n caseCounter += 1\n const suffix = Math.random().toString(36).slice(2, 8)\n return `tko-${label}-${Date.now()}-${caseCounter}-${suffix}`\n}\n\n/**\n * An in-process event log with real accumulated state, plus the two facts the\n * assertions need: what is stored (in append order) and how many times `close()`\n * ran.\n *\n * `snapshot()` returns fresh objects, per the `StreamDurability` contract, so a\n * caller cannot reach the stored log through the result.\n */\ninterface ConformanceLog {\n log: StreamDurability\n /** Stored chunks, in append order. The transcript under test. */\n stored: () => Array<StreamChunk>\n /** `close()` calls — proof that `close` is NOT fenced. */\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 = `conf:${entries.length}`\n entries.push({ offset, chunk })\n return offset\n }),\n ),\n // Nothing in this suite tails the log — every assertion reads the stored\n // transcript with `snapshot()`, which is also what `alignToStoredLog`\n // uses, 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 lock that grants every request immediately and never reports a loss.\n *\n * `InMemoryLockStore` SERIALIZES claims within one process, so a second attach\n * waits for the first to finish and the two drivers are never concurrent — which\n * means the epoch fence can never be observed there. `claim.ts` says exactly\n * that: in one process only layer 2, the `driverEpoch` fence, is provable. This\n * models a lease-less lock so the two drives overlap and layer 2 does the work.\n */\nconst permissiveLocks: LockStore = {\n withLock: (_key, fn) => fn(new AbortController().signal),\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 is REBUILT from them rather than asserted\n * into shape: a cast would let a provider that mangles the bytes (a folded\n * stderr diagnostic, a truncated line) reach `chunkFingerprint` as a\n * structurally invalid chunk and fail somewhere 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 `takeover 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 `takeover conformance: run ${runId} journal line has a non-string delta: ${JSON.stringify(value)}`,\n )\n }\n return contentChunk(messageId, delta)\n}\n\n/**\n * The translator. Deterministic by construction, which is what makes alignment\n * possible at all: the message id comes from {@link createRunScopedIdGen}, so\n * re-translating the same journal from byte 0 reproduces byte-identical chunks\n * (modulo `timestamp`, the one field `chunkFingerprint` excludes).\n */\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/**\n * A comparable transcript: each chunk reduced to its {@link chunkFingerprint}.\n *\n * The fingerprint, not the chunk object, and for the same reason alignment uses\n * it — `timestamp` is wall-clock and unreproducible, so a raw `toEqual` on\n * chunks would fail on the one field the feature deliberately ignores. Every\n * other field participates, so a duplicated prefix, a dropped chunk, or a\n * reordered one still fails.\n */\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 * A real agent: a shell command that prints one NDJSON line per delta, with an\n * optional real pause partway through, then exits.\n *\n * `printf '%s\\n' a b c` reuses the format for every operand on GNU coreutils and\n * on busybox alike, so this needs no loop. The JSON contains only double quotes,\n * so it is safe inside the POSIX single-quoted words this builds.\n */\nfunction agentCommand(deltas: Array<string>, pauseAfter: number): string {\n const line = (delta: string): string => `'{\"delta\":\"${delta}\"}'`\n const head = deltas.slice(0, pauseAfter)\n const tail = deltas.slice(pauseAfter)\n const parts = [`printf '%s\\\\n' ${head.map(line).join(' ')}`]\n if (tail.length > 0) {\n // A real sleep, so the takeover below happens while the agent is genuinely\n // still writing rather than against a finished file.\n parts.push('sleep 2', `printf '%s\\\\n' ${tail.map(line).join(' ')}`)\n }\n return parts.join('; ')\n}\n\n/** Resolve durability through the production resolver, fresh or attaching. */\nfunction durabilityFor(\n runs: RunStore,\n log: StreamDurability,\n attach: boolean,\n): SandboxRunDurability {\n const resolved = resolveSandboxDurability({\n runs,\n durability: {\n adapter: log,\n journal: CONFORMANCE_JOURNAL_DIR,\n attach,\n pollIntervalMs: POLL_INTERVAL_MS,\n },\n })\n if (resolved === undefined) {\n throw new Error(\n 'takeover conformance: resolveSandboxDurability returned undefined for a fully wired run',\n )\n }\n return resolved\n}\n\n/**\n * The reader's journal options for a resolved durability.\n *\n * `journalOptionsFor` answers `undefined` for a NON-durable run, which cannot\n * happen here — every run in this suite is fully wired. Narrowing it with a\n * thrown error rather than a non-null assertion keeps the impossible case loud\n * if the resolver's contract ever changes.\n */\nfunction journalOptions(\n durability: SandboxRunDurability,\n runId: string,\n): JournalOptions {\n const options = journalOptionsFor(durability, runId)\n if (options === undefined) {\n throw new Error(\n `takeover conformance: journalOptionsFor answered undefined for durable run ${runId}`,\n )\n }\n return options\n}\n\n/** A `'running'` record for `runId`, ready to be claimed. */\nasync function runningRun(\n runId: string,\n threadId: string,\n): Promise<InMemoryRunStore> {\n const runs = new InMemoryRunStore()\n await runs.createOrResume({ runId, threadId, startedAt: Date.now() })\n return runs\n}\n\n/**\n * Wrap a handle so the `process.exec` calls ONE operation makes can be counted.\n *\n * This is how the attach preflight's fail-fast cases are anchored, and the reason\n * they are not anchored on elapsed time. `awaitAttachableJournal` runs exactly one\n * `test -f` before it consults the run store, so a decision made from the record\n * costs one `exec` and a decision made by waiting costs one per\n * `probeIntervalMs`. The count separates those two behaviors exactly; elapsed time\n * does not, because a single `exec` is a provider round-trip whose latency the\n * suite does not control — a `docker exec` on a loaded daemon has been measured at\n * 9.6s, which fails a `< 4_000ms` bound while the preflight under test did\n * precisely the right thing. A timing bound that goes red on a busy machine\n * teaches people to ignore the suite.\n *\n * The spread copies the handle's own methods, so everything except `exec` is the\n * provider's; the wrapper delegates rather than reimplementing.\n */\nfunction countingExec(handle: SandboxHandle): {\n handle: SandboxHandle\n execs: () => number\n} {\n let execs = 0\n return {\n handle: {\n ...handle,\n process: {\n ...handle.process,\n exec: (command, options) => {\n execs += 1\n return handle.process.exec(command, options)\n },\n },\n },\n execs: () => execs,\n }\n}\n\n/** Poll `check` until it answers true, or fail with a message naming what never happened. */\nasync function waitUntil(\n check: () => Promise<boolean>,\n options: { timeoutMs: number; message: string },\n): Promise<void> {\n const deadline = Date.now() + options.timeoutMs\n for (;;) {\n if (await check()) return\n if (Date.now() > deadline) {\n throw new Error(\n `takeover conformance: ${options.message} within ${options.timeoutMs}ms`,\n )\n }\n await sleep(25)\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\ninterface Gate {\n promise: Promise<void>\n open: () => void\n}\n\n/** A one-shot gate, for sequencing two concurrent drivers deterministically. */\nfunction gate(): Gate {\n let open = (): void => {}\n const promise = new Promise<void>((resolve) => {\n open = () => resolve()\n })\n return { promise, open }\n}\n\n/**\n * Build the driver a host would build for one run.\n *\n * `drive` is the real journal path: read the run's journal from byte 0 (through\n * the attach preflight when attaching), translate, and align against the stored\n * log — `alignedIfAttaching`, so alignment runs on an attach and only on an\n * attach.\n *\n * Returns the driver alongside `backstopped()`, the causal witness for\n * {@link READ_BACKSTOP_MS}: every case that drives this must assert it is\n * `false` before its transcript assertions, so a read the CLOCK ended fails\n * naming the backstop instead of as a truncated-transcript diff.\n */\nfunction driverFor(input: {\n handle: SandboxHandle\n runs: RunStore\n locks: LockStore\n log: StreamDurability\n runId: string\n attach: boolean\n /** Awaited before the FIRST translated chunk is yielded, never after. */\n beforeFirstChunk?: () => Promise<void>\n}): {\n driver: ReturnType<typeof sandboxRunDriver>\n /** True if any read this driver started was ended by the backstop clock. */\n backstopped: () => boolean\n} {\n const durability = durabilityFor(input.runs, input.log, input.attach)\n // One entry per `drive` invocation, so a re-drive cannot hide a backstopped\n // read behind a healthy one.\n const backstops: Array<AbortSignal> = []\n const driver = sandboxRunDriver({\n request: new Request(\n `http://takeover.local/attach?runId=${encodeURIComponent(input.runId)}&offset=-1`,\n ),\n runs: input.runs,\n locks: input.locks,\n durability: () => input.log,\n fenceQuietMs: FENCE_QUIET_MS,\n drive: ({ runId, signal }) => {\n // The read is bounded independently of `signal`: an `InMemoryLockStore`\n // hands out a signal it never aborts, so a journal that stops growing\n // would otherwise park this read forever and turn a broken takeover into a\n // hung CI job instead of a failing assertion.\n //\n // Not the assertion — see {@link READ_BACKSTOP_MS}. `backstopped()` below\n // is what proves the clock was not what ended the read.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n backstops.push(backstop)\n const bounded = AbortSignal.any([signal, backstop])\n const lines = readJournalNdjson(input.handle, {\n signal: bounded,\n journal: journalOptions(durability, runId),\n })\n const gated = input.beforeFirstChunk\n const source =\n gated === undefined\n ? lines\n : (async function* afterGate() {\n let first = true\n for await (const value of lines) {\n if (first) {\n first = false\n await gated()\n }\n yield value\n }\n })()\n return alignedIfAttaching(translate(runId, source), durability)\n },\n })\n return { driver, backstopped: () => backstops.some((s) => s.aborted) }\n}\n\n/** Exactly what core's `startRunDriver` does: claim, then pipe the drive. */\nfunction takeOver(\n driver: ReturnType<typeof sandboxRunDriver>,\n input: { runs: RunStore; runId: string; threadId: string },\n): Promise<unknown> {\n const { runs, runId, threadId } = input\n return driver.claim({ runs, locks: driver.locks, runId }, (claim) =>\n driver.pipe(driver.drive({ runId, threadId, signal: claim.signal }), {\n runId,\n threadId,\n signal: claim.signal,\n }),\n )\n}\n\n/** Best-effort removal of a case's journal files, through the shell (rule 3). */\nasync function cleanup(handle: SandboxHandle, runId: string): Promise<void> {\n try {\n await handle.process.exec(\n journalCleanupCommand(journalPaths(runId, CONFORMANCE_JOURNAL_DIR)),\n )\n } catch {\n // The sandbox may already be gone. Nothing under test depends on the files\n // being absent afterwards — the cases that DO assert deletion assert it\n // directly.\n }\n}\n\n/**\n * Assert `createHandle` satisfies the takeover conformance contract. Each `it`\n * gets a fresh sandbox via `createHandle`/`dispose`, and a unique `runId`, so no\n * case can observe another's journal.\n */\nexport function runTakeoverConformance(\n config: TakeoverConformanceConfig,\n): void {\n describe(`takeover 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. A real takeover, end to end.\n // ---------------------------------------------------------------------\n it(\n 'delivers the run sequence exactly once when a second driver takes over mid-stream',\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('e2e')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3', '4', '5', '6']\n const prefixLength = 3\n const expected = expectedTranscript(runId, deltas)\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n try {\n const fresh = durabilityFor(runs, log.log, false)\n\n // THE HOST THAT DIES. A real claim, a real fence, a real journal read\n // of a real agent — and then it stops after `prefixLength` chunks\n // without closing the log and without terminalizing the record, which\n // is what a host vanishing looks like from the outside.\n const deliveredByFirst: Array<StreamChunk> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. Not the assertion — `backstopped` below proves it was not\n // what ended the loop.\n const firstBackstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n await withRunClaim(\n { runs, locks: new InMemoryLockStore(), runId },\n async (claim) => {\n const fenced = fenceDurability(log.log, claim, { runs })\n await startJournaledAgent(\n handle,\n agentCommand(deltas, prefixLength),\n { journal: journalOptions(fresh, runId) },\n )\n const lines = readJournalNdjson(handle, {\n signal: firstBackstop,\n journal: journalOptions(fresh, runId),\n })\n for await (const chunk of translate(runId, lines)) {\n await fenced.append([chunk])\n deliveredByFirst.push(chunk)\n // Breaking ends the reader's `tail` before this host walks away;\n // the AGENT keeps running, which is the whole premise.\n if (deliveredByFirst.length === prefixLength) break\n }\n },\n )\n // The causal witness for the dying host's read: it must stop because\n // the consumer broke at `prefixLength`, not because the clock ran out.\n // A backstopped read here delivers a short prefix and the takeover the\n // case exists to exercise would start from the wrong offset.\n expect({ backstopped: firstBackstop.aborted }).toEqual({\n backstopped: false,\n })\n expect(transcript(deliveredByFirst)).toEqual(\n transcript(expected.slice(0, prefixLength)),\n )\n\n // THE SUCCESSOR. Same runId, same journal, a fresh claim.\n const successor = driverFor({\n handle,\n runs,\n locks: new InMemoryLockStore(),\n log: log.log,\n runId,\n attach: true,\n })\n const record = await takeOver(successor.driver, {\n runs,\n runId,\n threadId,\n })\n\n // THE CAUSAL WITNESS, first — see {@link READ_BACKSTOP_MS}. The\n // transcript assertions below can only speak about chunks that\n // arrived; this one says the successor's read ended because the\n // journal ended, not because the clock did. Without it a backstopped\n // read reports as a confusing short-transcript diff.\n expect({ backstopped: successor.backstopped() }).toEqual({\n backstopped: false,\n })\n\n // THE TRANSCRIPT, element for element. This is the assertion that can\n // see the failure the feature exists to prevent: a takeover that\n // replays the journal from byte 0 without aligning re-appends the\n // prefix, so `stored` would be 9 entries beginning `1,2,3,1,2,3,…` —\n // and the user would watch the first half of the run twice. \"Chunks\n // arrived\" passes against that; this does not.\n expect(transcript(log.stored())).toEqual(transcript(expected))\n // Stated separately so a failure reads as what it is rather than as a\n // 9-vs-6 array diff.\n expect(log.stored()).toHaveLength(deltas.length)\n expect(transcript(log.stored().slice(prefixLength))).toEqual(\n transcript(expected.slice(prefixLength)),\n )\n\n const finalRecord = await runs.get(runId)\n expect(finalRecord?.status).toBe('completed')\n // The successor's claim, not the predecessor's: a hardcoded epoch\n // would read 1 here and every takeover would be fenced out.\n expect(finalRecord?.driverEpoch).toBe(2)\n expect(record).not.toBeUndefined()\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 2. The attach preflight, against a real filesystem.\n // ---------------------------------------------------------------------\n it(\n 'fails an attach to an unknown runId with unknown-run, without waiting it out',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('unknown')\n try {\n expect.hasAssertions()\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs: new InMemoryRunStore(),\n // Generous on purpose: were the store verdict skipped, this would\n // poll for the full 8s and the probe count below would catch it.\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('unknown-run')\n // Decided from the RECORD, not by waiting it out: one `test -f`, then\n // the store. A preflight that polled to the deadline would run ~80\n // probes here. See `countingExec` for why this is not a stopwatch.\n expect(probes.execs()).toBe(1)\n } finally {\n await dispose()\n }\n },\n )\n\n it(\n 'fails an attach to a terminal run whose journal is gone with terminal-run',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('terminal')\n const threadId = `${runId}-t`\n try {\n expect.hasAssertions()\n const runs = await runningRun(runId, threadId)\n await runs.update(runId, { status: 'completed', finishedAt: 2 })\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs,\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('terminal-run')\n // One `test -f`, then the record. Not a stopwatch — `countingExec`.\n expect(probes.execs()).toBe(1)\n } finally {\n await dispose()\n }\n },\n )\n\n it(\n 'waits for a live run whose journal appears late — the legitimate race',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('race')\n const threadId = `${runId}-t`\n const paths = journalPaths(runId, CONFORMANCE_JOURNAL_DIR)\n try {\n const runs = await runningRun(runId, threadId)\n // A real driver writing its real first line ~400ms after the attach\n // starts probing. This is the NORMAL case — `journalFollowCommand`'s\n // `: >> file` exists for it — so failing fast here would reintroduce\n // the defect that fix cured.\n const writer = sleep(400).then(() =>\n handle.process.exec(\n journaledCommand(`printf '{\"delta\":\"1\"}\\\\n'`, paths),\n ),\n )\n try {\n await awaitAttachableJournal(handle, {\n paths,\n runId,\n runs,\n // Comfortably longer than the write above; the per-test timeout is\n // what turns a never-resolving wait into a failure.\n waitMs: 20_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n })\n } finally {\n await writer\n }\n // Resolving at all is the assertion; this pins the premise that it\n // resolved because the file really is there now.\n expect(\n (await handle.process.exec(journalExistsCommand(paths))).exitCode,\n ).toBe(0)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n it(\n 'bounds the wait for a live run whose journal never appears, with journal-timeout',\n { timeout: 120_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('timeout')\n const threadId = `${runId}-t`\n try {\n expect.hasAssertions()\n const runs = await runningRun(runId, threadId)\n const error = await awaitAttachableJournal(handle, {\n paths: journalPaths(runId, CONFORMANCE_JOURNAL_DIR),\n runId,\n runs,\n waitMs: 600,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('journal-timeout')\n // The three assertions above ARE the proof the bound was applied: an\n // unbounded wait never produces a `JournalAttachUnavailableError` at\n // all, and `'600ms'` in the message is the configured bound reported\n // back. No stopwatch assertion here on purpose — the case's own\n // `{ timeout: 120_000 }` already converts an unbounded wait into a\n // failure, and a wall-clock ceiling would red a CORRECT implementation\n // on a machine where one `docker exec` was measured at 95s.\n expect(error.message).toContain('600ms')\n } finally {\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 3. The epoch fence and the shared latch, under real concurrency.\n // ---------------------------------------------------------------------\n it(\n 'lets the second of two concurrent drivers win, and the loser appends nothing at all',\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('fence')\n const threadId = `${runId}-t`\n const deltas = ['1', '2', '3']\n const expected = expectedTranscript(runId, deltas)\n try {\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n // One agent, one journal, two drivers reading it concurrently.\n await startJournaledAgent(\n handle,\n agentCommand(deltas, deltas.length),\n {\n journal: journalOptions(\n durabilityFor(runs, log.log, false),\n runId,\n ),\n },\n )\n\n // The LOSER: the original host, so it does not align (there is nothing\n // stored when it starts). Gated before its first chunk reaches the\n // log, which is where the fence has to catch it — after the successor\n // has claimed and finished. The gate sits INSIDE the source stream, so\n // the loser's alignment snapshot (were it attaching) and its first\n // append both happen after the release, exactly as a host paused by a\n // GC or a VM suspend would.\n const released = gate()\n const losingDriver = driverFor({\n handle,\n runs,\n locks: permissiveLocks,\n log: log.log,\n runId,\n attach: false,\n beforeFirstChunk: () => released.promise,\n })\n const loser = takeOver(losingDriver.driver, {\n runs,\n runId,\n threadId,\n })\n await waitUntil(\n async () => ((await runs.get(runId))?.driverEpoch ?? 0) >= 1,\n {\n timeoutMs: 30_000,\n message: `the first driver never claimed run ${runId}`,\n },\n )\n\n // The WINNER: claims at a higher epoch and drives the run to the end.\n const winner = driverFor({\n handle,\n runs,\n locks: permissiveLocks,\n log: log.log,\n runId,\n attach: true,\n })\n await takeOver(winner.driver, { runs, runId, threadId })\n // The causal witness, before the transcript — see\n // {@link READ_BACKSTOP_MS}. The winner drives the run to its sentinel,\n // so a clock-ended read here must say so rather than surface as a\n // missing chunk.\n expect({ backstopped: winner.backstopped() }).toEqual({\n backstopped: false,\n })\n expect(transcript(log.stored())).toEqual(transcript(expected))\n\n // Now let the superseded host try to write.\n released.open()\n await loser\n // The causal witness, and here it is load-bearing rather than merely\n // diagnostic: the loser's gate is awaited from INSIDE its read, so a\n // backstopped read would abandon the stream during the wait, the loser\n // would never attempt an append at all, and every \"nothing lands\"\n // assertion below would pass vacuously without the fence ever running.\n expect({ backstopped: losingDriver.backstopped() }).toEqual({\n backstopped: false,\n })\n\n // NOTHING lands — not the run's chunks a second time, and not\n // `pipeToRunLog`'s recovery `RUN_ERROR` either. That log belongs to the\n // WINNER: a terminal `RUN_ERROR` from a dead host would fail the stream\n // for every client attached to the live, healthy run.\n expect(transcript(log.stored())).toEqual(transcript(expected))\n expect(\n log.stored().some((chunk) => chunk.type === EventType.RUN_ERROR),\n ).toBe(false)\n // And nothing lands on the RECORD either: `isTerminalRunStatus` must\n // not answer for the loser's view of a run the winner completed.\n const record = await runs.get(runId)\n expect(record?.status).toBe('completed')\n expect(record?.error).toBeUndefined()\n expect(record?.driverEpoch).toBe(2)\n // `close()` is deliberately OUTSIDE both fences: it runs on the very\n // teardown caused by losing the claim, and a fenced close would wedge\n // the record at `'running'` with every live tailer parked forever. Two\n // drivers, two closes.\n expect(log.closes()).toBe(2)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n\n // ---------------------------------------------------------------------\n // 4. Journal cleanup on a terminal run, and the attach that follows it.\n // ---------------------------------------------------------------------\n it(\n \"deletes a terminal run's journal, and a later attach reports terminal-run instead of hanging\",\n { timeout: 180_000 },\n async () => {\n const { handle, dispose } = await config.createHandle()\n const runId = uniqueRunId('cleanup')\n const threadId = `${runId}-t`\n const deltas = ['1', '2']\n const paths = journalPaths(runId, CONFORMANCE_JOURNAL_DIR)\n try {\n const runs = await runningRun(runId, threadId)\n const log = conformanceLog()\n const fresh = durabilityFor(runs, log.log, false)\n await startJournaledAgent(\n handle,\n agentCommand(deltas, deltas.length),\n { journal: journalOptions(fresh, runId) },\n )\n const seen: Array<StreamChunk> = []\n // A backstop, so a reader that delivers nothing fails instead of\n // parking CI. Not the assertion — `backstopped` below proves it was not\n // what ended the loop.\n const backstop = AbortSignal.timeout(READ_BACKSTOP_MS)\n for await (const chunk of translate(\n runId,\n readJournalNdjson(handle, {\n signal: backstop,\n journal: journalOptions(fresh, runId),\n }),\n )) {\n seen.push(chunk)\n }\n // The causal witness, first: this loop has no `break`, so the ONLY\n // honest reasons for it to end are the sentinel or the backstop. A\n // clock-ended read must say so rather than report a short transcript.\n expect({ backstopped: backstop.aborted }).toEqual({\n backstopped: false,\n })\n // Reaching the sentinel is what makes the run terminal, and it is the\n // precondition for the deletion below.\n expect(transcript(seen)).toEqual(\n transcript(expectedTranscript(runId, deltas)),\n )\n\n // Real files, really gone — asserted through the shell, never\n // `handle.fs.exists`: on local-process the two resolve `/tmp`\n // differently, so an `fs` probe would answer about a path the journal\n // was never written to (`journal.ts` rule 3).\n // Named rather than two bare `.not.toBe(0)` assertions on an exit\n // code, so a regression reports WHICH file survived instead of\n // `expected +0 not to be +0`.\n const journalProbe = await handle.process.exec(\n journalExistsCommand(paths),\n )\n const stderrProbe = await handle.process.exec(\n journalExistsCommand({ ...paths, journal: paths.stderr }),\n )\n expect({\n journalDeleted: journalProbe.exitCode !== 0,\n stderrSidecarDeleted: stderrProbe.exitCode !== 0,\n }).toEqual({ journalDeleted: true, stderrSidecarDeleted: true })\n\n // The run is over, so the record says so — and the attach that follows\n // must answer from the record rather than tail the journal, which\n // `journalFollowCommand` would obligingly re-create as an empty file\n // that no sentinel can ever arrive in.\n await runs.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n })\n const probes = countingExec(handle)\n const error = await awaitAttachableJournal(probes.handle, {\n paths,\n runId,\n runs,\n waitMs: 8_000,\n probeIntervalMs: POLL_INTERVAL_MS,\n }).then(\n () => null,\n (reason: unknown) => reason,\n )\n expect(error).toBeInstanceOf(JournalAttachUnavailableError)\n if (!(error instanceof JournalAttachUnavailableError)) return\n expect(error.reason).toBe('terminal-run')\n // The journal really is gone (asserted above), so the preflight takes\n // the record arm: one `test -f`, then the store, no wait. This is the\n // bound that failed as `expected 9652 to be less than 4000` under\n // parallel Docker load, where the 9.6s was one `docker exec`\n // round-trip and not the preflight — see `countingExec`.\n expect(probes.execs()).toBe(1)\n } finally {\n await cleanup(handle, runId)\n await dispose()\n }\n },\n )\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-sandbox",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@ngrok/ngrok": "^1.0.0",
|
|
62
62
|
"vitest": "^4.1.10",
|
|
63
|
-
"@tanstack/ai": "^0.
|
|
63
|
+
"@tanstack/ai": "^0.46.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"@ngrok/ngrok": {
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@ngrok/ngrok": "^1.7.0",
|
|
75
|
-
"@vitest/coverage-v8": "4.
|
|
75
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
76
76
|
"vitest": "^4.1.10",
|
|
77
|
-
"@tanstack/ai": "0.
|
|
77
|
+
"@tanstack/ai": "0.46.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"build": "vite build",
|