fix(ci): preserve routing failure diagnostics

This commit is contained in:
Vincent Koc
2026-07-31 05:39:50 -07:00
parent 3cf0de893e
commit 89e4ef2ab7
2 changed files with 80 additions and 9 deletions
+59 -8
View File
@@ -431,6 +431,21 @@ function checkedOutput(
};
}
function recoveryCommand(commandArgs) {
return [binary, ...commandArgs].map(recoveryCommandArgument).join(" ");
}
function recoveryCommandArgument(value) {
const text = `${value}`;
if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(text)) {
return text;
}
if (process.platform === "win32") {
return `"${text.replaceAll('"', '""')}"`;
}
return `'${text.replaceAll("'", "'\\''")}'`;
}
// Probe Crabbox metadata (`--version` / `run --help`) with one generous retry.
// A cold Crabbox can be SIGKILLed by the snappy default timeout or emit nothing
// on the first call, then be instant and clean on the next. Retrying keeps the
@@ -898,6 +913,7 @@ function crabboxProviderReadiness(providerName, versionText, targetContext) {
return {
ready: false,
reason: `requires Crabbox >= ${formatVersionTuple(minimumBlacksmithCrabboxVersion)} for Blacksmith Testbox`,
recovery: "update Crabbox, then retry",
};
}
if (
@@ -907,10 +923,15 @@ function crabboxProviderReadiness(providerName, versionText, targetContext) {
return {
ready: false,
reason: `requires Crabbox >= ${formatVersionTuple(minimumBrokeredDaytonaCrabboxVersion)} for brokered Daytona`,
recovery: "update Crabbox, then retry",
};
}
if (["aws", "azure", "daytona"].includes(canonicalProvider) && !managedBrokerAuthConfigured()) {
return { ready: false, reason: "managed Crabbox broker auth unavailable" };
return {
ready: false,
reason: "managed Crabbox broker auth unavailable",
recovery: `run \`${recoveryCommand(["login", "--url", "https://crabbox.openclaw.ai"])}\`, then retry`,
};
}
const doctorArgs = ["doctor", "--provider", canonicalProvider];
if (targetContext.target) {
@@ -922,17 +943,40 @@ function crabboxProviderReadiness(providerName, versionText, targetContext) {
doctorArgs.push("--json");
const doctor = checkedOutput(binary, doctorArgs);
if (doctor.status !== 0) {
return { ready: false, reason: `doctor exited ${doctor.status}` };
const diagnostic = compactDiagnosticText(doctor.text);
return {
ready: false,
reason: `doctor exited ${doctor.status}${diagnostic ? `: ${diagnostic}` : ""}`,
recovery: `run \`${recoveryCommand(doctorArgs)}\``,
};
}
return { ready: true, reason: "doctor-ready" };
}
function compactDiagnosticText(value, maxLength = 500) {
const compact = `${value ?? ""}`.replace(/\s+/gu, " ").trim();
if (compact.length <= maxLength) {
return compact;
}
return `${compact.slice(0, Math.max(0, maxLength - 3))}...`;
}
function formatProviderReadiness(readiness) {
return [...readiness.entries()]
.map(([candidate, status]) => `${candidate}:${status.ready ? "ready" : status.reason}`)
.join(",");
}
function providerRecoveryAdvice(readiness) {
return [
...new Set(
[...readiness.values()]
.map((status) => status.recovery)
.filter((recovery) => typeof recovery === "string" && recovery.length > 0),
),
];
}
function shouldRequireBrokeredCloud(commandArgs, providerName, explicitProviderRequested = false) {
const canonicalProvider = canonicalProviderName(providerName);
if (!["aws", "azure", "daytona"].includes(canonicalProvider)) {
@@ -1012,7 +1056,7 @@ function enforceBrokeredCloud(commandArgs, providerName, explicitProviderRequest
const canonicalProvider = canonicalProviderName(providerName);
const instructions = [
`[crabbox] provider=${canonicalProvider} requires a configured managed Crabbox broker for OpenClaw proof.`,
"[crabbox] run `crabbox login --url https://crabbox.openclaw.ai`, then retry.",
`[crabbox] run \`${recoveryCommand(["login", "--url", "https://crabbox.openclaw.ai"])}\`, then retry.`,
];
if (canonicalProvider !== "aws") {
instructions.push(
@@ -3783,6 +3827,15 @@ const version = probeCrabboxMetadata(binary, ["--version"]);
const help = probeCrabboxMetadata(binary, ["run", "--help"]);
const providers = parseProvidersFromHelp(help.text);
const displayBinary = binary === "crabbox" ? "crabbox" : relative(repoRoot, binary);
if (version.status !== 0 || help.status !== 0) {
console.error(
`[crabbox] bin=${displayBinary} version=${version.text || "unknown"} providers=${providers.join(",") || "unknown"}`,
);
console.error("[crabbox] selected binary failed basic --version/--help sanity checks");
process.exit(2);
}
const providerSelection = selectedProvider(args, providers, version.text);
if (providerSelection.error) {
console.error(`[crabbox] ${providerSelection.error}`);
@@ -3790,6 +3843,9 @@ if (providerSelection.error) {
console.error(
`[crabbox] provider readiness ${formatProviderReadiness(providerSelection.readiness)}`,
);
for (const recovery of providerRecoveryAdvice(providerSelection.readiness)) {
console.error(`[crabbox] recovery: ${recovery}`);
}
}
process.exit(2);
}
@@ -3813,11 +3869,6 @@ if (providerSelection.source === "policy") {
);
}
if (version.status !== 0 || help.status !== 0) {
console.error("[crabbox] selected binary failed basic --version/--help sanity checks");
process.exit(2);
}
if (provider && !isProviderAdvertised(provider, providers)) {
if (providers.length === 0) {
console.error(
+21 -1
View File
@@ -1437,6 +1437,10 @@ describe("scripts/crabbox-wrapper", () => {
expect(result.stdout).toBe("");
expect(result.stderr).toContain("no ready provider for workload=ci-fast");
expect(result.stderr).toContain("provider readiness");
expect(result.stderr).toContain('{"ok":false,"provider":"blacksmith-testbox"}');
expect(result.stderr).toMatch(
/recovery: run `\S+crabbox doctor --provider blacksmith-testbox --json`/u,
);
});
it("rejects unknown workload policies before execution", () => {
@@ -2221,7 +2225,7 @@ describe("scripts/crabbox-wrapper", () => {
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("provider=aws requires a configured managed Crabbox broker");
expect(result.stderr).toContain("crabbox login --url https://crabbox.openclaw.ai");
expect(result.stderr).toContain("login --url https://crabbox.openclaw.ai");
expect(result.stderr).not.toContain("--provider aws");
expect(result.stderr).not.toContain("OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD");
});
@@ -3788,6 +3792,22 @@ describe("scripts/crabbox-wrapper", () => {
expect(result.stderr).toContain("selected binary failed basic --version/--help sanity checks");
});
it("rejects a broken binary before workload provider discovery", () => {
const helpText =
"provider: hetzner, aws, local-container, blacksmith-testbox, daytona, azure, or cloudflare\n";
const result = runWrapper(helpText, ["run", "--workload", "ci-fast", "--", "echo ok"], {
env: { OPENCLAW_TEST_CRABBOX_METADATA_PROBE_TIMEOUT_MS: "100" },
extraPathEntries: [makeSlowVersionCrabbox(helpText)],
nodePreload: testTimingPreload({ spawnTimeoutMs: 25 }),
});
expect(result.error).toBeUndefined();
expect(result.status).toBe(2);
expect(result.stderr).toContain("selected binary failed basic --version/--help sanity checks");
expect(result.stderr).not.toContain("no ready provider");
expect(result.stderr).not.toContain("provider readiness");
});
it("retries a cold Crabbox whose run --help is slower than the default probe timeout", () => {
const helpText = "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n";
// First probe is SIGKILLed at 25ms; the retry gets the full generous timeout