Daemon hardening epic
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
const repoRoot = path.resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
||||
const supervisorPath = fileURLToPath(new URL("./supervisor.ts", import.meta.url));
|
||||
|
||||
async function runSupervisorFixture(options: {
|
||||
workerSource: string;
|
||||
restartOnCrash?: boolean;
|
||||
}): Promise<{
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
log: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}> {
|
||||
const tempDir = await mkdtemp(path.join(tmpdir(), "paseo-supervisor-log-"));
|
||||
const logPath = path.join(tempDir, "daemon.log");
|
||||
const workerPath = path.join(tempDir, "worker.mjs");
|
||||
const runnerPath = path.join(tempDir, "runner.mjs");
|
||||
|
||||
await writeFile(workerPath, options.workerSource);
|
||||
await writeFile(
|
||||
runnerPath,
|
||||
`
|
||||
import { runSupervisor } from ${JSON.stringify(pathToFileURL(supervisorPath).href)};
|
||||
|
||||
runSupervisor({
|
||||
name: "TestSupervisor",
|
||||
startupMessage: "starting fixture",
|
||||
resolveWorkerEntry: () => ${JSON.stringify(workerPath)},
|
||||
workerArgs: [],
|
||||
workerEnv: process.env,
|
||||
workerExecArgv: [],
|
||||
restartOnCrash: ${JSON.stringify(options.restartOnCrash ?? false)},
|
||||
logFile: {
|
||||
path: ${JSON.stringify(logPath)},
|
||||
rotate: { maxSize: "1m", maxFiles: 2 },
|
||||
},
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
const child = spawn(process.execPath, ["--import", "tsx", runnerPath], {
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
const { code, signal } = await new Promise<{
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
}>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error("supervisor fixture timed out"));
|
||||
}, 10000);
|
||||
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
child.on("close", (exitCode, exitSignal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code: exitCode, signal: exitSignal });
|
||||
});
|
||||
});
|
||||
|
||||
const log = await readFile(logPath, "utf8");
|
||||
return { code, signal, log, stdout, stderr };
|
||||
}
|
||||
|
||||
describe("supervisor durable logging", () => {
|
||||
test("writes supervised worker stdout and stderr to daemon.log", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
workerSource: `
|
||||
process.stdout.write('{"level":30,"msg":"worker-json-stdout"}\\n');
|
||||
process.stderr.write('{"level":50,"msg":"worker-json-stderr"}\\n');
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.log).toContain('"worker-json-stdout"');
|
||||
expect(result.log).toContain('"worker-json-stderr"');
|
||||
expect(result.stdout).toContain('"worker-json-stdout"');
|
||||
expect(result.stderr).toContain('"worker-json-stderr"');
|
||||
});
|
||||
|
||||
test("preserves raw non-JSON stdout and stderr lines", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
workerSource: `
|
||||
process.stdout.write('raw stdout line\\n');
|
||||
process.stderr.write('raw stderr line\\n');
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.log).toContain("raw stdout line\n");
|
||||
expect(result.log).toContain("raw stderr line\n");
|
||||
});
|
||||
|
||||
test("logs worker signal exits even when the worker cannot log", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
workerSource: `
|
||||
process.kill(process.pid, "SIGKILL");
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.log).toContain('"msg":"Worker exited"');
|
||||
expect(result.log).toContain('"signal":"SIGKILL"');
|
||||
expect(result.log).toContain("Supervisor exiting");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user