mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-11 17:32:22 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bb7d65c2 | ||
|
|
a5ff738fbb |
No files matched your search
@@ -3,6 +3,7 @@ import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl , daemonStatusGuard} from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
import { resolveRigHandleInSummaries } from "../rig-handle.js";
|
||||
|
||||
const LONG_RUNNING_TIMEOUT_MS = 45_000;
|
||||
|
||||
@@ -30,8 +31,12 @@ Examples:
|
||||
throw new Error(error ?? `Failed to list rigs (HTTP ${summaries.status})`);
|
||||
}
|
||||
if (!Array.isArray(summaries.data)) throw new Error("The daemon returned an invalid rig summary.");
|
||||
const match = summaries.data.find((r) => r.name === rigRef || r.id === rigRef);
|
||||
if (!match) throw new Error(`Rig '${rigRef}' not found`);
|
||||
// Keep this command's existing HTTP/invalid-summary diagnostics and its one summary read.
|
||||
const match = resolveRigHandleInSummaries(summaries.data, rigRef);
|
||||
if (match.kind === "ambiguous") throw Object.assign(new Error(
|
||||
`rig_ambiguous: '${rigRef}' matches ${match.ids.length} rigs (${match.ids.join(", ")}). Nothing was done; pass one of these ids instead.`,
|
||||
), { code: "rig_ambiguous", candidates: match.ids });
|
||||
if (match.kind === "not_found") throw new Error(`Rig '${rigRef}' not found`);
|
||||
return match.id;
|
||||
}
|
||||
|
||||
@@ -48,7 +53,11 @@ Examples:
|
||||
|
||||
let rigId: string;
|
||||
try { rigId = await resolveRigId(client, rig); } catch (err) {
|
||||
console.error((err as Error).message); process.exitCode = 1; return;
|
||||
const error = err as Error & { code?: string; candidates?: string[] };
|
||||
if (opts.json && error.code === "rig_ambiguous") {
|
||||
console.log(JSON.stringify({ ok: false, code: error.code, message: error.message, candidates: error.candidates }, null, 2));
|
||||
} else console.error(error.message);
|
||||
process.exitCode = 1; return;
|
||||
}
|
||||
|
||||
const res = await client.get<Record<string, unknown>>(`/api/rigs/${encodeURIComponent(rigId)}/env`);
|
||||
|
||||
@@ -73,6 +73,15 @@ export async function resolveRigHandle(
|
||||
return { kind: "passthrough", handle };
|
||||
}
|
||||
|
||||
return resolveRigHandleInSummaries(summaries, handle, opts);
|
||||
}
|
||||
|
||||
/** The shared id-first/name-ambiguity rule for callers that already fetched and checked a summary. */
|
||||
export function resolveRigHandleInSummaries(
|
||||
summaries: RigSummaryEntry[],
|
||||
handle: string,
|
||||
opts: { archived?: boolean } = {},
|
||||
): Exclude<HandleResolution, { kind: "passthrough" }> {
|
||||
// 1. id-exact-match first, across ALL rigs incl. archived (ids are never
|
||||
// ambiguous; archived ids still reach the id path).
|
||||
if (summaries.some((r) => r.id === handle)) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Real command parsing and HTTP client, a task-owned simulated daemon, no service action.
|
||||
import { createServer } from "node:http";
|
||||
import { Command } from "commander";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { DaemonClient } from "../src/client.js";
|
||||
import { envCommand } from "../src/commands/env.js";
|
||||
import { STATE_FILE } from "../src/daemon-lifecycle.js";
|
||||
import type { StatusDeps } from "../src/commands/status.js";
|
||||
|
||||
const cases = [
|
||||
{ name: "ambiguous", ref: "twin", rigs: [{ id: "rig-a", name: "twin" }, { id: "rig-b", name: "twin" }], expected: null },
|
||||
{ name: "exact-id", ref: "chosen", rigs: [{ id: "rig-a", name: "chosen" }, { id: "chosen", name: "other" }], expected: "chosen" },
|
||||
{ name: "unique-name", ref: "solo", rigs: [{ id: "rig-a", name: "solo" }], expected: "rig-a" },
|
||||
];
|
||||
it.each(cases.flatMap(c => ["status", "logs", "down"].map(operation => ({ ...c, operation }))))(
|
||||
"env $operation resolves $name before a service request", async ({ operation, ref, rigs, expected }) => {
|
||||
const requests: string[] = [];
|
||||
const payloads: unknown[] = [];
|
||||
const server = createServer((req, res) => {
|
||||
requests.push(`${req.method} ${req.url}`);
|
||||
let body = ""; req.on("data", chunk => { body += chunk; });
|
||||
req.on("end", () => {
|
||||
if (body) payloads.push(JSON.parse(body));
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify(req.url === "/api/rigs/summary" ? rigs.map(r => ({ ...r, archivedAt: null }))
|
||||
: { ok: true, hasServices: false, output: "fixture logs" }));
|
||||
});
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = (server.address() as { port: number }).port;
|
||||
const deps: StatusDeps = {
|
||||
lifecycleDeps: {
|
||||
readFile: (p: string) => p === STATE_FILE ? JSON.stringify({ pid: 123, port, db: "fixture" }) : null,
|
||||
exists: (p: string) => p === STATE_FILE, isProcessAlive: () => true, fetch: async () => ({ ok: true }),
|
||||
} as StatusDeps["lifecycleDeps"],
|
||||
clientFactory: () => new DaemonClient(`http://127.0.0.1:${port}`),
|
||||
};
|
||||
const output: string[] = [];
|
||||
const log = vi.spyOn(console, "log").mockImplementation((...a) => { output.push(a.join(" ")); });
|
||||
const error = vi.spyOn(console, "error").mockImplementation((...a) => { output.push(a.join(" ")); });
|
||||
const priorExitCode = process.exitCode; process.exitCode = undefined;
|
||||
try {
|
||||
const program = new Command(); program.addCommand(envCommand(deps));
|
||||
await program.parseAsync(["node", "rig", "env", operation, ref,
|
||||
...(operation === "status" ? ["--json"] : operation === "down" ? ["--volumes"] : [])]);
|
||||
expect(["darwin", "linux"]).toContain(process.platform);
|
||||
expect(requests[0]).toBe("GET /api/rigs/summary");
|
||||
if (expected === null) {
|
||||
expect(requests).toEqual(["GET /api/rigs/summary"]);
|
||||
expect(payloads).toEqual([]);
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(output.join("\n")).toContain("rig_ambiguous");
|
||||
expect(output.join("\n")).toContain("rig-a"); expect(output.join("\n")).toContain("rig-b");
|
||||
if (operation === "status") expect(JSON.parse(output[0]!)).toMatchObject({ code: "rig_ambiguous", candidates: ["rig-a", "rig-b"] });
|
||||
} else {
|
||||
const route = `/api/rigs/${expected}/env` + (operation === "down" ? "/down" : operation === "logs" ? "/logs?tail=100" : "");
|
||||
expect(requests).toEqual(["GET /api/rigs/summary", `${operation === "down" ? "POST" : "GET"} ${route}`]);
|
||||
if (operation === "down") expect(payloads).toEqual([{ volumes: true }]);
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
}
|
||||
} finally {
|
||||
log.mockRestore(); error.mockRestore(); process.exitCode = priorExitCode;
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
expect(server.listening).toBe(false);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user