mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(cli,daemon): rig start uses id-based route for both preview AND apply (OPR.0.3.4.1 rev1 BLOCKING)
Selection and apply were name-grounded (rigName + POST /api/up) while preview was id-grounded (POST /api/rigs/:id/up). Same-name rigs could be listed but not restored (ambiguous_name 409 from /api/up). Now the entire flow is id-grounded: selectedCandidates carries the full candidate object with rigId; apply uses POST /api/rigs/:id/up; awaiting-decision fresh-prime retry is also id-based; idempotent re-check matches by rig.id not name; picker values use rigId. The Explorer route /:id/up now parses freshLogicalIds from the body and passes it to restore() so the fresh-prime retry works through the id-based route. Same-name test updated: /api/up returns ambiguous_name 409 to prove rig start never hits it; all four calls (2 plan + 2 apply) go through the id-based route verified via _rigIdRoute in upBodies.
This commit is contained in:
1 parent
7ecbe8d4e1
commit
6ba0de56d4
3 files changed
+163
-119
No files matched your search
@@ -325,15 +325,13 @@ Examples:
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- PHASE 4: selection ----
|
||||
let selectedNames: string[];
|
||||
if (opts.all) {
|
||||
selectedNames = candidates.map((c) => c.rigName);
|
||||
} else if (opts.last) {
|
||||
selectedNames = candidates.map((c) => c.rigName);
|
||||
// ---- PHASE 4: selection (id-grounded, rev1 BLOCKING b4c6ada4) ----
|
||||
let selectedCandidates: StartCandidate[];
|
||||
if (opts.all || opts.last) {
|
||||
selectedCandidates = [...candidates];
|
||||
} else if (opts.rigs) {
|
||||
selectedNames = opts.rigs;
|
||||
const missing = selectedNames.filter((n) => !candidates.some((c) => c.rigName === n));
|
||||
selectedCandidates = candidates.filter((c) => opts.rigs!.includes(c.rigName));
|
||||
const missing = opts.rigs.filter((n) => !candidates.some((c) => c.rigName === n));
|
||||
if (missing.length > 0) {
|
||||
console.error(`Rig(s) not found in candidate set: ${missing.join(", ")}`);
|
||||
console.error(`Available candidates: ${candidates.map((c) => c.rigName).join(", ")}`);
|
||||
@@ -364,42 +362,37 @@ Examples:
|
||||
const restoreAll = await ask(`Restore all ${candidates.length} rig(s)? [Y/pick] `);
|
||||
|
||||
if (restoreAll) {
|
||||
selectedNames = candidates.map((c) => c.rigName);
|
||||
selectedCandidates = [...candidates];
|
||||
} else {
|
||||
const picked = await multiSelectPicker(
|
||||
const pickedIds = await multiSelectPicker(
|
||||
candidates.map((c) => ({
|
||||
label: `${c.rigName} (${c.nodeCount} seats, ${c.lifecycleState}) ${summarizeReadiness(c)}`,
|
||||
value: c.rigName,
|
||||
value: c.rigId,
|
||||
checked: true,
|
||||
})),
|
||||
);
|
||||
selectedNames = picked;
|
||||
selectedCandidates = candidates.filter((c) => pickedIds.includes(c.rigId));
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedNames.length === 0) {
|
||||
if (selectedCandidates.length === 0) {
|
||||
if (!opts.json) console.log("No rigs selected for restore.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- PHASE 5: restore each selected rig via /api/up (slice-02 path) ----
|
||||
if (!opts.json) console.log(`\nRestoring ${selectedNames.length} rig(s)...\n`);
|
||||
// ---- PHASE 5: restore each selected rig via id-based route (compose slice-02 path) ----
|
||||
if (!opts.json) console.log(`\nRestoring ${selectedCandidates.length} rig(s)...\n`);
|
||||
|
||||
const results: Array<{ rigName: string; status: string; nodes: Array<{ logicalId: string; status: string; error?: string }> }> = [];
|
||||
|
||||
for (const rigName of selectedNames) {
|
||||
const candidate = candidates.find((c) => c.rigName === rigName);
|
||||
if (!candidate) {
|
||||
console.error(` ${rigName}: not found in candidates (skipping)`);
|
||||
results.push({ rigName, status: "skipped", nodes: [] });
|
||||
continue;
|
||||
}
|
||||
for (const candidate of selectedCandidates) {
|
||||
const rigName = candidate.rigName;
|
||||
const rigId = candidate.rigId;
|
||||
|
||||
// Check if the rig is already running (idempotent rerun: reconcile, don't relaunch).
|
||||
// Re-fetch status since we may have restored other rigs in the loop.
|
||||
try {
|
||||
const freshSummary = await client.get<RigSummary[]>("/api/rigs/summary");
|
||||
const current = (freshSummary.data ?? []).find((r) => r.name === rigName);
|
||||
const current = (freshSummary.data ?? []).find((r) => r.id === rigId);
|
||||
if (current?.lifecycleState === "running") {
|
||||
if (!opts.json) console.log(` ${rigName}: already running (skipping)`);
|
||||
results.push({ rigName, status: "already_running", nodes: [] });
|
||||
@@ -408,10 +401,13 @@ Examples:
|
||||
} catch { /* proceed with restore attempt */ }
|
||||
|
||||
try {
|
||||
const res = await client.post<Record<string, unknown>>("/api/up", {
|
||||
sourceRef: rigName,
|
||||
plan: false,
|
||||
}, { timeoutMs: LONG_RUNNING_UP_TIMEOUT_MS });
|
||||
// Rev1 BLOCKING b4c6ada4: use id-based route for restore too (not
|
||||
// name-based /api/up which 409s on same-name rigs).
|
||||
const res = await client.post<Record<string, unknown>>(
|
||||
`/api/rigs/${encodeURIComponent(rigId)}/up`,
|
||||
{ plan: false },
|
||||
{ timeoutMs: LONG_RUNNING_UP_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
// Guard BLOCKING 25661f72: check HTTP status before treating the response
|
||||
// as a success. The daemon returns non-2xx JSON payloads (rig_not_stopped,
|
||||
@@ -462,11 +458,11 @@ Examples:
|
||||
}
|
||||
if (accepted.length > 0) {
|
||||
try {
|
||||
const freshRes = await client.post<Record<string, unknown>>("/api/up", {
|
||||
sourceRef: rigName,
|
||||
plan: false,
|
||||
freshLogicalIds: accepted,
|
||||
}, { timeoutMs: LONG_RUNNING_UP_TIMEOUT_MS });
|
||||
const freshRes = await client.post<Record<string, unknown>>(
|
||||
`/api/rigs/${encodeURIComponent(rigId)}/up`,
|
||||
{ plan: false, freshLogicalIds: accepted },
|
||||
{ timeoutMs: LONG_RUNNING_UP_TIMEOUT_MS },
|
||||
);
|
||||
if (freshRes.status >= 400) {
|
||||
const freshError = String(freshRes.data["error"] ?? "fresh-prime failed");
|
||||
console.error(` Fresh-prime for ${rigName}: ${freshError} (HTTP ${freshRes.status})`);
|
||||
|
||||
+128
-86
@@ -144,19 +144,20 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
{ id: "rig-1", name, nodeCount: 2, lifecycleState },
|
||||
];
|
||||
routes["/api/rigs/:id"] = () => ({ sessions: [{ lastSeenAt: "2026-06-14T12:00:00Z" }] });
|
||||
// Plan preview via id-based route (candidate discovery uses POST /api/rigs/:id/up).
|
||||
routes["/api/rigs/:id/up"] = () => ({
|
||||
status: "plan", mode: "restore", rigId: "rig-1", rigName: name,
|
||||
snapshot: { id: "snap-1", kind: "auto-pre-down", createdAt: "2026-06-14T11:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [
|
||||
{ logicalId: "dev.impl", intendedAction: hasToken ? "resume-original" : "awaiting-decision", ...(hasToken ? {} : { reason: "no token available" }) },
|
||||
{ logicalId: "dev.qa", intendedAction: "resume-original" },
|
||||
],
|
||||
mutated: false,
|
||||
});
|
||||
// Apply via name-based route (restore uses POST /api/up).
|
||||
routes["/api/up"] = () => {
|
||||
// Both plan preview AND apply go through the id-based route.
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => {
|
||||
if (body.plan === true) {
|
||||
return {
|
||||
status: "plan", mode: "restore", rigId: "rig-1", rigName: name,
|
||||
snapshot: { id: "snap-1", kind: "auto-pre-down", createdAt: "2026-06-14T11:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [
|
||||
{ logicalId: "dev.impl", intendedAction: hasToken ? "resume-original" : "awaiting-decision", ...(hasToken ? {} : { reason: "no token available" }) },
|
||||
{ logicalId: "dev.qa", intendedAction: "resume-original" },
|
||||
],
|
||||
mutated: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "restored", rigId: "rig-1", rigName: name,
|
||||
rigResult: "fully_restored",
|
||||
@@ -178,9 +179,9 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
await makeCmd(prompt).parseAsync(["node", "rig", "start", "--last"]);
|
||||
});
|
||||
expect(prompt).not.toHaveBeenCalled();
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true);
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true && b._rigIdRoute);
|
||||
expect(applyBodies.length).toBeGreaterThanOrEqual(1);
|
||||
expect(applyBodies[0]!.sourceRef).toBe("my-rig");
|
||||
expect(applyBodies[0]!._rigIdRoute).toBe("rig-1");
|
||||
const output = logs.join("\n");
|
||||
expect(output).toContain("my-rig");
|
||||
});
|
||||
@@ -202,33 +203,37 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
{ id: "rig-2", name: "beta", nodeCount: 1, lifecycleState: "recoverable" },
|
||||
];
|
||||
routes["/api/rigs/:id"] = () => ({ sessions: [] });
|
||||
routes["/api/rigs/:id/up"] = () => ({
|
||||
status: "plan", mode: "restore", rigId: "rig-x", rigName: "rig",
|
||||
snapshot: { id: "snap-1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
});
|
||||
routes["/api/up"] = () => ({ status: "restored", rigResult: "fully_restored", nodes: [{ logicalId: "w", status: "resumed" }], warnings: [] });
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => {
|
||||
if (body.plan === true) {
|
||||
return {
|
||||
status: "plan", mode: "restore", rigId: body._rigId ?? "rig-x", rigName: "rig",
|
||||
snapshot: { id: "snap-1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
};
|
||||
}
|
||||
return { status: "restored", rigResult: "fully_restored", nodes: [{ logicalId: "w", status: "resumed" }], warnings: [] };
|
||||
};
|
||||
|
||||
const { logs } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "start", "--rigs", "alpha"]);
|
||||
});
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true);
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true && b._rigIdRoute);
|
||||
expect(applyBodies).toHaveLength(1);
|
||||
expect(applyBodies[0]!.sourceRef).toBe("alpha");
|
||||
expect(applyBodies[0]!._rigIdRoute).toBe("rig-1");
|
||||
});
|
||||
|
||||
// ---- COMPOSE, NOT REIMPLEMENT ----
|
||||
|
||||
it("restore goes through /api/up (slice-02 path), not a private implementation", async () => {
|
||||
it("restore goes through /api/rigs/:id/up (id-based route), not a private implementation", async () => {
|
||||
setupCandidateRig("my-rig");
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "start", "--last"]);
|
||||
});
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true);
|
||||
const applyBodies = upBodies.filter((b) => b.plan !== true && b._rigIdRoute);
|
||||
expect(applyBodies.length).toBeGreaterThanOrEqual(1);
|
||||
expect(applyBodies.some((b) => b.sourceRef === "my-rig")).toBe(true);
|
||||
expect(applyBodies.some((b) => b._rigIdRoute === "rig-1")).toBe(true);
|
||||
});
|
||||
|
||||
// ---- TOKENLESS INCLUSION (guard rev1 discriminator) ----
|
||||
@@ -296,7 +301,8 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
let planCallCount = 0;
|
||||
throwClient.post = async (path: string, body?: unknown, opts?: unknown) => {
|
||||
const b = body as Record<string, unknown> | undefined;
|
||||
if (path === "/api/up" && b?.plan !== true) {
|
||||
// Apply now uses the id-based route; throw on any non-plan POST to /api/rigs/:id/up.
|
||||
if (path.match(/^\/api\/rigs\/[^/]+\/up$/) && b?.plan !== true) {
|
||||
throw new (await import("../src/client.js")).DaemonConnectionError("request timed out");
|
||||
}
|
||||
return origPost(path, body, opts);
|
||||
@@ -321,26 +327,30 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
const validTerms = ["resumed", "fresh-primed", "awaiting-decision", "attention_required", "failed"];
|
||||
routes["/api/rigs/summary"] = () => [{ id: "r1", name: "term-rig", nodeCount: 3, lifecycleState: "recoverable" }];
|
||||
routes["/api/rigs/:id"] = () => ({ sessions: [] });
|
||||
routes["/api/rigs/:id/up"] = () => ({
|
||||
status: "plan", mode: "restore", rigId: "r1", rigName: "term-rig",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [
|
||||
{ logicalId: "a", intendedAction: "resume-original" },
|
||||
{ logicalId: "b", intendedAction: "awaiting-decision", reason: "no token" },
|
||||
{ logicalId: "c", intendedAction: "fresh-primed" },
|
||||
],
|
||||
mutated: false,
|
||||
});
|
||||
routes["/api/up"] = () => ({
|
||||
status: "restored", rigResult: "partially_restored",
|
||||
nodes: [
|
||||
{ logicalId: "a", status: "resumed" },
|
||||
{ logicalId: "b", status: "awaiting-decision", error: "no token" },
|
||||
{ logicalId: "c", status: "fresh-primed" },
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => {
|
||||
if (body.plan === true) {
|
||||
return {
|
||||
status: "plan", mode: "restore", rigId: "r1", rigName: "term-rig",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [
|
||||
{ logicalId: "a", intendedAction: "resume-original" },
|
||||
{ logicalId: "b", intendedAction: "awaiting-decision", reason: "no token" },
|
||||
{ logicalId: "c", intendedAction: "fresh-primed" },
|
||||
],
|
||||
mutated: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "restored", rigResult: "partially_restored",
|
||||
nodes: [
|
||||
{ logicalId: "a", status: "resumed" },
|
||||
{ logicalId: "b", status: "awaiting-decision", error: "no token" },
|
||||
{ logicalId: "c", status: "fresh-primed" },
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
};
|
||||
|
||||
const { logs } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "start", "--last"]);
|
||||
@@ -360,19 +370,37 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
{ id: "rig-2", name: "foo", nodeCount: 1, lifecycleState: "detached" },
|
||||
];
|
||||
routes["/api/rigs/:id"] = () => ({ sessions: [] });
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => ({
|
||||
status: "plan", mode: "restore",
|
||||
rigId: body._rigId ?? "unknown", rigName: "foo",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
});
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => {
|
||||
if (body.plan === true || body._rigId) {
|
||||
if (body.plan === true) {
|
||||
return {
|
||||
status: "plan", mode: "restore",
|
||||
rigId: body._rigId ?? "unknown", rigName: "foo",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "restored", rigResult: "fully_restored",
|
||||
nodes: [{ logicalId: "w", status: "resumed" }],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "restored", rigResult: "fully_restored",
|
||||
nodes: [{ logicalId: "w", status: "resumed" }],
|
||||
warnings: [],
|
||||
};
|
||||
};
|
||||
// Name-based /api/up would return ambiguous_name 409 for same-name rigs;
|
||||
// rig start should never reach this path for restore since it uses id-based route.
|
||||
routes["/api/up"] = () => ({
|
||||
status: "restored", rigResult: "fully_restored",
|
||||
nodes: [{ logicalId: "w", status: "resumed" }],
|
||||
warnings: [],
|
||||
});
|
||||
_httpStatus: 409,
|
||||
error: 'Multiple rigs named "foo" found',
|
||||
code: "ambiguous_name",
|
||||
} as never);
|
||||
|
||||
const { logs } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "start", "--all"]);
|
||||
@@ -380,11 +408,15 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
const output = logs.join("\n");
|
||||
// Both should appear — never "No rigs to restore".
|
||||
expect(output).not.toContain("No rigs to restore");
|
||||
// The id-based preview route was used (verify via upBodies).
|
||||
const planBodies = upBodies.filter((b) => b.plan === true);
|
||||
expect(planBodies.length).toBe(2);
|
||||
expect(planBodies.some((b) => b._rigIdRoute === "rig-1")).toBe(true);
|
||||
expect(planBodies.some((b) => b._rigIdRoute === "rig-2")).toBe(true);
|
||||
expect(output).not.toContain("ambiguous_name");
|
||||
// The id-based route was used for both preview AND apply.
|
||||
const idBodies = upBodies.filter((b) => b._rigIdRoute);
|
||||
expect(idBodies.length).toBeGreaterThanOrEqual(4); // 2 plan + 2 apply
|
||||
expect(idBodies.some((b) => b._rigIdRoute === "rig-1")).toBe(true);
|
||||
expect(idBodies.some((b) => b._rigIdRoute === "rig-2")).toBe(true);
|
||||
// Name-based /api/up was NOT called for restore.
|
||||
const nameBodies = upBodies.filter((b) => !b._rigIdRoute);
|
||||
expect(nameBodies).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ---- PREVIEW ERROR STATE (guard BLOCKING bfed4ce3) ----
|
||||
@@ -489,20 +521,19 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
const rigUpMatch2 = method === "POST" && url.match(/^\/api\/rigs\/([^/]+)\/up$/);
|
||||
if (rigUpMatch2) {
|
||||
upBodies.push({ ...body, _rigIdRoute: rigUpMatch2[1] });
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
status: "plan", mode: "restore", rigId: rigUpMatch2[1], rigName: "live-rig",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (method === "POST" && url === "/api/up") {
|
||||
upBodies.push(body);
|
||||
res.writeHead(409, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "Rig r1 has live sessions", code: "rig_not_stopped" }));
|
||||
if (body.plan === true) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({
|
||||
status: "plan", mode: "restore", rigId: rigUpMatch2[1], rigName: "live-rig",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "w", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(409, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "Rig r1 has live sessions", code: "rig_not_stopped" }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
res.writeHead(404).end();
|
||||
@@ -541,12 +572,23 @@ describe("rig start (OPR.0.3.4.1)", () => {
|
||||
|
||||
it("partially_restored rigResult -> nonzero exit (not treated as clean)", async () => {
|
||||
setupCandidateRig("partial-rig");
|
||||
// Override apply response only; the id-based plan preview from setupCandidateRig is fine.
|
||||
routes["/api/up"] = () => ({
|
||||
status: "restored", rigResult: "partially_restored",
|
||||
nodes: [{ logicalId: "dev.impl", status: "fresh-primed" }],
|
||||
warnings: [],
|
||||
});
|
||||
// Override both plan and apply to return partially_restored on apply.
|
||||
routes["/api/rigs/:id/up"] = (_req, body) => {
|
||||
if (body.plan === true) {
|
||||
return {
|
||||
status: "plan", mode: "restore", rigId: "rig-1", rigName: "partial-rig",
|
||||
snapshot: { id: "s1", kind: "auto-pre-down", createdAt: "2026-06-14T00:00:00Z" },
|
||||
wouldCaptureCurrentState: false,
|
||||
nodes: [{ logicalId: "dev.impl", intendedAction: "resume-original" }],
|
||||
mutated: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "restored", rigResult: "partially_restored",
|
||||
nodes: [{ logicalId: "dev.impl", status: "fresh-primed" }],
|
||||
warnings: [],
|
||||
};
|
||||
};
|
||||
|
||||
const { exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "start", "--last"]);
|
||||
|
||||
@@ -481,6 +481,11 @@ rigsRoutes.post("/:id/up", async (c) => {
|
||||
// NO body, so plan:true was silently ignored and the route always mutated.
|
||||
const body: Record<string, unknown> = await c.req.json().catch(() => ({}));
|
||||
const plan = body["plan"] === true;
|
||||
// OPR.0.3.4.1 — thread freshLogicalIds so the id-based route supports
|
||||
// the awaiting-decision fresh-prime retry (operation B).
|
||||
const freshLogicalIds = Array.isArray(body["freshLogicalIds"])
|
||||
? (body["freshLogicalIds"] as unknown[]).filter((v): v is string => typeof v === "string")
|
||||
: undefined;
|
||||
|
||||
const snapshotRepo = c.get("snapshotRepo" as never) as SnapshotRepository;
|
||||
const snapshotCapture = c.get("snapshotCapture" as never) as SnapshotCapture;
|
||||
@@ -518,6 +523,7 @@ rigsRoutes.post("/:id/up", async (c) => {
|
||||
const result = await restoreOrch.restore(snapshot.id, {
|
||||
adapters: adapters ?? {},
|
||||
fsOps: { exists: (p: string) => fs.existsSync(p) },
|
||||
freshLogicalIds,
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (result.code === "pre_restore_validation_failed") {
|
||||
|
||||
Reference in new issue
Block a user