import { fork, spawn } from "node:child_process";
import { unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import pidusage from "pidusage";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
const childScript = join(tmpdir(), "test_child.mjs");
beforeAll(() => {
  writeFileSync(
    childScript,
    `process.on('message', (msg) => process.send?.({ echo: msg }));
     setInterval(() => {}, 1000);`
  );
});
afterAll(() => {
  try {
    unlinkSync(childScript);
  } catch {
  }
});
async function getChildMemoryUsageMB(pid) {
  if (!pid) return 0;
  try {
    const stats = await pidusage(pid);
    return stats.memory / (1024 * 1024);
  } catch (err) {
    const code = err.code;
    if (code === "ENOENT" || code === "ESRCH") {
      return 0;
    }
    throw err;
  }
}
describe("pidusage on dead process", () => {
  it("raw pidusage throws on dead pid", async () => {
    const child = spawn("sleep", ["10"]);
    const pid = child.pid;
    child.kill("SIGKILL");
    await new Promise((r) => child.on("exit", r));
    await expect(pidusage(pid)).rejects.toThrow();
  });
  it("fixed version returns 0 instead of crashing", async () => {
    const child = spawn("sleep", ["10"]);
    const pid = child.pid;
    child.kill("SIGKILL");
    await new Promise((r) => child.on("exit", r));
    const mem = await getChildMemoryUsageMB(pid);
    expect(mem).toBe(0);
  });
  it("handles concurrent calls on dying process", async () => {
    const child = spawn("sleep", ["10"]);
    const pid = child.pid;
    const exitPromise = new Promise((r) => child.on("exit", r));
    child.kill("SIGKILL");
    await exitPromise;
    const results = await Promise.all([
      getChildMemoryUsageMB(pid),
      getChildMemoryUsageMB(pid),
      getChildMemoryUsageMB(pid)
    ]);
    expect(results.every((r) => r === 0)).toBe(true);
  });
});
describe("IPC send on dead process", () => {
  it("child.connected becomes false when child dies", async () => {
    const child = fork(childScript, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
    const exitPromise = new Promise((r) => child.on("exit", r));
    await new Promise((r) => setTimeout(r, 50));
    expect(child.connected).toBe(true);
    child.kill("SIGKILL");
    await exitPromise;
    expect(child.connected).toBe(false);
  });
  it("checking connected before send prevents crash", async () => {
    const child = fork(childScript, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
    const exitPromise = new Promise((r) => child.on("exit", r));
    child.on("error", (err) => {
      if (err.code !== "EPIPE") throw err;
    });
    let sent = 0;
    let skipped = 0;
    const interval = setInterval(() => {
      if (child.connected) {
        child.send({ ping: Date.now() });
        sent++;
      } else {
        skipped++;
      }
    }, 20);
    await new Promise((r) => setTimeout(r, 60));
    child.kill("SIGKILL");
    await exitPromise;
    await new Promise((r) => setTimeout(r, 80));
    clearInterval(interval);
    expect(sent).toBeGreaterThan(0);
    expect(skipped).toBeGreaterThan(0);
  });
});
describe("init timeout rejection handling", () => {
  it("does not produce unhandled rejection when init times out", async () => {
    var _a;
    const unhandled = [];
    const handler = (reason) => unhandled.push(reason);
    process.on("unhandledRejection", handler);
    const slowScript = join(tmpdir(), "test_slow_init_child.mjs");
    writeFileSync(
      slowScript,
      `process.on('message', () => {
        setTimeout(() => process.send({ case: 'initializeResponse' }), 200);
      });
      setInterval(() => {}, 1000);`
    );
    const { SupervisedProc } = await import("./supervised_proc.js");
    class TestProc extends SupervisedProc {
      get processKind() {
        return "job";
      }
      createProcess() {
        return fork(slowScript, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
      }
      async mainTask() {
      }
    }
    const proc = new TestProc(
      50,
      // initializeTimeout — fires before child responds at 200ms
      1e3,
      // closeTimeout
      0,
      // memoryWarnMB
      0,
      // memoryLimitMB
      5e3,
      // pingInterval
      6e4,
      // pingTimeout
      2500
      // highPingThreshold
    );
    await proc.start();
    await expect(proc.initialize()).rejects.toThrow("runner initialization timed out");
    await new Promise((r) => setTimeout(r, 200));
    process.off("unhandledRejection", handler);
    (_a = proc.proc) == null ? void 0 : _a.kill();
    try {
      unlinkSync(slowScript);
    } catch {
    }
    expect(unhandled).toEqual([]);
  });
  it("join() resolves after init timeout instead of hanging forever", async () => {
    var _a;
    const slowScript = join(tmpdir(), "test_slow_init_child_join.mjs");
    writeFileSync(
      slowScript,
      `process.on('message', () => {
        setTimeout(() => process.send({ case: 'initializeResponse' }), 200);
      });
      setInterval(() => {}, 1000);`
    );
    const { SupervisedProc } = await import("./supervised_proc.js");
    class TestProc extends SupervisedProc {
      get processKind() {
        return "job";
      }
      createProcess() {
        return fork(slowScript, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
      }
      async mainTask() {
      }
    }
    const proc = new TestProc(50, 1e3, 0, 0, 5e3, 6e4, 2500);
    await proc.start();
    await proc.initialize().catch(() => {
    });
    const result = await Promise.race([
      proc.join().then(() => "resolved"),
      new Promise((r) => setTimeout(() => r("timeout"), 2e3))
    ]);
    (_a = proc.proc) == null ? void 0 : _a.kill();
    try {
      unlinkSync(slowScript);
    } catch {
    }
    expect(result).toBe("resolved");
  });
  it("kills the child when initialize times out", async () => {
    var _a;
    const hangScript = join(tmpdir(), "test_hang_init_child.mjs");
    writeFileSync(hangScript, `setInterval(() => {}, 1000);`);
    const { SupervisedProc } = await import("./supervised_proc.js");
    class TestProc extends SupervisedProc {
      createProcess() {
        return fork(hangScript, [], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
      }
      async mainTask() {
      }
    }
    const proc = new TestProc(50, 1e3, 0, 0, 5e3, 6e4, 2500);
    await proc.start();
    const exited = new Promise((r) => proc.proc.on("exit", () => r()));
    await expect(proc.initialize()).rejects.toThrow("runner initialization timed out");
    const result = await Promise.race([
      exited.then(() => "exited"),
      new Promise((r) => setTimeout(() => r("timeout"), 2e3))
    ]);
    (_a = proc.proc) == null ? void 0 : _a.kill();
    try {
      unlinkSync(hangScript);
    } catch {
    }
    expect(result).toBe("exited");
  });
});
describe("timer cleanup", () => {
  it("clearInterval stops the interval", async () => {
    let count = 0;
    const interval = setInterval(() => count++, 30);
    await new Promise((r) => setTimeout(r, 80));
    const countAtClear = count;
    clearInterval(interval);
    await new Promise((r) => setTimeout(r, 80));
    expect(count).toBe(countAtClear);
  });
  it("double clear is safe", () => {
    const interval = setInterval(() => {
    }, 100);
    const timeout = setTimeout(() => {
    }, 1e3);
    clearInterval(interval);
    clearTimeout(timeout);
    expect(() => {
      clearInterval(interval);
      clearTimeout(timeout);
    }).not.toThrow();
  });
});
describe("memory warning bookkeeping", () => {
  const MEMORY_WARN_COOLDOWN = 12e4;
  const MEMORY_WARN_RESET_DELTA_MB = 50;
  const makeProc = async (memoryWarnMB = 500, memoryLimitMB = 0) => {
    const { SupervisedProc } = await import("./supervised_proc.js");
    class FakeProc extends SupervisedProc {
      get processKind() {
        return "job";
      }
      createProcess() {
        throw new Error("not implemented");
      }
      async mainTask() {
      }
    }
    return new FakeProc(1e4, 1e4, memoryWarnMB, memoryLimitMB, 2500, 6e4, 500);
  };
  it("rate-limits the warning", async () => {
    const proc = await makeProc();
    expect(proc.shouldEmitMemoryWarning(520, 1e3)).toBe(true);
    expect(proc.shouldEmitMemoryWarning(521, 1005)).toBe(false);
    expect(proc.shouldEmitMemoryWarning(522, 1010)).toBe(false);
    expect(proc.shouldEmitMemoryWarning(522, 1e3 + MEMORY_WARN_COOLDOWN + 1)).toBe(true);
  });
  it("re-emits on significant growth within the cooldown", async () => {
    const proc = await makeProc();
    expect(proc.shouldEmitMemoryWarning(520, 1e3)).toBe(true);
    const grown = 520 + MEMORY_WARN_RESET_DELTA_MB + 1;
    expect(proc.shouldEmitMemoryWarning(grown, 1005)).toBe(true);
  });
  it("reports baseline and growth in the logging fields", async () => {
    const proc = await makeProc();
    let fields = proc.memoryLoggingFields(520);
    expect(fields.memoryUsageMB).toBe(520);
    expect(fields.memoryWarnMB).toBe(500);
    expect(fields.hasRunningJob).toBe(false);
    expect("uptime" in fields).toBe(true);
    expect("baselineMemoryMB" in fields).toBe(false);
    proc.memoryBaselineMB = 300;
    fields = proc.memoryLoggingFields(520);
    expect(fields.baselineMemoryMB).toBe(300);
    expect(fields.growthMemoryMB).toBe(220);
  });
  it("rounds reported memory to one decimal", async () => {
    const proc = await makeProc();
    expect(proc.memoryLoggingFields(520.567).memoryUsageMB).toBe(520.6);
  });
  it("uptime is 0 before start", async () => {
    const proc = await makeProc();
    expect(proc.uptime).toBe(0);
  });
  it("processKind renders into the message string", async () => {
    const proc = await makeProc();
    expect(`${proc.processKind} process`).toBe("job process");
  });
});
//# sourceMappingURL=supervised_proc.test.js.map