Compare commits

...
Author SHA1 Message Date
dev-guard b0923e638a test: keep a still-starting daemon after its readiness deadline 2026-10-29 11:05:37 +00:00
@@ -4,7 +4,7 @@ import type { ChildProcess } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { startDaemon, STATE_FILE, type LifecycleDeps } from "../src/daemon-lifecycle.js";
import { startDaemon, getDaemonStatus, STATE_FILE, type LifecycleDeps } from "../src/daemon-lifecycle.js";
import { acquireDaemonStartLock } from "../src/daemon-start-lock.js";
afterEach(() => vi.useRealTimers());
@@ -67,6 +67,8 @@ describe("child-bound startup", () => {
? { ok: true, json: async () => ({ ...f.body, pid: 888 }) } : fetch(url);
await expect(startDaemon(opts, f.deps)).rejects.toThrow(/identity mismatch.*127.0.0.2/);
expect(f.files.has(STATE_FILE)).toBe(false);
expect(f.deps.kill).toHaveBeenCalledWith(12345, "SIGTERM");
expect(f.lock.release).toHaveBeenCalledWith(false);
});
it("publishes the verified child, naming the unverified listener, when only a secondary listener stays uncertain", async () => {
vi.useFakeTimers();
@@ -114,7 +116,7 @@ describe("child-bound startup", () => {
expect((outcome as Error).message).toMatch(/identity mismatch.*observed 888/);
expect(f.files.has(STATE_FILE)).toBe(false);
});
it("gives up on a never-answering endpoint within one readiness deadline", async () => {
it("returns pending within one readiness deadline without stopping its still-starting child", async () => {
vi.useFakeTimers();
const f = fixture(); const fetch = f.deps.fetch;
f.deps.fetch = async (url) => (await fetch(url), new Promise(() => {}));
@@ -122,7 +124,11 @@ describe("child-bound startup", () => {
expect((outcome as Error).message).toMatch(/Last probe: .*timed out|Last probe: .*healthz/);
expect(elapsed).toBeLessThanOrEqual(20_000);
expect(f.files.has(STATE_FILE)).toBe(false);
expect(f.deps.kill).toHaveBeenCalledWith(12345, "SIGTERM");
expect(f.deps.kill).not.toHaveBeenCalled();
expect(f.lock.release).toHaveBeenCalledWith(true);
expect((outcome as Error).message).toMatch(/pending.*12345/);
expect((outcome as Error).message).toContain("rig daemon status");
expect((outcome as Error).message).toContain("daemon.log");
});
// Synchronous work crossing the deadline is modelled by a clock jump inside one fetch, before
// any timer runs. Post-spawn fetches: 0 primary read, 1 primary listener check, 2 secondary check.
@@ -190,6 +196,8 @@ describe("child-bound startup", () => {
};
await expect(startDaemon(opts, f.deps)).rejects.toThrow(/exited/);
expect(f.files.has(STATE_FILE)).toBe(false);
expect(f.deps.kill).not.toHaveBeenCalled();
expect(f.lock.release).toHaveBeenCalledWith(false);
});
it("handles an asynchronous native spawn error without a PID", async () => {
const f = fixture();
@@ -280,3 +288,39 @@ describe("local startup reservation", () => {
} finally { fs.rmSync(home, { recursive: true, force: true }); }
});
});
it("keeps a slow child discoverable after the start deadline and excludes a duplicate launch", async () => {
vi.useFakeTimers();
const home = fs.mkdtempSync(path.join(os.tmpdir(), "openrig-pending-start-"));
const f = fixture(), fetch = f.deps.fetch;
let ready = false;
f.deps.acquireStartLock = () => acquireDaemonStartLock(home);
f.deps.fetch = async url => {
const response = await fetch(url);
if (!ready) throw Object.assign(new Error("still initializing"), { code: "ECONNREFUSED" });
return response;
};
vi.stubEnv("OPENRIG_URL", `http://127.0.0.1:${opts.port}`);
vi.stubEnv("RIGGED_URL", "");
try {
const began = Date.now(), result = startDaemon(opts, f.deps).catch((error: Error) => error);
await vi.runAllTimersAsync();
const outcome = await result;
expect(outcome).toBeInstanceOf(Error);
expect((outcome as Error).message).toMatch(/pending.*12345/);
expect(Date.now() - began).toBeLessThanOrEqual(20_000);
expect(f.deps.kill).not.toHaveBeenCalled();
expect(f.child.exitCode).toBeNull();
expect(f.files.has(STATE_FILE)).toBe(false);
expect(fs.readFileSync(path.join(home, "daemon-start.lock"), "utf8")).toContain('"childPid":12345');
ready = true;
await expect(getDaemonStatus(f.deps)).resolves.toMatchObject({ state: "running", healthy: true, port: opts.port });
expect(f.files.has(STATE_FILE)).toBe(false); // A status probe never retroactively publishes start readiness.
await expect(startDaemon(opts, f.deps)).rejects.toThrow(/already reserved/);
expect(f.deps.spawn).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllEnvs();
fs.rmSync(home, { recursive: true, force: true });
}
});