// --------------------------------------------------------------------------- // Kill-test child harness. Invoked as a subprocess by // sqlite-workflow-runner.kill.test.ts. It runs the SAME published workflow over // the SAME journal DB across two phases, driving the body inside the SANDBOX // (the orchestrator is sandboxed now — the body never runs in this process). // // phase=1 start the run. Step "write-once" is a step.tool whose bound tool // appends one line to the marker file (the exactly-once side effect), // then step "hang" (a step.tool) blocks forever. The parent SIGKILLs // this process while "hang" is running, so "hang" is never journaled. // phase=2 resume the run over the same DB. "write-once" is journaled -> // replays WITHOUT re-running its bound tool (no second marker line). // "hang" now completes fast and the run finishes. // // Assertion (in the parent): the marker file has exactly ONE line after both // phases — the completed step's host-side side effect happened exactly once // despite the kill+restart, with a SANDBOXED orchestrator. // --------------------------------------------------------------------------- import { appendFileSync } from "node:fs"; import { Effect } from "effect"; import { makeSqliteWorkflowRunner } from "../backing/sqlite-workflow-runner"; import { makeQuickjsWorkflowDriver } from "../backing/quickjs-workflow-driver"; import { makeInMemoryArtifactStore } from "./index"; import type { WorkflowBindings } from "../seams/workflow-runner"; const [, , phase, dbPath, markerPath] = process.argv; // The workflow SOURCE: runs in the sandbox. The exactly-once side effect and the // hang both cross the bridge as step.tool calls serviced host-side below. const WORKFLOW_SOURCE = `import { defineWorkflow } from "executor:app"; export default defineWorkflow({ async run(step) { await step.tool("write-once", {}); await step.tool("hang", {}); return { done: true }; }, });`; const bindings: WorkflowBindings = { runTool: async (address) => { if (address === "write-once") { // The exactly-once side effect: only when the step actually runs. appendFileSync(markerPath, "ran\n"); return { wrote: true }; } if (address === "hang") { if (phase === "1") { // Signal readiness so the parent knows the side effect landed, then hang // forever inside this NOT-yet-journaled step; the parent kills us here. process.stdout.write("HUNG\n"); await new Promise(() => { /* never resolves */ }); } return { hung: false }; } return {}; }, notify: async () => {}, }; const main = async () => { const artifactStore = makeInMemoryArtifactStore(); const driver = makeQuickjsWorkflowDriver({ artifactStore }); const runner = makeSqliteWorkflowRunner({ path: dbPath, driver }); const entryPath = "workflows/kill-wf.ts"; const store = await Effect.runPromise(artifactStore.forScope("s")); const meta = await Effect.runPromise( store.commit(new Map([[entryPath, WORKFLOW_SOURCE]]), "publish kill-wf"), ); if (phase === "1") { await Effect.runPromise( runner.start( { scope: "s", workflow: "kill-wf", snapshotId: meta.id, entryPath, input: {}, runId: "kill-run", }, bindings, ), ); } else { const view = await Effect.runPromise(runner.resume("kill-run", bindings)); process.stdout.write(`STATUS:${view.status}\n`); await Effect.runPromise(runner.close()); } }; void main();