mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
fix(slice-22): import_rig stage failures surface HTTP 4xx + top-level code (OPR.0.3.2.22 Bug 1 BONUS)
Closes the homepage rig up <builtin> bare-500 UX trap from a fresh 0.3.1 install: when the daemon's import_rig stage failed with a code like cycle_error, the daemon returned HTTP 500 with the real code buried in stages[N].detail.code. The CLI's code-specific branches (packages/cli/src/commands/up.ts:208-227) never fired because res.data["code"] was undefined at top level. The fix: - Extend the apply-mode 4xx mapping in /api/up to include import_rig stage failures with known codes: cycle_error, preflight_failed, validation_failed, service_boot_failed. resolve_spec stage mapping is preserved as-is. - Surface the failure code at the top level of the response body (spread result + add code) when a 4xx-mappable code is found, so the CLI can branch on res.data["code"]. Plan-mode mapping (line 178-184) is NOT changed: pod-aware plan only runs resolve_spec + preflight stages and never reaches import_rig, so the existing resolve_spec-only check is correct for that path. Bug 1 itself (the conveyor delegates_to cycle) was already closed on main viaf3449baf, brought in by mergedc57b1e0(conveyor-trust recoverable-state fix). That cherry-pick was therefore a no-op and was explicitly skipped per the slice-22 dispatch ruling on qitem-20260601044405-584f6be2 state-query reply. This commit ships the in-scope BONUS that makes operator UX of any future import_rig-stage failure visible at the correct surface, not just the conveyor case. Verification: - HG-1 supporting evidence: existing apply-mode cycle_error case (covered by the conveyor builtin spec +f3449bafregression gate spec-library-starters.test.ts) plus this commit's new route-level pin: apply-mode delegates_to cycle returns HTTP 400 with code=cycle_error at top level. - Regression: full packages/daemon test/ suite green (271 files, 3987+ tests; 2 cold-start timeouts on schema-packages migration tests cleared on retry, pre-existing baseline flakiness — not introduced by this change). - tsc --noEmit on @openrig/daemon EXIT=0. Branch: slice-22-rig-up-paper-cut-fix-round off LOCAL main0c9e63f1. LOCAL only, NO push. Worktree at openrig/.worktrees/slice-22-rig-up-paper-cut-fix-round.
This commit is contained in:
1 parent
0c9e63f10e
commit
5048842701
2 files changed
+131
-3
No files matched your search
@@ -294,12 +294,29 @@ upRoutes.post("/", async (c) => {
|
||||
}
|
||||
eventBus.emit({ type: "bootstrap.failed", runId: result.runId, sourceRef, error: result.errors[0] ?? "failed" });
|
||||
const hasBlocked = result.stages.some((s) => s.status === "blocked");
|
||||
// OPR.0.3.2.22 Bug 1 BONUS — surface the failure code at the TOP LEVEL
|
||||
// and include import_rig-stage codes (cycle_error, preflight_failed,
|
||||
// validation_failed, service_boot_failed) in the 4xx map. CLI up.ts
|
||||
// already branches on res.data["code"] for these codes; before this
|
||||
// fix the daemon left the code buried in stages[N].detail.code and
|
||||
// returned a bare 500, which is exactly what the openrig-comms
|
||||
// hero-flow dogfood hit on a fresh 0.3.1 install.
|
||||
let topLevelCode: string | undefined;
|
||||
const hasBadRequest = result.stages.some((s) => {
|
||||
if (s.stage !== "resolve_spec") return false;
|
||||
if (s.status !== "failed") return false;
|
||||
const detail = s.detail as { code?: string } | undefined;
|
||||
return detail?.code === "file_not_found" || detail?.code === "parse_error" || detail?.code === "validation_failed" || detail?.code === "bundle_error" || detail?.code === "cycle_error" || detail?.code === "invalid_cwd";
|
||||
const code = detail?.code;
|
||||
if (!code) return false;
|
||||
const isResolveSpec4xx = s.stage === "resolve_spec" && (code === "file_not_found" || code === "parse_error" || code === "validation_failed" || code === "bundle_error" || code === "cycle_error" || code === "invalid_cwd");
|
||||
const isImportRig4xx = s.stage === "import_rig" && (code === "validation_failed" || code === "preflight_failed" || code === "cycle_error" || code === "service_boot_failed");
|
||||
if (isResolveSpec4xx || isImportRig4xx) {
|
||||
topLevelCode ??= code;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return c.json(result, hasBlocked ? 409 : hasBadRequest ? 400 : 500);
|
||||
const failedBody = topLevelCode ? { ...result, code: topLevelCode } : result;
|
||||
return c.json(failedBody, hasBlocked ? 409 : hasBadRequest ? 400 : 500);
|
||||
} catch (err) {
|
||||
bootstrapRepo.updateRunStatus(run.id, "failed");
|
||||
eventBus.emit({ type: "bootstrap.failed", runId: run.id, sourceRef, error: (err as Error).message });
|
||||
|
||||
@@ -794,4 +794,115 @@ edges: []
|
||||
db = { close: () => undefined } as unknown as Database.Database;
|
||||
});
|
||||
});
|
||||
|
||||
// --- OPR.0.3.2.22 Bug 1 BONUS — import_rig stage failures with known codes
|
||||
// (cycle_error, preflight_failed, validation_failed, service_boot_failed)
|
||||
// surface as HTTP 4xx with the code at the TOP LEVEL of the response
|
||||
// body. CLI up.ts:208-227 already branches on res.data["code"] for
|
||||
// these codes — the daemon just has to put the code where the CLI
|
||||
// looks, instead of leaving operators staring at a bare 500 with the
|
||||
// real cause buried in stages[N].detail.code.
|
||||
//
|
||||
// The conveyor delegates_to cycle (f3449baf already on main) is the
|
||||
// direct manifestation reported by the openrig-comms hero-flow
|
||||
// dogfood. This regression also pins the more general contract.
|
||||
describe("OPR.0.3.2.22 Bug 1 BONUS: import_rig stage failures map to HTTP 4xx with top-level code", () => {
|
||||
let specDir: string;
|
||||
let app3: ReturnType<typeof createTestApp>["app"];
|
||||
let db3: Database.Database;
|
||||
|
||||
beforeEach(() => {
|
||||
specDir = fs.mkdtempSync(path.join(os.tmpdir(), "up-route-import-rig-fail-"));
|
||||
|
||||
for (const name of ["builder", "reviewer"]) {
|
||||
const agentDir = path.join(specDir, "agents", name);
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(agentDir, "agent.yaml"),
|
||||
`name: ${name}\nversion: "1.0.0"\nresources:\n skills: []\nprofiles:\n default:\n uses:\n skills: []\n`,
|
||||
);
|
||||
}
|
||||
|
||||
db3 = createFullTestDb();
|
||||
const realRouterFsOps = {
|
||||
exists: (p: string) => fs.existsSync(p),
|
||||
readFile: (p: string) => fs.readFileSync(p, "utf-8"),
|
||||
readHead: (p: string, n: number) => {
|
||||
const buf = Buffer.alloc(n);
|
||||
const fd = fs.openSync(p, "r");
|
||||
try { fs.readSync(fd, buf, 0, n, 0); } finally { fs.closeSync(fd); }
|
||||
return buf;
|
||||
},
|
||||
};
|
||||
const realInstantiatorFsOps = {
|
||||
exists: (p: string) => fs.existsSync(p),
|
||||
readFile: (p: string) => fs.readFileSync(p, "utf-8"),
|
||||
};
|
||||
const setup3 = createTestApp(db3, {
|
||||
upRouterFsOps: realRouterFsOps,
|
||||
podInstantiatorFsOps: realInstantiatorFsOps,
|
||||
});
|
||||
app3 = setup3.app;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db3.close();
|
||||
fs.rmSync(specDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("apply-mode delegates_to cycle returns HTTP 400 with code=cycle_error at top level", async () => {
|
||||
const yaml = `version: "0.2"
|
||||
name: cycle-bug-rig
|
||||
pods:
|
||||
- id: build
|
||||
label: Build
|
||||
members:
|
||||
- id: builder
|
||||
agent_ref: local:agents/builder
|
||||
profile: default
|
||||
runtime: claude-code
|
||||
cwd: .
|
||||
edges: []
|
||||
- id: review
|
||||
label: Review
|
||||
members:
|
||||
- id: reviewer
|
||||
agent_ref: local:agents/reviewer
|
||||
profile: default
|
||||
runtime: claude-code
|
||||
cwd: .
|
||||
edges: []
|
||||
edges:
|
||||
- from: build.builder
|
||||
to: review.reviewer
|
||||
kind: delegates_to
|
||||
- from: review.reviewer
|
||||
to: build.builder
|
||||
kind: delegates_to
|
||||
`;
|
||||
const specPath = path.join(specDir, "cycle.yaml");
|
||||
fs.writeFileSync(specPath, yaml, "utf-8");
|
||||
|
||||
const res = await app3.request("/api/up", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sourceRef: specPath }),
|
||||
});
|
||||
|
||||
const bodyForDebug = await res.clone().json();
|
||||
expect(res.status, `expected 400 for import_rig cycle_error, got ${res.status} with body ${JSON.stringify(bodyForDebug)}`).toBe(400);
|
||||
const body = await res.json() as { code?: string; stages?: Array<{ stage: string; status: string; detail?: { code?: string } }> };
|
||||
|
||||
// The load-bearing fix: code surfaces at TOP LEVEL where CLI looks.
|
||||
expect(body.code, `expected top-level code=cycle_error, got body=${JSON.stringify(body)}`).toBe("cycle_error");
|
||||
|
||||
// Provenance discriminator — proves the code came from import_rig stage
|
||||
// (not a resolve_spec misclassification that the existing 400-mapping
|
||||
// path was already handling).
|
||||
const importRigStage = (body.stages ?? []).find((s) => s.stage === "import_rig");
|
||||
expect(importRigStage, `expected import_rig stage in body.stages, got ${JSON.stringify(body.stages)}`).toBeDefined();
|
||||
expect(importRigStage!.status).toBe("failed");
|
||||
expect(importRigStage!.detail?.code).toBe("cycle_error");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user