mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
rig compact-plan no longer treats an incomplete daemon inventory as a complete one. For a fleet plan, a rig whose node read fails is left out and reported in incompleteRigs with its HTTP status, invalid_inventory or request_failed; the plan is built from the rigs that read successfully, text output prints a partial-plan warning on stderr, JSON always carries incompleteRigs, and the exit code is 1 when anything is incomplete. A failed read for an explicit --rig, or of the process list, still fails with exit 2 and no plan.
101 lines
5.8 KiB
TypeScript
101 lines
5.8 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
|
import http from "node:http";
|
|
import { compactPlanCommand } from "../src/commands/compact-plan.js";
|
|
import { DaemonClient } from "../src/client.js";
|
|
import { STATE_FILE } from "../src/daemon-lifecycle.js";
|
|
|
|
let server: http.Server | undefined;
|
|
afterEach(async () => {
|
|
if (server) {
|
|
server.closeAllConnections();
|
|
await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
server = undefined;
|
|
}
|
|
vi.restoreAllMocks();
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
it("marks partial native HTTP inventory while preserving healthy rigs", async () => {
|
|
let failedRoute = "/api/ps";
|
|
let status = 503;
|
|
let malformedNodes = false;
|
|
let malformedRigs = false;
|
|
const requests: Array<{ method: string; path: string }> = [];
|
|
const candidate = {
|
|
rigId: "rig-a", rigName: "alpha", logicalId: "orch.lead", canonicalSessionName: "orch-lead@alpha",
|
|
runtime: "claude-code", sessionStatus: "running", startupStatus: "ready",
|
|
tmuxAttachCommand: "tmux attach -t orch-lead@alpha", resumeCommand: null,
|
|
contextUsage: { usedPercentage: 90, remainingPercentage: 10, contextWindowSize: 1_000_000,
|
|
source: "claude_statusline_json", availability: "known", sampledAt: new Date().toISOString(), fresh: true },
|
|
};
|
|
server = http.createServer((req, res) => {
|
|
requests.push({ method: req.method!, path: req.url! });
|
|
if (req.url === failedRoute && status === 0) { req.socket.destroy(); return; }
|
|
const code = req.url === failedRoute ? status : 200;
|
|
const data = req.url === "/api/ps"
|
|
? code === 200 ? malformedRigs ? null : [{ rigId: "rig-a", name: "alpha" }, { rigId: "rig-b", name: "beta" }] : []
|
|
: req.url === "/api/rigs/rig-a/nodes" ? [candidate] : code === 200 && !malformedNodes ? [] : { error: "inventory unavailable" };
|
|
res.writeHead(code, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify(data));
|
|
});
|
|
await new Promise<void>((resolve) => server!.listen(0, "127.0.0.1", resolve));
|
|
const port = (server.address() as { port: number }).port;
|
|
const deps = { clientFactory: (base: string) => new DaemonClient(base), lifecycleDeps: {
|
|
exists: (p: string) => p === STATE_FILE,
|
|
readFile: () => JSON.stringify({ pid: process.pid, port, db: "fixture.sqlite", startedAt: new Date().toISOString() }),
|
|
isProcessAlive: () => true, fetch: async () => ({ ok: true }),
|
|
} as never };
|
|
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
for (const output of [[], ["--json"]]) {
|
|
for (const route of ["/api/ps", "/api/rigs/rig-b/nodes"]) {
|
|
for (const code of [401, 503]) {
|
|
failedRoute = route; status = code; process.exitCode = undefined;
|
|
log.mockClear(); error.mockClear(); requests.length = 0;
|
|
await compactPlanCommand(deps).parseAsync(output, { from: "user" });
|
|
expect.soft(process.exitCode).toBe(route === "/api/ps" ? 2 : 1);
|
|
if (route === "/api/ps") {
|
|
expect.soft(error.mock.calls.flat().join(" ")).toContain(`HTTP ${code}`);
|
|
expect.soft(log).not.toHaveBeenCalled();
|
|
} else if (output.length) {
|
|
const plan = JSON.parse(String(log.mock.calls[0]?.[0] ?? "{}"));
|
|
expect.soft(plan.incompleteRigs).toEqual([{ rig: "beta", status: code }]);
|
|
expect.soft(plan.candidates?.[0]?.session).toBe("orch-lead@alpha");
|
|
} else {
|
|
expect.soft(log.mock.calls.flat().join(" ")).toContain("orch-lead@alpha");
|
|
expect.soft(error.mock.calls.flat().join(" ")).toContain(`beta (HTTP ${code})`);
|
|
}
|
|
expect(requests.every((request) => request.method === "GET")).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
failedRoute = ""; malformedRigs = true; process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--json"], { from: "user" });
|
|
expect(process.exitCode).toBe(2); expect(error.mock.calls.flat().join(" ")).toContain("Rig inventory is not an array");
|
|
expect(log).not.toHaveBeenCalled();
|
|
malformedRigs = false; malformedNodes = true; process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--json"], { from: "user" });
|
|
expect(process.exitCode).toBe(1);
|
|
expect(JSON.parse(String(log.mock.calls[0]![0])).incompleteRigs).toEqual([{ rig: "beta", status: "invalid_inventory" }]);
|
|
process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--rig", "beta", "--json"], { from: "user" });
|
|
expect(process.exitCode).toBe(2); expect(log).not.toHaveBeenCalled();
|
|
expect(error.mock.calls.flat().join(" ")).toContain("is not an array");
|
|
malformedNodes = false; failedRoute = "/api/rigs/rig-b/nodes"; status = 503;
|
|
process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--rig", "beta", "--json"], { from: "user" });
|
|
expect(process.exitCode).toBe(2); expect(log).not.toHaveBeenCalled();
|
|
expect(error.mock.calls.flat().join(" ")).toContain("HTTP 503");
|
|
status = 0; process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--json"], { from: "user" });
|
|
const partial = JSON.parse(String(log.mock.calls[0]![0]));
|
|
expect(process.exitCode).toBe(1);
|
|
expect(partial.incompleteRigs).toEqual([{ rig: "beta", status: "request_failed" }]);
|
|
expect(partial.candidates[0].session).toBe("orch-lead@alpha");
|
|
failedRoute = ""; process.exitCode = undefined; log.mockClear(); error.mockClear();
|
|
await compactPlanCommand(deps).parseAsync(["--json"], { from: "user" });
|
|
expect(process.exitCode).toBeUndefined(); expect(error).not.toHaveBeenCalled();
|
|
expect(JSON.parse(String(log.mock.calls[0]![0])).summary.candidateCount).toBe(1);
|
|
expect(JSON.parse(String(log.mock.calls[0]![0])).incompleteRigs).toEqual([]);
|
|
});
|