test(cli): add bootstrap-external unit and bootstrap-command tests

This commit is contained in:
kumarabhirup 2026-03-02 18:31:40 -08:00
parent 0f057c0346
commit 5eb084db95
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
2 changed files with 834 additions and 0 deletions

View File

@ -0,0 +1,564 @@
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { bootstrapCommand } from "./bootstrap-external.js";
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: vi.fn(),
};
});
type SpawnCall = {
command: string;
args: string[];
options?: { stdio?: unknown };
};
function createTempStateDir(): string {
const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
const dir = path.join(os.tmpdir(), `ironclaw-bootstrap-${suffix}`);
mkdirSync(dir, { recursive: true });
return dir;
}
function writeBootstrapFixtures(stateDir: string): void {
const config = {
agents: {
defaults: {
model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.6" },
},
},
gateway: {
mode: "local",
},
};
writeFileSync(path.join(stateDir, "openclaw.json"), JSON.stringify(config));
const authDir = path.join(stateDir, "agents", "main", "agent");
mkdirSync(authDir, { recursive: true });
writeFileSync(
path.join(authDir, "auth-profiles.json"),
JSON.stringify({
profiles: {
"vercel-ai-gateway:default": {
provider: "vercel-ai-gateway",
key: "vck_test_123",
},
},
}),
);
}
function createMockChild(params: {
code: number;
stdout?: string;
stderr?: string;
}): EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
kill: ReturnType<typeof vi.fn>;
} {
const child = new EventEmitter() as EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
kill: ReturnType<typeof vi.fn>;
};
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.kill = vi.fn();
queueMicrotask(() => {
if (params.stdout) {
child.stdout.emit("data", Buffer.from(params.stdout));
}
if (params.stderr) {
child.stderr.emit("data", Buffer.from(params.stderr));
}
child.emit("close", params.code);
});
return child;
}
describe("bootstrapCommand always-onboard behavior", () => {
const originalEnv = { ...process.env };
const spawnMock = vi.mocked(spawn);
let stateDir = "";
let spawnCalls: SpawnCall[] = [];
let forceGlobalMissing = false;
let globalDetectCount = 0;
let healthFailuresBeforeSuccess = 0;
let healthCallCount = 0;
let alwaysHealthFail = false;
beforeEach(() => {
stateDir = createTempStateDir();
writeBootstrapFixtures(stateDir);
spawnCalls = [];
forceGlobalMissing = false;
globalDetectCount = 0;
healthFailuresBeforeSuccess = 0;
healthCallCount = 0;
alwaysHealthFail = false;
process.env = {
...originalEnv,
OPENCLAW_PROFILE: "ironclaw",
OPENCLAW_STATE_DIR: stateDir,
VITEST: "true",
};
spawnMock.mockImplementation((command, args = [], options) => {
const commandString = String(command);
const argList = Array.isArray(args) ? args.map(String) : [];
spawnCalls.push({
command: commandString,
args: argList,
options: options as { stdio?: unknown } | undefined,
});
if (commandString === "openclaw" && argList[0] === "--version") {
return createMockChild({ code: 0, stdout: "2026.3.1\n" }) as never;
}
if (
commandString === "npm" &&
argList.includes("ls") &&
argList.includes("-g") &&
argList.includes("openclaw")
) {
globalDetectCount += 1;
const reportMissing = forceGlobalMissing && globalDetectCount === 1;
return createMockChild({
code: reportMissing ? 1 : 0,
stdout: reportMissing
? '{"dependencies":{}}'
: '{"dependencies":{"openclaw":{"version":"2026.3.1"}}}',
}) as never;
}
if (commandString === "npm" && argList.includes("prefix") && argList.includes("-g")) {
return createMockChild({
code: 0,
stdout: `${path.join(stateDir, "npm-global")}\n`,
}) as never;
}
if (commandString === "npm" && argList.includes("install") && argList.includes("-g")) {
return createMockChild({ code: 0, stdout: "installed\n" }) as never;
}
if ((commandString === "which" || commandString === "where") && argList[0] === "openclaw") {
return createMockChild({ code: 0, stdout: "/usr/local/bin/openclaw\n" }) as never;
}
if (
commandString === "openclaw" &&
argList.includes("config") &&
argList.includes("get") &&
argList.includes("gateway.mode")
) {
return createMockChild({ code: 0, stdout: "local\n" }) as never;
}
if (commandString === "openclaw" && argList.includes("health")) {
healthCallCount += 1;
if (alwaysHealthFail || healthCallCount <= healthFailuresBeforeSuccess) {
return createMockChild({
code: 1,
stderr: "gateway closed (1006 abnormal closure)\n",
}) as never;
}
return createMockChild({ code: 0, stdout: '{"ok":true}\n' }) as never;
}
return createMockChild({ code: 0, stdout: "ok\n" }) as never;
});
vi.stubGlobal(
"fetch",
vi.fn(async () => ({ status: 200 }) as unknown as Response),
);
});
afterEach(() => {
process.env = originalEnv;
rmSync(stateDir, { recursive: true, force: true });
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("runs onboard every bootstrap even when config already exists (prevents stale auth drift)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const onboardCalls = spawnCalls.filter(
(call) => call.command === "openclaw" && call.args.includes("onboard"),
);
expect(onboardCalls).toHaveLength(1);
expect(onboardCalls[0]?.args).toEqual(
expect.arrayContaining([
"--profile",
"ironclaw",
"onboard",
"--install-daemon",
"--non-interactive",
"--accept-risk",
"--skip-ui",
]),
);
expect(onboardCalls[0]?.options?.stdio).toEqual(["ignore", "pipe", "pipe"]);
expect(summary.onboarded).toBe(true);
});
it("seeds workspace.duckdb on bootstrap when missing", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const workspaceDir = path.join(stateDir, "workspace");
const workspaceDbPath = path.join(workspaceDir, "workspace.duckdb");
expect(existsSync(workspaceDbPath)).toBe(false);
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
expect(existsSync(workspaceDbPath)).toBe(true);
expect(summary.workspaceSeed?.seeded).toBe(true);
expect(summary.workspaceSeed?.reason).toBe("seeded");
expect(summary.workspaceSeed?.workspaceDir).toBe(workspaceDir);
});
it("skips workspace seeding when workspace.duckdb already exists", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const workspaceDir = path.join(stateDir, "workspace");
const workspaceDbPath = path.join(workspaceDir, "workspace.duckdb");
const identityPath = path.join(workspaceDir, "IDENTITY.md");
mkdirSync(workspaceDir, { recursive: true });
writeFileSync(workspaceDbPath, "existing-db-content", "utf-8");
writeFileSync(identityPath, "# stale identity\n", "utf-8");
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
expect(summary.workspaceSeed?.seeded).toBe(false);
expect(summary.workspaceSeed?.reason).toBe("already-exists");
expect(readFileSync(workspaceDbPath, "utf-8")).toBe("existing-db-content");
const identityContent = readFileSync(identityPath, "utf-8");
expect(identityContent).toContain("You are **Ironclaw**");
expect(identityContent).toContain("~skills/dench/SKILL.md");
expect(identityContent).not.toContain("# stale identity");
});
it("creates people/company/task object projection files when seeding a new workspace", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const customWorkspace = path.join(stateDir, "seed-projection-workspace");
writeFileSync(
path.join(stateDir, "openclaw.json"),
JSON.stringify({
agents: {
defaults: {
model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.6" },
workspace: customWorkspace,
},
},
gateway: { mode: "local" },
}),
);
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
expect(summary.workspaceSeed?.seeded).toBe(true);
expect(summary.workspaceSeed?.workspaceDir).toBe(customWorkspace);
expect(existsSync(path.join(customWorkspace, "people", ".object.yaml"))).toBe(true);
expect(existsSync(path.join(customWorkspace, "company", ".object.yaml"))).toBe(true);
expect(existsSync(path.join(customWorkspace, "task", ".object.yaml"))).toBe(true);
expect(existsSync(path.join(customWorkspace, "WORKSPACE.md"))).toBe(true);
const identityPath = path.join(customWorkspace, "IDENTITY.md");
expect(existsSync(identityPath)).toBe(true);
const identityContent = readFileSync(identityPath, "utf-8");
expect(identityContent).toContain("You are **Ironclaw**");
expect(identityContent).toContain("~skills/dench/SKILL.md");
});
it("installs Dench skill into managed profile skills directory (keeps it out of editable workspace)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const targetSkill = path.join(stateDir, "skills", "dench", "SKILL.md");
const workspaceSkill = path.join(stateDir, "workspace", "skills", "dench", "SKILL.md");
expect(existsSync(targetSkill)).toBe(false);
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
expect(existsSync(targetSkill)).toBe(true);
expect(existsSync(workspaceSkill)).toBe(false);
expect(readFileSync(targetSkill, "utf-8")).toContain("name: database-crm-system");
});
it("replaces existing managed Dench skill on bootstrap (keeps updates in sync)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const targetDir = path.join(stateDir, "skills", "dench");
const targetSkill = path.join(targetDir, "SKILL.md");
mkdirSync(targetDir, { recursive: true });
writeFileSync(targetSkill, "name: dench\n# custom\n");
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const content = readFileSync(targetSkill, "utf-8");
expect(content).toContain("name: database-crm-system");
expect(content).not.toContain("# custom");
});
it("keeps Dench in managed skills even when workspace path is custom", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const customWorkspace = path.join(stateDir, "custom-workspace-root");
writeFileSync(
path.join(stateDir, "openclaw.json"),
JSON.stringify({
agents: {
defaults: {
model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.6" },
workspace: customWorkspace,
},
},
gateway: { mode: "local" },
}),
);
const managedSkill = path.join(stateDir, "skills", "dench", "SKILL.md");
const workspaceSkill = path.join(customWorkspace, "skills", "dench", "SKILL.md");
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
expect(existsSync(managedSkill)).toBe(true);
expect(existsSync(workspaceSkill)).toBe(false);
});
it("uses inherited stdio for onboarding in interactive mode (shows wizard prompts)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
noOpen: true,
skipUpdate: true,
},
runtime,
);
const onboardCalls = spawnCalls.filter(
(call) => call.command === "openclaw" && call.args.includes("onboard"),
);
expect(onboardCalls).toHaveLength(1);
expect(onboardCalls[0]?.options?.stdio).toBe("inherit");
expect(onboardCalls[0]?.args).not.toContain("--non-interactive");
expect(onboardCalls[0]?.args).not.toContain("--accept-risk");
});
it("does not call gateway install/start fallback when onboarding is always used", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const gatewayInstallCalled = spawnCalls.some(
(call) =>
call.command === "openclaw" &&
call.args.includes("gateway") &&
call.args.includes("install"),
);
const gatewayStartCalled = spawnCalls.some(
(call) =>
call.command === "openclaw" && call.args.includes("gateway") && call.args.includes("start"),
);
expect(gatewayInstallCalled).toBe(false);
expect(gatewayStartCalled).toBe(false);
});
it("installs global OpenClaw even when a local binary already resolves", async () => {
forceGlobalMissing = true;
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const globalInstallCalls = spawnCalls.filter(
(call) =>
call.command === "npm" &&
call.args.includes("install") &&
call.args.includes("-g") &&
call.args.includes("openclaw@latest"),
);
expect(globalInstallCalls.length).toBeGreaterThan(0);
expect(summary.installedOpenClawCli).toBe(true);
});
it("runs doctor/gateway autofix steps when initial health probe fails", async () => {
healthFailuresBeforeSuccess = 1;
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const doctorFixCalled = spawnCalls.some(
(call) =>
call.command === "openclaw" && call.args.includes("doctor") && call.args.includes("--fix"),
);
const gatewayInstallCalled = spawnCalls.some(
(call) =>
call.command === "openclaw" &&
call.args.includes("gateway") &&
call.args.includes("install"),
);
const gatewayStartCalled = spawnCalls.some(
(call) =>
call.command === "openclaw" && call.args.includes("gateway") && call.args.includes("start"),
);
expect(doctorFixCalled).toBe(true);
expect(gatewayInstallCalled).toBe(true);
expect(gatewayStartCalled).toBe(true);
expect(summary.gatewayReachable).toBe(true);
expect(summary.gatewayAutoFix?.attempted).toBe(true);
expect(summary.gatewayAutoFix?.recovered).toBe(true);
});
it("prints likely gateway cause with log excerpt when autofix cannot recover", async () => {
alwaysHealthFail = true;
mkdirSync(path.join(stateDir, "logs"), { recursive: true });
writeFileSync(
path.join(stateDir, "logs", "gateway.err.log"),
[
"unauthorized: gateway token mismatch",
"Invalid config",
"plugins.slots.memory: plugin not found: memory-core",
].join("\n"),
);
const logSpy = vi.fn();
const runtime: RuntimeEnv = {
log: logSpy,
error: vi.fn(),
exit: vi.fn(),
};
const summary = await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const logMessages = logSpy.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
expect(summary.gatewayReachable).toBe(false);
expect(summary.gatewayAutoFix?.attempted).toBe(true);
expect(logMessages).toContain("Likely gateway cause:");
expect(logMessages).toContain("gateway.err.log");
});
});

View File

@ -0,0 +1,270 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
buildBootstrapDiagnostics,
checkAgentAuth,
resolveBootstrapRolloutStage,
isLegacyFallbackEnabled,
type BootstrapDiagnostics,
} from "./bootstrap-external.js";
function getCheck(
diagnostics: BootstrapDiagnostics,
id: BootstrapDiagnostics["checks"][number]["id"],
) {
const check = diagnostics.checks.find((item) => item.id === id);
expect(check).toBeDefined();
return check!;
}
function createTempStateDir(): string {
const dir = path.join(
tmpdir(),
`ironclaw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
mkdirSync(dir, { recursive: true });
return dir;
}
function writeConfig(stateDir: string, config: Record<string, unknown>): void {
writeFileSync(path.join(stateDir, "openclaw.json"), JSON.stringify(config));
}
function writeAuthProfiles(stateDir: string, profiles: Record<string, unknown>): void {
const agentDir = path.join(stateDir, "agents", "main", "agent");
mkdirSync(agentDir, { recursive: true });
writeFileSync(path.join(agentDir, "auth-profiles.json"), JSON.stringify(profiles));
}
describe("bootstrap-external diagnostics", () => {
let stateDir: string;
beforeEach(() => {
stateDir = createTempStateDir();
writeConfig(stateDir, {
agents: { defaults: { model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.6" } } },
});
writeAuthProfiles(stateDir, {
version: 1,
profiles: {
"vercel-ai-gateway:default": {
type: "api_key",
provider: "vercel-ai-gateway",
key: "vck_test_key_1234567890",
},
},
});
});
afterEach(() => {
rmSync(stateDir, { recursive: true, force: true });
});
const baseParams = (dir: string) => ({
profile: "ironclaw",
openClawCliAvailable: true,
openClawVersion: "2026.3.1",
gatewayPort: 18789,
gatewayUrl: "ws://127.0.0.1:18789",
gatewayProbe: { ok: true as const },
webPort: 3100,
webReachable: true,
rolloutStage: "default" as const,
legacyFallbackEnabled: false,
stateDir: dir,
env: { HOME: "/home/testuser" },
});
it("reports passing checks including agent-auth when config and keys exist", () => {
const diagnostics = buildBootstrapDiagnostics(baseParams(stateDir));
expect(getCheck(diagnostics, "profile").status).toBe("pass");
expect(getCheck(diagnostics, "gateway").status).toBe("pass");
expect(getCheck(diagnostics, "agent-auth").status).toBe("pass");
expect(getCheck(diagnostics, "web-ui").status).toBe("pass");
expect(diagnostics.hasFailures).toBe(false);
});
it("fails agent-auth when auth-profiles.json is missing (catches missing onboard)", () => {
const emptyDir = createTempStateDir();
writeConfig(emptyDir, {
agents: { defaults: { model: { primary: "vercel-ai-gateway/anthropic/claude-4" } } },
});
try {
const diagnostics = buildBootstrapDiagnostics(baseParams(emptyDir));
const auth = getCheck(diagnostics, "agent-auth");
expect(auth.status).toBe("fail");
expect(auth.detail).toContain("auth-profiles.json");
expect(auth.remediation).toContain("onboard --install-daemon");
expect(diagnostics.hasFailures).toBe(true);
} finally {
rmSync(emptyDir, { recursive: true, force: true });
}
});
it("fails agent-auth when key exists for wrong provider (catches provider mismatch)", () => {
const dir = createTempStateDir();
writeConfig(dir, {
agents: { defaults: { model: { primary: "anthropic/claude-4" } } },
});
writeAuthProfiles(dir, {
profiles: {
"openai:default": { provider: "openai", key: "sk-test" },
},
});
try {
const diagnostics = buildBootstrapDiagnostics(baseParams(dir));
const auth = getCheck(diagnostics, "agent-auth");
expect(auth.status).toBe("fail");
expect(auth.detail).toContain('"anthropic"');
expect(auth.remediation).toContain("onboard --install-daemon");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("fails agent-auth when no model provider is configured", () => {
const dir = createTempStateDir();
writeConfig(dir, { agents: {} });
try {
const diagnostics = buildBootstrapDiagnostics(baseParams(dir));
const auth = getCheck(diagnostics, "agent-auth");
expect(auth.status).toBe("fail");
expect(auth.detail).toContain("No model provider configured");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("surfaces actionable remediation for gateway auth failures", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
gatewayProbe: { ok: false as const, detail: "Unauthorized: token mismatch" },
});
const gateway = getCheck(diagnostics, "gateway");
expect(gateway.status).toBe("fail");
expect(String(gateway.remediation)).toContain("onboard");
expect(diagnostics.hasFailures).toBe(true);
});
it("marks rollout-stage as warning for beta and includes opt-in guidance", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
rolloutStage: "beta",
});
const rollout = getCheck(diagnostics, "rollout-stage");
expect(rollout.status).toBe("warn");
expect(String(rollout.remediation)).toContain("IRONCLAW_BOOTSTRAP_BETA_OPT_IN");
});
it("fails cutover-gates when enforcement is enabled without gate envs", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
env: { HOME: "/home/testuser", IRONCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES: "1" },
});
expect(getCheck(diagnostics, "cutover-gates").status).toBe("fail");
expect(diagnostics.hasFailures).toBe(true);
});
it("passes cutover-gates when both required gate envs are set", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
env: {
HOME: "/home/testuser",
IRONCLAW_BOOTSTRAP_MIGRATION_SUITE_OK: "1",
IRONCLAW_BOOTSTRAP_ONBOARDING_E2E_OK: "1",
},
});
expect(getCheck(diagnostics, "cutover-gates").status).toBe("pass");
});
});
describe("checkAgentAuth", () => {
let stateDir: string;
beforeEach(() => {
stateDir = createTempStateDir();
});
afterEach(() => {
rmSync(stateDir, { recursive: true, force: true });
});
it("returns ok when a valid key exists for the requested provider", () => {
writeAuthProfiles(stateDir, {
profiles: {
"vercel-ai-gateway:default": {
provider: "vercel-ai-gateway",
key: "vck_valid_key",
},
},
});
const result = checkAgentAuth(stateDir, "vercel-ai-gateway");
expect(result.ok).toBe(true);
expect(result.provider).toBe("vercel-ai-gateway");
});
it("returns not ok when auth-profiles.json does not exist", () => {
const result = checkAgentAuth(stateDir, "vercel-ai-gateway");
expect(result.ok).toBe(false);
expect(result.detail).toContain("auth-profiles.json");
});
it("returns not ok when key exists for a different provider", () => {
writeAuthProfiles(stateDir, {
profiles: {
"openai:default": { provider: "openai", key: "sk-test" },
},
});
const result = checkAgentAuth(stateDir, "anthropic");
expect(result.ok).toBe(false);
expect(result.detail).toContain('"anthropic"');
});
it("returns not ok when key string is empty", () => {
writeAuthProfiles(stateDir, {
profiles: {
"vercel-ai-gateway:default": { provider: "vercel-ai-gateway", key: "" },
},
});
const result = checkAgentAuth(stateDir, "vercel-ai-gateway");
expect(result.ok).toBe(false);
});
it("returns not ok when provider is undefined", () => {
const result = checkAgentAuth(stateDir, undefined);
expect(result.ok).toBe(false);
expect(result.detail).toContain("No model provider configured");
});
it("returns not ok when profiles object is empty", () => {
writeAuthProfiles(stateDir, { profiles: {} });
const result = checkAgentAuth(stateDir, "vercel-ai-gateway");
expect(result.ok).toBe(false);
});
});
describe("bootstrap-external rollout env helpers", () => {
it("resolves rollout stage from ironclaw/openclaw env vars", () => {
expect(resolveBootstrapRolloutStage({ IRONCLAW_BOOTSTRAP_ROLLOUT: "beta" })).toBe("beta");
expect(resolveBootstrapRolloutStage({ OPENCLAW_BOOTSTRAP_ROLLOUT: "internal" })).toBe(
"internal",
);
expect(resolveBootstrapRolloutStage({ IRONCLAW_BOOTSTRAP_ROLLOUT: "invalid" })).toBe("default");
});
it("detects legacy fallback via either env namespace", () => {
expect(isLegacyFallbackEnabled({ IRONCLAW_BOOTSTRAP_LEGACY_FALLBACK: "1" })).toBe(true);
expect(isLegacyFallbackEnabled({ OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK: "true" })).toBe(true);
expect(isLegacyFallbackEnabled({})).toBe(false);
});
});