69 lines · 2.9 KB · TypeScript
RawDownload
import { spawn } from "node:child_process";import { mkdtempSync, readFileSync, existsSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { describe, expect, it } from "@effect/vitest"; // ---------------------------------------------------------------------------// The REAL kill test: SIGKILL a child process mid-step, restart over the same// data dir, and prove the completed step did NOT re-execute. A side-effect file// written exactly once is the assertion.//// Phase 1 (child): start the run; step "write-once" appends one line to the// marker file, then step "hang" blocks forever. When the child prints "HUNG"// the side effect has landed and it is stuck in a NOT-journaled step. We// SIGKILL it there.//// Phase 2 (child): resume the run over the same journal DB. "write-once" is// journaled so it replays without re-running; the run completes.//// Assertion: the marker file has exactly ONE line.// --------------------------------------------------------------------------- const CHILD = join(import.meta.dirname, "..", "testing", "kill-child.ts"); const runChild = (  phase: string,  dbPath: string,  markerPath: string,  waitForHung: boolean,): Promise<{ code: number | null; killed: boolean; stdout: string }> =>  new Promise((resolve) => {    const child = spawn("bun", [CHILD, phase, dbPath, markerPath], {      stdio: ["ignore", "pipe", "inherit"],    });    let stdout = "";    let killed = false;    child.stdout.on("data", (chunk: Buffer) => {      stdout += chunk.toString();      if (waitForHung && stdout.includes("HUNG") && !killed) {        killed = true;        child.kill("SIGKILL");      }    });    child.on("exit", (code) => resolve({ code, killed, stdout }));  }); describe("WorkflowRunner kill test (SIGKILL mid-step, restart, no double-execute)", () => {  it("runs a completed step exactly once across a kill+restart", async () => {    const dir = mkdtempSync(join(tmpdir(), "apps-kill-"));    const dbPath = join(dir, "journal.db");    const markerPath = join(dir, "marker.txt");     // Phase 1: start, land the side effect, get killed mid-"hang".    const phase1 = await runChild("1", dbPath, markerPath, true);    expect(phase1.killed).toBe(true);    expect(existsSync(markerPath)).toBe(true);    const afterKill = readFileSync(markerPath, "utf8").trim().split("\n").filter(Boolean);    expect(afterKill.length).toBe(1); // side effect happened once before the kill     // Phase 2: resume over the SAME db; completed step must NOT re-run.    const phase2 = await runChild("2", dbPath, markerPath, false);    expect(phase2.code).toBe(0);    expect(phase2.stdout).toContain("STATUS:completed");     const finalLines = readFileSync(markerPath, "utf8").trim().split("\n").filter(Boolean);    expect(finalLines.length).toBe(1); // STILL exactly one line: no double-execute  }, 60_000);});