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);});
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);
});