mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
841817845e | ||
|
|
4109a13145 | ||
|
|
d73bece8fc | ||
|
|
a703016381 |
No files matched your search
@@ -264,7 +264,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
|
||||
// know whether the kernel itself reached ready. Operators who
|
||||
// need a kernel-ready signal opt in here.
|
||||
if (opts.waitForKernel) {
|
||||
const { waitForKernelReady } = await import("../daemon-lifecycle.js");
|
||||
const { waitForKernelReady, skippedKernelMessage } = await import("../daemon-lifecycle.js");
|
||||
const timeoutMs = opts.waitForKernelMs && /^\d+$/.test(opts.waitForKernelMs)
|
||||
? parseInt(opts.waitForKernelMs, 10)
|
||||
: 60_000;
|
||||
@@ -272,6 +272,10 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
|
||||
const result = await waitForKernelReady(baseUrl, timeoutMs);
|
||||
if (result.ok) {
|
||||
console.log(`Kernel ${result.kernelState}; variant=${result.variant ?? "(none)"}`);
|
||||
} else if (result.kernelState === "skipped") {
|
||||
// Skipped on purpose, so neither a timeout nor an auth problem: say why, and the way back.
|
||||
console.error(skippedKernelMessage(result.detail));
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
// Honest 3-part error per banked discipline.
|
||||
console.error(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getDaemonUrl,
|
||||
startDaemon,
|
||||
waitForKernelReady,
|
||||
skippedKernelMessage,
|
||||
type LifecycleDeps, daemonStatusGuard } from "../daemon-lifecycle.js";
|
||||
import { shouldAutoStartDaemon } from "../daemon-reachability.js";
|
||||
import { freshSeatCommand, launchFreshSeat } from "../fresh-seat-command.js";
|
||||
@@ -286,11 +287,8 @@ status); a restore that returned without a ready kernel exits 1.
|
||||
if (!opts.json) console.log("Continuing with your other rigs.");
|
||||
} else if (!kernelResult.ok) {
|
||||
if (kernelResult.kernelState === "skipped") {
|
||||
// The daemon also skips its boot when a kernel rig already exists (kernel-boot.ts).
|
||||
// That rig need not be running, after a reboot it is not, and this command does not restore it.
|
||||
if (!opts.json) console.log(kernelResult.detail === "kernel rig already managed"
|
||||
? "Kernel auto-boot skipped (kernel rig already managed). If its seats are not running, for example after a reboot, bring it back with: rig up kernel --existing"
|
||||
: "Kernel auto-boot skipped (--no-kernel or test mode), including the operator that helps you start a team. To start it later, run rig, set up or select kernel, then start its operator seat.");
|
||||
// This command does not restore a kernel the daemon skipped; say why, and the way back.
|
||||
if (!opts.json) console.log(skippedKernelMessage(kernelResult.detail));
|
||||
} else {
|
||||
const terminalFailure = ["auth_blocked", "spec_missing", "bootstrap_failed", "degraded"].includes(kernelResult.kernelState ?? "");
|
||||
console.error(`${terminalFailure ? "Kernel failed to start" : "Kernel did not report ready before the deadline"}: state=${kernelResult.kernelState ?? "unknown"}, detail=${kernelResult.detail ?? "none"}`);
|
||||
|
||||
@@ -1318,6 +1318,18 @@ export interface KernelReadyResult {
|
||||
existingRestore?: "in_progress" | "finished";
|
||||
}
|
||||
|
||||
/** What to tell a person when daemon start skipped the kernel on purpose (kernel_state `skipped`).
|
||||
* The daemon also skips when a kernel rig already exists (kernel-boot.ts); that rig need not be running.
|
||||
* A longer detail with the same start is the daemon's own reason, such as a kernel already down before it started. */
|
||||
export function skippedKernelMessage(detail: string | null): string {
|
||||
const d = detail ?? "";
|
||||
return d === "kernel rig already managed"
|
||||
? "Kernel auto-boot skipped (kernel rig already managed). If its seats are not running, for example after a reboot, bring it back with: rig up kernel --existing"
|
||||
: d.startsWith("kernel rig already managed")
|
||||
? `Kernel auto-boot skipped: ${d}`
|
||||
: "Kernel auto-boot skipped (--no-kernel or test mode), including the operator that helps you start a team. To start it later, run rig, set up or select kernel, then start its operator seat.";
|
||||
}
|
||||
|
||||
export async function waitForKernelReady(
|
||||
baseUrl: string,
|
||||
timeoutMs: number,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { waitForKernelReady, type KernelReadyResult } from "../src/daemon-lifecycle.js";
|
||||
import { daemonCommand } from "../src/commands/daemon.js";
|
||||
|
||||
vi.mock("../src/daemon-lifecycle.js", async (original) => {
|
||||
const actual = await original<typeof import("../src/daemon-lifecycle.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getDaemonStatus: vi.fn(async () => ({ state: "stopped" })),
|
||||
startDaemon: vi.fn(async () => ({ pid: 4242, port: 7433, host: "127.0.0.1" })),
|
||||
waitForKernelReady: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock("../src/system-preflight.js", () => ({ SystemPreflight: class { run = async () => ({ ready: true, checks: [] }); }, quietPreflightExec: async () => "" }));
|
||||
let home: string;
|
||||
let errors: string[];
|
||||
beforeEach(() => {
|
||||
home = mkdtempSync(join(tmpdir(), "daemon-start-wait-for-kernel-"));
|
||||
vi.stubEnv("OPENRIG_HOME", home);
|
||||
errors = [];
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => { errors.push(args.join(" ")); });
|
||||
});
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); rmSync(home, { recursive: true, force: true }); process.exitCode = undefined; });
|
||||
|
||||
async function startWaiting(result: Partial<KernelReadyResult>) {
|
||||
vi.mocked(waitForKernelReady).mockResolvedValue({ ok: false, kernelState: null, variant: null, detail: null, ...result });
|
||||
await daemonCommand({} as never).parseAsync(["start", "--wait-for-kernel"], { from: "user" });
|
||||
return errors.join("\n");
|
||||
}
|
||||
|
||||
it("a kernel already down before this start: says so and how to bring it back, not a timeout or an auth fix", async () => {
|
||||
const out = await startWaiting({ kernelState: "skipped",
|
||||
detail: "kernel rig already managed; it was already down before this daemon start, so it was not restored. Bring it back with: rig up kernel --existing" });
|
||||
expect(out).toContain("Kernel auto-boot skipped: kernel rig already managed; it was already down before this daemon start");
|
||||
expect(out).toContain("rig up kernel --existing");
|
||||
expect(out).not.toContain("did not reach ready");
|
||||
expect(out).not.toContain("auth status");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("a kernel skipped on purpose: names the opt-out, not a timeout or an auth fix", async () => {
|
||||
const out = await startWaiting({ kernelState: "skipped", detail: "OPENRIG_NO_KERNEL=1" });
|
||||
expect(out).toContain("Kernel auto-boot skipped (--no-kernel or test mode)");
|
||||
expect(out).not.toContain("did not reach ready");
|
||||
expect(out).not.toContain("auth status");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("a kernel still booting at the deadline keeps the timeout and its fix", async () => {
|
||||
const out = await startWaiting({ kernelState: "booting" });
|
||||
expect(out).toContain("Error: kernel did not reach ready / partial_ready within 60000ms.");
|
||||
expect(out).toContain("Reason: kernel_state=booting");
|
||||
expect(out).toContain("Fix: inspect `rig ps --rig kernel` for stalled agents");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
@@ -72,6 +72,14 @@ describe("rig start kernel wait evidence", () => {
|
||||
expect(result.errors).toBe(""); expect(result.exitCode).toBe(0);
|
||||
expect(result.received).toContain("/api/rigs/summary");
|
||||
});
|
||||
it("says a kernel that was already down before this daemon start stays down, and how to bring it back", async () => {
|
||||
const result = await run("skipped", 200, 200, {}, false,
|
||||
"kernel rig already managed; it was already down before this daemon start, so it was not restored. Bring it back with: rig up kernel --existing");
|
||||
expect(result.logs).toContain("Kernel auto-boot skipped: kernel rig already managed; it was already down before this daemon start");
|
||||
expect(result.logs).toContain("rig up kernel --existing");
|
||||
expect(result.logs).not.toContain("--no-kernel or test mode");
|
||||
expect(result.errors).toBe(""); expect(result.exitCode).toBe(0);
|
||||
});
|
||||
it("names an already-managed kernel and how to bring it back, instead of blaming --no-kernel", async () => {
|
||||
const result = await run("skipped", 200, 200, {}, false, "kernel rig already managed");
|
||||
expect(result.logs).not.toContain("--no-kernel or test mode");
|
||||
|
||||
@@ -116,7 +116,10 @@ export async function bootKernelIfNeeded(deps: KernelBootDeps): Promise<KernelBo
|
||||
return tracker;
|
||||
}
|
||||
log("info", `kernel-boot: kernel rig already managed (${managed.kind}); skipping builtin boot`);
|
||||
tracker.setSkipped("kernel rig already managed");
|
||||
// A stale kernel stays down on purpose; say why and how to bring it back (`rig start` prints it).
|
||||
tracker.setSkipped(managed.kind === "stale"
|
||||
? "kernel rig already managed; it was already down before this daemon start, so it was not restored. Bring it back with: rig up kernel --existing"
|
||||
: "kernel rig already managed");
|
||||
if (managed.kind === "live") tracker.observeManagedKernel();
|
||||
return tracker;
|
||||
}
|
||||
|
||||
@@ -108,9 +108,10 @@ export class Reconciler {
|
||||
});
|
||||
});
|
||||
const persistedEvent = txn();
|
||||
this.eventBus.notifySubscribers(persistedEvent);
|
||||
// Detached once the transaction commits, even if notifying afterwards throws.
|
||||
detached++;
|
||||
detachedSessionIds.push(session.id);
|
||||
this.eventBus.notifySubscribers(persistedEvent);
|
||||
} catch (err) {
|
||||
errors.push({
|
||||
sessionId: session.id,
|
||||
|
||||
@@ -133,7 +133,11 @@ describe("daemon start with an existing kernel rig", () => {
|
||||
const restore = vi.fn(async () => ({ errors: [] }));
|
||||
const tracker = await h.daemonStart(restore);
|
||||
expect(restore).not.toHaveBeenCalled();
|
||||
expect(tracker.getStatus()).toMatchObject({ kernelState: "skipped", detail: "kernel rig already managed" });
|
||||
// The person is told why it stayed down and how to bring it back.
|
||||
expect(tracker.getStatus()).toMatchObject({
|
||||
kernelState: "skipped",
|
||||
detail: "kernel rig already managed; it was already down before this daemon start, so it was not restored. Bring it back with: rig up kernel --existing",
|
||||
});
|
||||
expect(classifyManagedKernel(h.rigRepo, h.sessionRegistry, await h.reconcile())).toEqual({ kind: "stale" });
|
||||
expect(h.bootstrap.bootstrap).not.toHaveBeenCalled();
|
||||
expect(h.statuses()).toEqual(["detached", "detached"]);
|
||||
|
||||
@@ -119,6 +119,23 @@ describe("Reconciler", () => {
|
||||
expect(notifications[0]!.type).toBe("session.detached");
|
||||
});
|
||||
|
||||
it("a session counts as detached once its transaction commits, even if notifying afterwards throws", async () => {
|
||||
const { rig } = seedRigWithSessions([
|
||||
{ logicalId: "dev1-impl", sessionName: "r01-dev1-impl", status: "running" },
|
||||
]);
|
||||
// The bus catches a subscriber's own error; this stands for a database error in its post-commit drain.
|
||||
vi.spyOn(eventBus, "notifySubscribers").mockImplementation(() => { throw new Error("post-commit notify failed"); });
|
||||
|
||||
const reconciler = createReconciler(mockTmuxAdapter({ "r01-dev1-impl": false }));
|
||||
const result = await reconciler.reconcile(rig.id);
|
||||
|
||||
const session = sessionRegistry.getSessionsForRig(rig.id)[0]!;
|
||||
expect(session.status).toBe("detached");
|
||||
expect(result.detached).toBe(1);
|
||||
expect(result.detachedSessionIds).toEqual([session.id]);
|
||||
expect(result.errors).toEqual([{ sessionId: session.id, error: "post-commit notify failed" }]);
|
||||
});
|
||||
|
||||
it("no sessions in DB: no changes, no events", async () => {
|
||||
const rig = rigRepo.createRig("r01");
|
||||
const reconciler = createReconciler(mockTmuxAdapter({}));
|
||||
|
||||
Reference in new issue
Block a user