mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:01:34 +00:00
feat(ci): add workload-aware Crabbox routing
This commit is contained in:
@@ -345,6 +345,7 @@
|
||||
"!docs/**/*.jpg",
|
||||
"!docs/**/*.png",
|
||||
"src/agents/templates/",
|
||||
"scripts/crabbox-routing-policy.mjs",
|
||||
"scripts/crabbox-wrapper-providers.mjs",
|
||||
"scripts/crabbox-wrapper.mjs",
|
||||
"scripts/testbox-lease-freshness.mjs",
|
||||
|
||||
70
scripts/crabbox-routing-policy.mjs
Normal file
70
scripts/crabbox-routing-policy.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
const workloadAliases = new Map([
|
||||
["check", "ci-fast"],
|
||||
["ci", "ci-fast"],
|
||||
["ci-fast", "ci-fast"],
|
||||
["ci-proof", "ci-proof"],
|
||||
["desktop", "desktop"],
|
||||
["interactive", "interactive"],
|
||||
["release", "release-proof"],
|
||||
["release-proof", "release-proof"],
|
||||
["untrusted", "untrusted"],
|
||||
["windows", "windows"],
|
||||
]);
|
||||
|
||||
export function normalizeCrabboxWorkload(value) {
|
||||
const normalized = `${value ?? ""}`.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return workloadAliases.get(normalized) ?? null;
|
||||
}
|
||||
|
||||
export function crabboxProviderChain({
|
||||
workload,
|
||||
configuredProvider,
|
||||
target,
|
||||
advertisedProviders,
|
||||
}) {
|
||||
const providers = new Set(advertisedProviders);
|
||||
const normalizedConfigured = `${configuredProvider ?? ""}`.trim();
|
||||
const normalizedTarget = `${target ?? ""}`.trim().toLowerCase();
|
||||
|
||||
if (normalizedTarget === "macos") {
|
||||
return available(["aws"], providers);
|
||||
}
|
||||
if (normalizedTarget === "windows" || workload === "windows") {
|
||||
return available(["azure", "aws"], providers);
|
||||
}
|
||||
|
||||
const cloudFallback = ["azure", "aws"];
|
||||
switch (workload) {
|
||||
case "ci-fast":
|
||||
return available(["blacksmith-testbox", "daytona", ...cloudFallback], providers);
|
||||
case "ci-proof":
|
||||
case "release-proof":
|
||||
return available(["blacksmith-testbox", "daytona", ...cloudFallback], providers);
|
||||
case "interactive":
|
||||
return available(["daytona", ...cloudFallback], providers);
|
||||
case "desktop":
|
||||
return available(cloudFallback, providers);
|
||||
case "untrusted":
|
||||
// Daytona remains excluded until its brokered isolation profile has live proof.
|
||||
return available(cloudFallback, providers);
|
||||
default:
|
||||
return available([normalizedConfigured], providers);
|
||||
}
|
||||
}
|
||||
|
||||
export function selectReadyCrabboxProvider(chain, readiness) {
|
||||
for (const provider of chain) {
|
||||
const status = readiness.get(provider);
|
||||
if (status?.ready) {
|
||||
return { provider, readiness: status };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function available(candidates, advertisedProviders) {
|
||||
return candidates.filter((provider) => provider && advertisedProviders.has(provider));
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { homedir, tmpdir } from "node:os";
|
||||
import { delimiter, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { crabboxProviderChain, normalizeCrabboxWorkload } from "./crabbox-routing-policy.mjs";
|
||||
import {
|
||||
canonicalProviderName,
|
||||
isProviderAdvertised,
|
||||
@@ -54,11 +55,45 @@ const args = process.argv.slice(2);
|
||||
if (args[0] === "--") {
|
||||
args.shift();
|
||||
}
|
||||
const workloadOption = isWorkloadRoutedCommand(args)
|
||||
? extractWrapperValueOption(args, "--workload")
|
||||
: undefined;
|
||||
const userArgStart = args[0] === "actions" && args[1] === "hydrate" ? 2 : 1;
|
||||
if (args[userArgStart] === "--") {
|
||||
args.splice(userArgStart, 1);
|
||||
}
|
||||
|
||||
function extractWrapperValueOption(commandArgs, name) {
|
||||
const equalsPrefix = `${name}=`;
|
||||
for (let index = 0; index < commandArgs.length; index += 1) {
|
||||
const arg = commandArgs[index];
|
||||
if (arg === "--") {
|
||||
break;
|
||||
}
|
||||
if (arg === name) {
|
||||
const value = commandArgs[index + 1];
|
||||
if (!value || value === "--" || value.startsWith("-")) {
|
||||
commandArgs.splice(index, 1);
|
||||
return null;
|
||||
}
|
||||
commandArgs.splice(index, 2);
|
||||
return value;
|
||||
}
|
||||
if (arg.startsWith(equalsPrefix)) {
|
||||
commandArgs.splice(index, 1);
|
||||
return arg.slice(equalsPrefix.length) || null;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isWorkloadRoutedCommand(commandArgs) {
|
||||
return (
|
||||
["run", "warmup"].includes(commandArgs[0]) ||
|
||||
(commandArgs[0] === "actions" && commandArgs[1] === "hydrate")
|
||||
);
|
||||
}
|
||||
|
||||
function commandCandidates(command, platform) {
|
||||
if (platform !== "win32") {
|
||||
return [command];
|
||||
@@ -232,6 +267,7 @@ const awsMacosPackageManagerScriptTargets = new Set([
|
||||
"scripts/restart-mac.sh",
|
||||
]);
|
||||
const minimumBlacksmithCrabboxVersion = [0, 22, 0];
|
||||
const minimumBrokeredDaytonaCrabboxVersion = [0, 40, 0];
|
||||
const shellControlCommandPrefixes = new Set([
|
||||
"if",
|
||||
"while",
|
||||
@@ -479,6 +515,27 @@ function gitOutput(commandArgs) {
|
||||
};
|
||||
}
|
||||
|
||||
let resolvedCrabboxConfigCache;
|
||||
|
||||
function resolvedCrabboxConfig() {
|
||||
if (resolvedCrabboxConfigCache !== undefined) {
|
||||
return resolvedCrabboxConfigCache;
|
||||
}
|
||||
const result = checkedOutput(binary, ["config", "show", "--json"]);
|
||||
if (result.status !== 0) {
|
||||
resolvedCrabboxConfigCache = null;
|
||||
return resolvedCrabboxConfigCache;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(result.stdout || result.text);
|
||||
resolvedCrabboxConfigCache =
|
||||
parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
||||
} catch {
|
||||
resolvedCrabboxConfigCache = null;
|
||||
}
|
||||
return resolvedCrabboxConfigCache;
|
||||
}
|
||||
|
||||
function envProvider() {
|
||||
const envProviderValue = process.env.CRABBOX_PROVIDER?.trim();
|
||||
if (envProviderValue) {
|
||||
@@ -488,6 +545,10 @@ function envProvider() {
|
||||
}
|
||||
|
||||
function configProvider() {
|
||||
const resolved = resolvedCrabboxConfig()?.provider;
|
||||
if (typeof resolved === "string" && resolved.trim()) {
|
||||
return resolved.trim();
|
||||
}
|
||||
try {
|
||||
const config = readFileSync(resolve(repoRoot, ".crabbox.yaml"), "utf8");
|
||||
const match = config.match(/^provider:\s*([^\s#]+)/m);
|
||||
@@ -501,6 +562,26 @@ function configuredProvider() {
|
||||
return envProvider() || configProvider();
|
||||
}
|
||||
|
||||
function effectiveTargetContext(commandArgs) {
|
||||
const config = resolvedCrabboxConfig();
|
||||
const configuredTarget = typeof config?.target === "string" ? config.target.trim() : "";
|
||||
const configuredWindowsMode =
|
||||
typeof config?.windowsMode === "string" ? config.windowsMode.trim() : "";
|
||||
return {
|
||||
target: (
|
||||
optionValue(commandArgs, "--target") ||
|
||||
process.env.CRABBOX_TARGET?.trim() ||
|
||||
process.env.CRABBOX_TARGET_OS?.trim() ||
|
||||
configuredTarget
|
||||
).toLowerCase(),
|
||||
windowsMode: (
|
||||
optionValue(commandArgs, "--windows-mode") ||
|
||||
process.env.CRABBOX_WINDOWS_MODE?.trim() ||
|
||||
configuredWindowsMode
|
||||
).toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
const runValueOptions = new Set([
|
||||
"allow-env",
|
||||
"artifact-glob",
|
||||
@@ -682,23 +763,165 @@ function commandProvider(commandArgsInput) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function selectedProvider(commandArgs, advertisedProviders = []) {
|
||||
function selectedProvider(commandArgs, advertisedProviders = [], versionText = "") {
|
||||
const targetContext = effectiveTargetContext(commandArgs);
|
||||
if (workloadOption === null) {
|
||||
return {
|
||||
provider: "",
|
||||
source: "policy",
|
||||
workload: "",
|
||||
chain: [],
|
||||
error: "--workload requires a value",
|
||||
};
|
||||
}
|
||||
const workload = requestedWorkload(commandArgs);
|
||||
if (workload === null) {
|
||||
return {
|
||||
provider: "",
|
||||
source: "policy",
|
||||
workload: workloadOption ?? process.env.OPENCLAW_CRABBOX_WORKLOAD ?? "",
|
||||
chain: [],
|
||||
error: `unsupported Crabbox workload ${JSON.stringify(workloadOption ?? process.env.OPENCLAW_CRABBOX_WORKLOAD)}`,
|
||||
};
|
||||
}
|
||||
if (workload === "windows" && targetContext.target !== "windows") {
|
||||
return {
|
||||
provider: "",
|
||||
source: "policy",
|
||||
workload,
|
||||
chain: [],
|
||||
error: "Crabbox workload=windows requires target=windows",
|
||||
};
|
||||
}
|
||||
const explicitProvider = commandProvider(commandArgs);
|
||||
if (explicitProvider) {
|
||||
return explicitProvider;
|
||||
return { provider: explicitProvider, source: "explicit", workload: "", chain: [] };
|
||||
}
|
||||
if (shouldPreferAzureForWindows(commandArgs, advertisedProviders)) {
|
||||
return "azure";
|
||||
const environmentProvider = envProvider();
|
||||
if (environmentProvider) {
|
||||
return { provider: environmentProvider, source: "environment", workload: "", chain: [] };
|
||||
}
|
||||
return configuredProvider();
|
||||
if (workload && hasOption(commandArgs, "--id")) {
|
||||
return {
|
||||
provider: "",
|
||||
source: "policy",
|
||||
workload,
|
||||
chain: [],
|
||||
error:
|
||||
"reusing a workload-routed lease with --id requires --provider (or CRABBOX_PROVIDER) from the originating route",
|
||||
};
|
||||
}
|
||||
const configured = canonicalProviderName(configProvider());
|
||||
if (!workload && shouldPreferAzureForWindows(commandArgs, advertisedProviders)) {
|
||||
return { provider: "azure", source: "windows-default", workload: "", chain: [] };
|
||||
}
|
||||
if (!workload) {
|
||||
return { provider: configured, source: "config", workload: "", chain: [] };
|
||||
}
|
||||
|
||||
const chain = crabboxProviderChain({
|
||||
workload,
|
||||
configuredProvider: configured,
|
||||
target: targetContext.target,
|
||||
advertisedProviders: advertisedProviders.map(canonicalProviderName),
|
||||
});
|
||||
const readiness = new Map();
|
||||
let selectedProviderName = "";
|
||||
for (const candidate of chain) {
|
||||
const status = crabboxProviderReadiness(candidate, versionText, targetContext);
|
||||
readiness.set(candidate, status);
|
||||
if (status.ready) {
|
||||
selectedProviderName = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!selectedProviderName) {
|
||||
return {
|
||||
provider: "",
|
||||
source: "policy",
|
||||
workload,
|
||||
chain,
|
||||
readiness,
|
||||
error: `no ready provider for workload=${workload}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
provider: selectedProviderName,
|
||||
source: "policy",
|
||||
workload,
|
||||
chain,
|
||||
readiness,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRequireBrokeredAws(commandArgs, providerName) {
|
||||
if (process.env.OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS === "1") {
|
||||
function requestedWorkload(commandArgs) {
|
||||
if (!isWorkloadRoutedCommand(commandArgs)) {
|
||||
return "";
|
||||
}
|
||||
const raw = workloadOption ?? process.env.OPENCLAW_CRABBOX_WORKLOAD?.trim() ?? "";
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
return normalizeCrabboxWorkload(raw);
|
||||
}
|
||||
|
||||
let managedBrokerAuthConfiguredCache;
|
||||
|
||||
function crabboxProviderReadiness(providerName, versionText, targetContext) {
|
||||
const canonicalProvider = canonicalProviderName(providerName);
|
||||
if (
|
||||
canonicalProvider === "blacksmith-testbox" &&
|
||||
!satisfiesMinimumCrabboxVersion(versionText, minimumBlacksmithCrabboxVersion)
|
||||
) {
|
||||
return {
|
||||
ready: false,
|
||||
reason: `requires Crabbox >= ${formatVersionTuple(minimumBlacksmithCrabboxVersion)} for Blacksmith Testbox`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
canonicalProvider === "daytona" &&
|
||||
!directCloudOverrideEnabled(canonicalProvider) &&
|
||||
!satisfiesMinimumCrabboxVersion(versionText, minimumBrokeredDaytonaCrabboxVersion)
|
||||
) {
|
||||
return {
|
||||
ready: false,
|
||||
reason: `requires Crabbox >= ${formatVersionTuple(minimumBrokeredDaytonaCrabboxVersion)} for brokered Daytona`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
["aws", "azure", "daytona"].includes(canonicalProvider) &&
|
||||
!directCloudOverrideEnabled(canonicalProvider) &&
|
||||
!managedBrokerAuthConfigured()
|
||||
) {
|
||||
return { ready: false, reason: "managed Crabbox broker auth unavailable" };
|
||||
}
|
||||
const doctorArgs = ["doctor", "--provider", canonicalProvider];
|
||||
if (targetContext.target) {
|
||||
doctorArgs.push("--target", targetContext.target);
|
||||
}
|
||||
if (targetContext.target === "windows" && targetContext.windowsMode) {
|
||||
doctorArgs.push("--windows-mode", targetContext.windowsMode);
|
||||
}
|
||||
doctorArgs.push("--json");
|
||||
const doctor = checkedOutput(binary, doctorArgs);
|
||||
if (doctor.status !== 0) {
|
||||
return { ready: false, reason: `doctor exited ${doctor.status}` };
|
||||
}
|
||||
return { ready: true, reason: "doctor-ready" };
|
||||
}
|
||||
|
||||
function formatProviderReadiness(readiness) {
|
||||
return [...readiness.entries()]
|
||||
.map(([candidate, status]) => `${candidate}:${status.ready ? "ready" : status.reason}`)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function shouldRequireBrokeredCloud(commandArgs, providerName) {
|
||||
if (directCloudOverrideEnabled(providerName)) {
|
||||
return false;
|
||||
}
|
||||
const canonicalProvider = canonicalProviderName(providerName);
|
||||
if (canonicalProvider !== "aws") {
|
||||
if (!["aws", "azure", "daytona"].includes(canonicalProvider)) {
|
||||
return false;
|
||||
}
|
||||
if (commandArgs[0] === "run" || commandArgs[0] === "warmup") {
|
||||
@@ -707,32 +930,59 @@ function shouldRequireBrokeredAws(commandArgs, providerName) {
|
||||
return commandArgs[0] === "actions" && commandArgs[1] === "hydrate";
|
||||
}
|
||||
|
||||
function brokerAuthConfigured() {
|
||||
const config = checkedOutput(binary, ["config", "show", "--json"]);
|
||||
if (config.status !== 0) {
|
||||
return false;
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(config.stdout || config.text);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!parsed?.coordinator || parsed?.brokerAuth !== "configured") {
|
||||
return false;
|
||||
}
|
||||
return checkedOutput(binary, ["whoami"]).status === 0;
|
||||
function directCloudOverrideEnabled(providerName) {
|
||||
return (
|
||||
process.env.OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD === "1" ||
|
||||
(canonicalProviderName(providerName) === "aws" &&
|
||||
process.env.OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS === "1")
|
||||
);
|
||||
}
|
||||
|
||||
function enforceBrokeredAws(commandArgs, providerName) {
|
||||
if (!shouldRequireBrokeredAws(commandArgs, providerName) || brokerAuthConfigured()) {
|
||||
function managedBrokerAuthConfigured() {
|
||||
if (managedBrokerAuthConfiguredCache !== undefined) {
|
||||
return managedBrokerAuthConfiguredCache;
|
||||
}
|
||||
const parsed = resolvedCrabboxConfig();
|
||||
if (
|
||||
!parsed?.coordinator ||
|
||||
parsed?.brokerMode !== "managed" ||
|
||||
parsed?.brokerAuth !== "configured"
|
||||
) {
|
||||
managedBrokerAuthConfiguredCache = false;
|
||||
return managedBrokerAuthConfiguredCache;
|
||||
}
|
||||
managedBrokerAuthConfiguredCache = checkedOutput(binary, ["whoami"]).status === 0;
|
||||
return managedBrokerAuthConfiguredCache;
|
||||
}
|
||||
|
||||
function enforceBrokeredDaytonaVersion(commandArgs, providerName, versionText) {
|
||||
if (
|
||||
canonicalProviderName(providerName) !== "daytona" ||
|
||||
!shouldRequireBrokeredCloud(commandArgs, providerName) ||
|
||||
satisfiesMinimumCrabboxVersion(versionText, minimumBrokeredDaytonaCrabboxVersion)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
[
|
||||
"[crabbox] provider=aws requires a configured Crabbox broker for OpenClaw proof.",
|
||||
"[crabbox] run `crabbox login --url https://crabbox.openclaw.ai --provider aws`, then retry.",
|
||||
"[crabbox] for intentional direct AWS provider debugging, set OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS=1.",
|
||||
`[crabbox] provider=daytona requires Crabbox >= ${formatVersionTuple(minimumBrokeredDaytonaCrabboxVersion)} for brokered execution.`,
|
||||
`[crabbox] selected binary reported version=${versionText || "unknown"}.`,
|
||||
"[crabbox] update Crabbox, or set OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD=1 only for intentional direct-provider debugging.",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function enforceBrokeredCloud(commandArgs, providerName) {
|
||||
if (!shouldRequireBrokeredCloud(commandArgs, providerName) || managedBrokerAuthConfigured()) {
|
||||
return;
|
||||
}
|
||||
const canonicalProvider = canonicalProviderName(providerName);
|
||||
console.error(
|
||||
[
|
||||
`[crabbox] provider=${canonicalProvider} requires a configured managed Crabbox broker for OpenClaw proof.`,
|
||||
"[crabbox] run `crabbox login --url https://crabbox.openclaw.ai`, then retry.",
|
||||
"[crabbox] for intentional direct cloud debugging, set OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD=1.",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(2);
|
||||
@@ -800,6 +1050,21 @@ function ensureAzureWindowsProvider(commandArgs, providerName, advertisedProvide
|
||||
return normalizedArgs;
|
||||
}
|
||||
|
||||
function ensurePolicyProvider(commandArgs, selection) {
|
||||
if (
|
||||
selection.source !== "policy" ||
|
||||
!selection.provider ||
|
||||
commandProvider(commandArgs) ||
|
||||
envProvider()
|
||||
) {
|
||||
return commandArgs;
|
||||
}
|
||||
const normalizedArgs = [...commandArgs];
|
||||
const optionEnd = commandOptionEnd(normalizedArgs);
|
||||
normalizedArgs.splice(optionEnd, 0, "--provider", selection.provider);
|
||||
return normalizedArgs;
|
||||
}
|
||||
|
||||
function ensureAwsMacOnDemandMarket(commandArgs, providerName) {
|
||||
if (
|
||||
!["run", "warmup"].includes(commandArgs[0]) ||
|
||||
@@ -3483,17 +3748,35 @@ const version = probeCrabboxMetadata(binary, ["--version"]);
|
||||
const help = probeCrabboxMetadata(binary, ["run", "--help"]);
|
||||
const providers = parseProvidersFromHelp(help.text);
|
||||
const displayBinary = binary === "crabbox" ? "crabbox" : relative(repoRoot, binary);
|
||||
const provider = selectedProvider(args, providers);
|
||||
const providerSelection = selectedProvider(args, providers, version.text);
|
||||
if (providerSelection.error) {
|
||||
console.error(`[crabbox] ${providerSelection.error}`);
|
||||
if (providerSelection.readiness) {
|
||||
console.error(
|
||||
`[crabbox] provider readiness ${formatProviderReadiness(providerSelection.readiness)}`,
|
||||
);
|
||||
}
|
||||
process.exit(2);
|
||||
}
|
||||
const provider = providerSelection.provider;
|
||||
const canonicalProvider = canonicalProviderName(provider);
|
||||
const commandProviderValue = commandProvider(args);
|
||||
let normalizedArgs = ensureAwsMacOnDemandMarket(
|
||||
ensureNativeWindowsHydrateJob(ensureAzureWindowsProvider(args, provider, providers)),
|
||||
ensurePolicyProvider(
|
||||
ensureNativeWindowsHydrateJob(ensureAzureWindowsProvider(args, provider, providers)),
|
||||
providerSelection,
|
||||
),
|
||||
provider,
|
||||
);
|
||||
|
||||
console.error(
|
||||
`[crabbox] bin=${displayBinary} version=${version.text || "unknown"} provider=${provider || "unknown"} providers=${providers.join(",") || "unknown"}`,
|
||||
);
|
||||
if (providerSelection.source === "policy") {
|
||||
console.error(
|
||||
`[crabbox] route workload=${providerSelection.workload} selected=${provider} chain=${providerSelection.chain.join(",")} readiness=${formatProviderReadiness(providerSelection.readiness)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (version.status !== 0 || help.status !== 0) {
|
||||
console.error("[crabbox] selected binary failed basic --version/--help sanity checks");
|
||||
@@ -3536,7 +3819,8 @@ if (canonicalProvider === "blacksmith-testbox") {
|
||||
}
|
||||
}
|
||||
|
||||
enforceBrokeredAws(normalizedArgs, provider);
|
||||
enforceBrokeredDaytonaVersion(normalizedArgs, provider, version.text);
|
||||
enforceBrokeredCloud(normalizedArgs, provider);
|
||||
|
||||
if (canonicalProvider === "blacksmith-testbox") {
|
||||
const envProviderLocal = process.env.CRABBOX_PROVIDER?.trim();
|
||||
|
||||
94
test/scripts/crabbox-routing-policy.test.ts
Normal file
94
test/scripts/crabbox-routing-policy.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
crabboxProviderChain,
|
||||
normalizeCrabboxWorkload,
|
||||
selectReadyCrabboxProvider,
|
||||
} from "../../scripts/crabbox-routing-policy.mjs";
|
||||
|
||||
const advertisedProviders = ["aws", "azure", "blacksmith-testbox", "daytona"];
|
||||
|
||||
describe("Crabbox routing policy", () => {
|
||||
it("normalizes supported workload aliases", () => {
|
||||
expect(normalizeCrabboxWorkload("check")).toBe("ci-fast");
|
||||
expect(normalizeCrabboxWorkload("release")).toBe("release-proof");
|
||||
expect(normalizeCrabboxWorkload("unknown")).toBeNull();
|
||||
});
|
||||
|
||||
it("routes CI checks through Blacksmith, Daytona, Azure, then AWS", () => {
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "ci-fast",
|
||||
configuredProvider: "blacksmith-testbox",
|
||||
target: "linux",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["blacksmith-testbox", "daytona", "azure", "aws"]);
|
||||
});
|
||||
|
||||
it("does not let persistent cloud config outrank the CI policy", () => {
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "ci-proof",
|
||||
configuredProvider: "aws",
|
||||
target: "linux",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["blacksmith-testbox", "daytona", "azure", "aws"]);
|
||||
});
|
||||
|
||||
it("prefers Daytona for interactive Linux work", () => {
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "interactive",
|
||||
configuredProvider: "blacksmith-testbox",
|
||||
target: "linux",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["daytona", "azure", "aws"]);
|
||||
});
|
||||
|
||||
it("keeps untrusted work off Blacksmith and Daytona pending isolation proof", () => {
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "untrusted",
|
||||
configuredProvider: "blacksmith-testbox",
|
||||
target: "linux",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["azure", "aws"]);
|
||||
});
|
||||
|
||||
it("uses Azure then AWS for Windows and only AWS for macOS", () => {
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "ci-fast",
|
||||
configuredProvider: "blacksmith-testbox",
|
||||
target: "windows",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["azure", "aws"]);
|
||||
expect(
|
||||
crabboxProviderChain({
|
||||
workload: "ci-fast",
|
||||
configuredProvider: "blacksmith-testbox",
|
||||
target: "macos",
|
||||
advertisedProviders,
|
||||
}),
|
||||
).toEqual(["aws"]);
|
||||
});
|
||||
|
||||
it("selects the first ready provider without racing providers", () => {
|
||||
const readiness = new Map([
|
||||
["blacksmith-testbox", { ready: false, reason: "queued" }],
|
||||
["daytona", { ready: true, reason: "broker-ready" }],
|
||||
["azure", { ready: true, reason: "broker-ready" }],
|
||||
]);
|
||||
|
||||
expect(
|
||||
selectReadyCrabboxProvider(["blacksmith-testbox", "daytona", "azure"], readiness),
|
||||
).toEqual({
|
||||
provider: "daytona",
|
||||
readiness: { ready: true, reason: "broker-ready" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -93,6 +93,36 @@ function writeFakeCrabbox(binDir: string, helpText: string): string {
|
||||
` printf "%s" ${shellSingleQuote(helpText)}`,
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [ "$1" = "doctor" ]; then',
|
||||
' provider=""',
|
||||
' target=""',
|
||||
' windows_mode=""',
|
||||
' previous_arg=""',
|
||||
' for arg in "$@"; do',
|
||||
' if [ "$previous_arg" = "--provider" ] || [ "$previous_arg" = "-provider" ]; then provider="$arg"; fi',
|
||||
' if [ "$previous_arg" = "--target" ] || [ "$previous_arg" = "-target" ]; then target="$arg"; fi',
|
||||
' if [ "$previous_arg" = "--windows-mode" ] || [ "$previous_arg" = "-windows-mode" ]; then windows_mode="$arg"; fi',
|
||||
' case "$arg" in',
|
||||
' --provider=*|-provider=*) provider="${arg#*=}" ;;',
|
||||
' --target=*|-target=*) target="${arg#*=}" ;;',
|
||||
' --windows-mode=*|-windows-mode=*) windows_mode="${arg#*=}" ;;',
|
||||
" esac",
|
||||
' previous_arg="$arg"',
|
||||
" done",
|
||||
' if [ -n "${OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET:-}" ] && [ "$target" != "$OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET" ]; then',
|
||||
' printf "%s\\n" "doctor target mismatch: got=$target" >&2',
|
||||
" exit 64",
|
||||
" fi",
|
||||
' if [ -n "${OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_WINDOWS_MODE:-}" ] && [ "$windows_mode" != "$OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_WINDOWS_MODE" ]; then',
|
||||
' printf "%s\\n" "doctor windows mode mismatch: got=$windows_mode" >&2',
|
||||
" exit 64",
|
||||
" fi",
|
||||
' case ",${OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS:-}," in',
|
||||
' *,"$provider",*) printf "%s\\n" "{\\"ok\\":false,\\"provider\\":\\"$provider\\"}" >&2; exit 1 ;;',
|
||||
" esac",
|
||||
' printf "%s\\n" "{\\"ok\\":true,\\"provider\\":\\"$provider\\"}"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if { [ "$1" = "run" ] || [ "$1" = "warmup" ]; } && { [ -n "${OPENCLAW_FAKE_CRABBOX_CLAIM_PATH:-}" ] || [ -n "${OPENCLAW_FAKE_CRABBOX_TIMING_LEASE_ID:-}" ]; }; then',
|
||||
` ${shellSingleQuote(process.execPath)} --eval ${shellSingleQuote(stampClaimScript)}`,
|
||||
"fi",
|
||||
@@ -111,7 +141,7 @@ function writeFakeCrabbox(binDir: string, helpText: string): string {
|
||||
' if [ -n "${OPENCLAW_FAKE_CRABBOX_CONFIG_JSON+x}" ]; then',
|
||||
' printf "%s" "$OPENCLAW_FAKE_CRABBOX_CONFIG_JSON"',
|
||||
" else",
|
||||
' printf "%s" "{\\"coordinator\\":\\"configured-broker\\",\\"brokerAuth\\":\\"configured\\"}"',
|
||||
' printf "%s" "{\\"coordinator\\":\\"configured-broker\\",\\"brokerMode\\":\\"managed\\",\\"brokerAuth\\":\\"configured\\"}"',
|
||||
" fi",
|
||||
" exit 0",
|
||||
" fi",
|
||||
@@ -220,7 +250,7 @@ function writeFakeCrabbox(binDir: string, helpText: string): string {
|
||||
" process.stderr.write('config unavailable\\n');",
|
||||
" process.exit(status);",
|
||||
" }",
|
||||
' process.stdout.write(process.env.OPENCLAW_FAKE_CRABBOX_CONFIG_JSON || \'{"coordinator":"configured-broker","brokerAuth":"configured"}\');',
|
||||
' process.stdout.write(process.env.OPENCLAW_FAKE_CRABBOX_CONFIG_JSON || \'{"coordinator":"configured-broker","brokerMode":"managed","brokerAuth":"configured"}\');',
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
'if (args[0] === "whoami") {',
|
||||
@@ -265,6 +295,20 @@ function writeFakeCrabbox(binDir: string, helpText: string): string {
|
||||
` process.stdout.write(${JSON.stringify(helpText)});`,
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
'if (args[0] === "doctor") {',
|
||||
" const optionValue = (name) => { const index = args.findIndex((arg) => arg === `--${name}` || arg === `-${name}`); const assigned = args.find((arg) => arg.startsWith(`--${name}=`) || arg.startsWith(`-${name}=`)); return index >= 0 ? args[index + 1] || '' : assigned?.slice(assigned.indexOf('=') + 1) || ''; };",
|
||||
" const providerIndex = args.findIndex((arg) => arg === '--provider' || arg === '-provider');",
|
||||
" const providerArg = args.find((arg) => arg.startsWith('--provider=') || arg.startsWith('-provider='));",
|
||||
" const provider = providerIndex >= 0 ? args[providerIndex + 1] : providerArg?.slice(providerArg.indexOf('=') + 1) || '';",
|
||||
" const target = optionValue('target');",
|
||||
" const windowsMode = optionValue('windows-mode');",
|
||||
" if (process.env.OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET && target !== process.env.OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET) { process.stderr.write(`doctor target mismatch: got=${target}\\n`); process.exit(64); }",
|
||||
" if (process.env.OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_WINDOWS_MODE && windowsMode !== process.env.OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_WINDOWS_MODE) { process.stderr.write(`doctor windows mode mismatch: got=${windowsMode}\\n`); process.exit(64); }",
|
||||
" const unready = new Set((process.env.OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS || '').split(',').filter(Boolean));",
|
||||
" if (unready.has(provider)) { process.stderr.write(JSON.stringify({ ok: false, provider }) + '\\n'); process.exit(1); }",
|
||||
" process.stdout.write(JSON.stringify({ ok: true, provider }) + '\\n');",
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
`if (args[0] === "run" || args[0] === "warmup") { ${stampClaimScript} }`,
|
||||
'if (args[0] === "run" && Number.parseInt(process.env.OPENCLAW_FAKE_CRABBOX_RUN_STATUS || "0", 10) !== 0) {',
|
||||
" process.stderr.write('fake run failure\\n');",
|
||||
@@ -566,7 +610,12 @@ function wrapperEnv(helpText: string, options: WrapperOptions): NodeJS.ProcessEn
|
||||
OPENCLAW_CRABBOX_SYNC_MIN_FREE_BYTES: "0",
|
||||
OPENCLAW_CRABBOX_WRAPPER_IGNORE_REPO_BINARY: "1",
|
||||
...(options.configJson
|
||||
? { OPENCLAW_FAKE_CRABBOX_CONFIG_JSON: JSON.stringify(options.configJson) }
|
||||
? {
|
||||
OPENCLAW_FAKE_CRABBOX_CONFIG_JSON: JSON.stringify({
|
||||
brokerMode: "managed",
|
||||
...options.configJson,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(options.configStatus
|
||||
? { OPENCLAW_FAKE_CRABBOX_CONFIG_STATUS: String(options.configStatus) }
|
||||
@@ -829,6 +878,365 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
runWrapper("provider: aws\n", ["--version"]);
|
||||
});
|
||||
|
||||
it("routes CI workloads through the first ready provider", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "ci-fast", "--", "echo ok"],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS: "blacksmith-testbox",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("daytona");
|
||||
expect(result.stderr).toContain(
|
||||
"route workload=ci-fast selected=daytona chain=blacksmith-testbox,daytona,azure,aws",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses brokered cloud providers as the final CI fallback", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload=ci-fast", "--", "echo ok"],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS: "blacksmith-testbox,daytona,azure",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("aws");
|
||||
expect(result.stderr).toContain("selected=aws");
|
||||
});
|
||||
|
||||
it("keeps the configured provider when no workload is requested", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--", "echo ok"],
|
||||
{
|
||||
configJson: {
|
||||
provider: "aws",
|
||||
target: "linux",
|
||||
windowsMode: "normal",
|
||||
coordinator: "configured-broker",
|
||||
brokerMode: "managed",
|
||||
brokerAuth: "configured",
|
||||
},
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).not.toContain("--provider");
|
||||
expect(result.stderr).not.toContain("route workload=");
|
||||
});
|
||||
|
||||
it("requires the originating provider when reusing a workload-routed lease", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "interactive", "--id", "cbx_existing", "--", "echo ok"],
|
||||
{
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"reusing a workload-routed lease with --id requires --provider",
|
||||
);
|
||||
});
|
||||
|
||||
it("reuses a workload-routed lease through its explicit provider", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
[
|
||||
"run",
|
||||
"--provider",
|
||||
"daytona",
|
||||
"--workload",
|
||||
"interactive",
|
||||
"--id",
|
||||
"cbx_existing",
|
||||
"--",
|
||||
"echo ok",
|
||||
],
|
||||
{
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("daytona");
|
||||
expect(result.stderr).not.toContain("route workload=");
|
||||
});
|
||||
|
||||
it("routes configured macOS targets through AWS", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "ci-proof", "--", "echo ok"],
|
||||
{
|
||||
configJson: {
|
||||
provider: "blacksmith-testbox",
|
||||
target: "macos",
|
||||
windowsMode: "normal",
|
||||
coordinator: "configured-broker",
|
||||
brokerAuth: "configured",
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET: "macos",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("aws");
|
||||
expect(result.stderr).toContain("chain=aws");
|
||||
});
|
||||
|
||||
it("probes native Windows readiness with the requested target context", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
[
|
||||
"run",
|
||||
"--workload",
|
||||
"ci-proof",
|
||||
"--target",
|
||||
"windows",
|
||||
"--windows-mode",
|
||||
"normal",
|
||||
"--",
|
||||
"echo ok",
|
||||
],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_TARGET: "windows",
|
||||
OPENCLAW_FAKE_CRABBOX_EXPECT_DOCTOR_WINDOWS_MODE: "normal",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("azure");
|
||||
expect(result.stderr).toContain("chain=azure,aws");
|
||||
});
|
||||
|
||||
it("rejects the Windows workload without a Windows target", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "windows", "--", "echo ok"],
|
||||
{ env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stderr).toContain("workload=windows requires target=windows");
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("preserves following options when workload has no value", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "--target", "windows", "--", "echo ok"],
|
||||
{ env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stderr).toContain("--workload requires a value");
|
||||
expect(result.stderr).not.toContain('unsupported Crabbox workload "--target"');
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("rejects authenticated registered mode for broker-only cloud routing", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "desktop", "--", "echo ok"],
|
||||
{
|
||||
configJson: {
|
||||
provider: "azure",
|
||||
target: "linux",
|
||||
windowsMode: "normal",
|
||||
coordinator: "configured-broker",
|
||||
brokerMode: "registered",
|
||||
brokerAuth: "configured",
|
||||
},
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stderr).toContain("managed Crabbox broker auth unavailable");
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("falls through Blacksmith when the Crabbox binary is too old", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "ci-fast", "--", "echo ok"],
|
||||
{
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.21.9" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("azure");
|
||||
expect(result.stderr).toContain(
|
||||
"blacksmith-testbox:requires Crabbox >= 0.22.0 for Blacksmith Testbox",
|
||||
);
|
||||
});
|
||||
|
||||
it("honors direct-cloud debugging during automatic readiness checks", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "desktop", "--", "echo ok"],
|
||||
{
|
||||
configJson: { coordinator: "", brokerAuth: "missing" },
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD: "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("azure");
|
||||
expect(result.stderr).toContain("selected=azure");
|
||||
});
|
||||
|
||||
it("keeps workload configuration away from administrative commands", () => {
|
||||
const result = runDefaultWrapper(["--version"], {
|
||||
env: { OPENCLAW_CRABBOX_WORKLOAD: "ci-fast" },
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout.trim()).toBe("crabbox 0.22.1");
|
||||
expect(result.stderr).not.toContain("route workload=");
|
||||
});
|
||||
|
||||
it("does not validate workload flags on administrative commands", () => {
|
||||
const result = runDefaultWrapper(["--version", "--workload", "surprise"]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout.trim()).toBe("crabbox 0.22.1");
|
||||
expect(result.stderr).not.toContain("unsupported Crabbox workload");
|
||||
});
|
||||
|
||||
it("keeps explicit provider choices outside automatic routing", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--provider", "azure", "--workload", "interactive", "--", "echo ok"],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS: "azure",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("azure");
|
||||
expect(result.stderr).not.toContain("route workload=");
|
||||
});
|
||||
|
||||
it("still validates workloads when a provider is explicit", () => {
|
||||
const result = runWrapper("provider: aws, azure, blacksmith-testbox, or daytona\n", [
|
||||
"run",
|
||||
"--provider",
|
||||
"azure",
|
||||
"--workload",
|
||||
"surprise",
|
||||
"--",
|
||||
"echo ok",
|
||||
]);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain('unsupported Crabbox workload "surprise"');
|
||||
});
|
||||
|
||||
it("requires a compatible Crabbox for explicit brokered Daytona runs", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--provider", "daytona", "--", "echo ok"],
|
||||
{
|
||||
env: { OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.39.9" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"provider=daytona requires Crabbox >= 0.40.0 for brokered execution",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows intentional direct Daytona debugging on older Crabbox versions", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--provider", "daytona", "--", "echo ok"],
|
||||
{
|
||||
configJson: { coordinator: "", brokerAuth: "missing" },
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.39.9",
|
||||
OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD: "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("daytona");
|
||||
});
|
||||
|
||||
it("allows automatic direct Daytona routing on older Crabbox versions", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "interactive", "--", "echo ok"],
|
||||
{
|
||||
configJson: { coordinator: "", brokerAuth: "missing" },
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.39.9",
|
||||
OPENCLAW_CRABBOX_ALLOW_DIRECT_CLOUD: "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("daytona");
|
||||
expect(result.stderr).toContain("selected=daytona");
|
||||
});
|
||||
|
||||
it("fails closed when no policy provider is ready", () => {
|
||||
const result = runWrapper(
|
||||
"provider: aws, azure, blacksmith-testbox, or daytona\n",
|
||||
["run", "--workload", "ci-fast", "--", "echo ok"],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_FAKE_CRABBOX_VERSION: "crabbox 0.40.0",
|
||||
OPENCLAW_FAKE_CRABBOX_UNREADY_PROVIDERS: "blacksmith-testbox,daytona,azure,aws",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("no ready provider for workload=ci-fast");
|
||||
expect(result.stderr).toContain("provider readiness");
|
||||
});
|
||||
|
||||
it("rejects unknown workload policies before execution", () => {
|
||||
const result = runDefaultWrapper(["run", "--workload", "surprise", "--", "echo ok"]);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain('unsupported Crabbox workload "surprise"');
|
||||
});
|
||||
|
||||
it("accepts advertised canonical providers from Crabbox help", () => {
|
||||
const result = runDefaultWrapper(["run", "--provider", "local-container", "--", "echo ok"]);
|
||||
|
||||
@@ -1608,10 +2016,9 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("provider=aws requires a configured Crabbox broker");
|
||||
expect(result.stderr).toContain(
|
||||
"crabbox login --url https://crabbox.openclaw.ai --provider aws",
|
||||
);
|
||||
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).not.toContain("--provider aws");
|
||||
});
|
||||
|
||||
it("fails closed for AWS proof when broker auth is stale", () => {
|
||||
@@ -1622,7 +2029,7 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("provider=aws requires a configured Crabbox broker");
|
||||
expect(result.stderr).toContain("provider=aws requires a configured managed Crabbox broker");
|
||||
});
|
||||
|
||||
it("allows explicit direct AWS debugging without broker auth", () => {
|
||||
|
||||
Reference in New Issue
Block a user