mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-24 14:51:14 +00:00
* feat(onboarding): unify setup recommendations * fix(control-ui): proxy model setup icons * fix(onboarding): refresh landing artifacts * chore(ui): sync translated i18n catalogs * chore(ui): keep generated locale artifacts on the refresh workflow
5617 lines
189 KiB
TypeScript
5617 lines
189 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { resolveAgentDir } from "../agents/agent-scope-config.js";
|
|
import {
|
|
readAuthProfileStoreForTest,
|
|
removeOAuthTestTempRoot,
|
|
} from "../agents/auth-profiles/oauth-test-utils.js";
|
|
import { upsertAuthProfileWithLock } from "../agents/auth-profiles/profiles.js";
|
|
import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js";
|
|
import {
|
|
fingerprintAuthProfileCredential,
|
|
fingerprintResolvedProviderAuth,
|
|
type AgentExecutionAuthBinding,
|
|
} from "../agents/execution-auth-binding.js";
|
|
import { detectInferenceBackends } from "../commands/onboard-inference.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|
import { withoutPluginInstallRecords } from "../plugins/installed-plugin-index-records.js";
|
|
import { hasRetainedManagedNpmInstallMarker } from "../plugins/managed-npm-retention.js";
|
|
import type { ProviderAuthChoiceMetadata } from "../plugins/provider-auth-choices.js";
|
|
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
|
import {
|
|
getActivePluginRegistry,
|
|
getActivePluginRegistryKey,
|
|
getActivePluginRegistryWorkspaceDir,
|
|
resetPluginRuntimeStateForTest,
|
|
setActivePluginRegistry,
|
|
} from "../plugins/runtime.js";
|
|
import { ensurePluginRegistryLoaded } from "../plugins/runtime/runtime-registry-loader.js";
|
|
import type { ProviderPlugin } from "../plugins/types.js";
|
|
import { disposeOpenClawAgentDatabaseByPath } from "../state/openclaw-agent-db.js";
|
|
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
|
import { cleanupSystemAgentSession, createSystemAgentSession } from "./agent-turn.js";
|
|
import { runSystemAgentTurnWithDeps } from "./agent-turn.test-support.js";
|
|
import { resolveSystemAgentConfiguredRouteFromConfig } from "./inference-route.js";
|
|
import { applySystemAgentModelSelection } from "./setup-apply.js";
|
|
import { resolveSetupInferenceProbeStreamParams } from "./setup-inference-probe.js";
|
|
import {
|
|
SetupInferenceActivationIndeterminateError,
|
|
activateSetupInference as activateSetupInferenceImpl,
|
|
type BoundVerifySetupInferenceResult,
|
|
detectSetupInference,
|
|
listSetupInferenceAuthOptions,
|
|
listSetupInferenceManualProviders,
|
|
resolvePersistentApplyInference,
|
|
type VerifySetupInferenceResult,
|
|
verifySetupInference as verifySetupInferenceImpl,
|
|
verifySetupInferenceConfig as verifySetupInferenceConfigImpl,
|
|
} from "./setup-inference.js";
|
|
import {
|
|
createSystemAgentVerifiedInferenceBinding,
|
|
type SystemAgentVerifiedInferenceBinding,
|
|
} from "./verified-inference.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
appendAudit: vi.fn(),
|
|
ensureSelectedAgentHarnessPlugin: vi.fn(),
|
|
refreshPluginRegistryAfterConfigMutation: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./audit.js", () => ({
|
|
appendSystemAgentAuditEntry: mocks.appendAudit,
|
|
}));
|
|
|
|
vi.mock("../agents/harness/runtime-plugin.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../agents/harness/runtime-plugin.js")>()),
|
|
ensureSelectedAgentHarnessPlugin: mocks.ensureSelectedAgentHarnessPlugin,
|
|
}));
|
|
|
|
vi.mock("../plugins/registry-refresh.js", () => ({
|
|
refreshPluginRegistryAfterConfigMutation: mocks.refreshPluginRegistryAfterConfigMutation,
|
|
}));
|
|
|
|
vi.mock("../config/config.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
|
return {
|
|
...actual,
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: false,
|
|
valid: false,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: {},
|
|
})),
|
|
};
|
|
});
|
|
|
|
vi.mock("../commands/onboard-inference.js", async (importActual) => {
|
|
const actual = await importActual<typeof import("../commands/onboard-inference.js")>();
|
|
return {
|
|
...actual,
|
|
detectInferenceBackends: vi.fn(async () => [
|
|
{
|
|
kind: "claude-cli",
|
|
modelRef: "claude-cli/claude-opus-4-8",
|
|
label: "Claude Code",
|
|
detail: "logged in",
|
|
credentials: true,
|
|
},
|
|
{
|
|
kind: "codex-cli",
|
|
modelRef: "openai/gpt-5.5",
|
|
label: "Codex",
|
|
detail: "installed, not logged in — run `codex login`, then check again",
|
|
credentials: false,
|
|
},
|
|
]),
|
|
};
|
|
});
|
|
|
|
const runtime = { log: () => {}, error: () => {}, exit: () => {} } as never;
|
|
const testCliRuntimeArtifactFingerprint = "test-cli-runtime-artifact";
|
|
const testCodexRuntimeArtifact = {
|
|
id: "codex-app-server",
|
|
fingerprint: "codex-runtime-v1",
|
|
} as const;
|
|
const suiteTempRootTracker = createSuiteTempRootTracker({
|
|
prefix: "setup-inference-test-",
|
|
});
|
|
|
|
beforeAll(async () => {
|
|
await suiteTempRootTracker.setup();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await suiteTempRootTracker.cleanup();
|
|
});
|
|
|
|
async function makeTempDir(): Promise<string> {
|
|
return await suiteTempRootTracker.make("case");
|
|
}
|
|
|
|
const deferSuiteTempDirCleanup = async () => {};
|
|
|
|
function withSuiteTempDirs<
|
|
T extends NonNullable<Parameters<typeof activateSetupInferenceImpl>[0]["deps"]>,
|
|
>(input: T | undefined): T {
|
|
// Keep operation dirs real and unique; only their routine cleanup moves to suite teardown.
|
|
const deps = Object.create(
|
|
Object.getPrototypeOf(input ?? {}),
|
|
Object.getOwnPropertyDescriptors(input ?? {}),
|
|
) as T;
|
|
if (!deps.createTempDir) {
|
|
deps.createTempDir = makeTempDir;
|
|
}
|
|
if (deps.createTempDir === makeTempDir && !deps.removeTempDir) {
|
|
deps.removeTempDir = deferSuiteTempDirCleanup;
|
|
}
|
|
return deps;
|
|
}
|
|
|
|
async function activateSetupInference(
|
|
params: Parameters<typeof activateSetupInferenceImpl>[0],
|
|
): ReturnType<typeof activateSetupInferenceImpl> {
|
|
const deps = withSuiteTempDirs(params.deps);
|
|
const ownerPluginArtifacts = { ownerPluginIds: [], ownerPluginArtifacts: [] } as const;
|
|
const usesRealOwnerBinding =
|
|
params.deps?.createSystemAgentVerifiedInferenceBinding ===
|
|
createSystemAgentVerifiedInferenceBinding;
|
|
if (!deps.captureSystemAgentOwnerPluginArtifacts && !usesRealOwnerBinding) {
|
|
deps.captureSystemAgentOwnerPluginArtifacts = () => ownerPluginArtifacts;
|
|
}
|
|
if (!deps.createSystemAgentVerifiedInferenceBinding) {
|
|
deps.createSystemAgentVerifiedInferenceBinding = async () => ownerPluginArtifacts as never;
|
|
}
|
|
if (!deps.ensurePluginRegistryLoaded) {
|
|
deps.ensurePluginRegistryLoaded = () => {};
|
|
}
|
|
if (!deps.resolveCliRuntimeArtifactFingerprint) {
|
|
deps.resolveCliRuntimeArtifactFingerprint = vi.fn(
|
|
async () => testCliRuntimeArtifactFingerprint,
|
|
);
|
|
}
|
|
return activateSetupInferenceImpl({
|
|
...params,
|
|
// Most activation tests isolate commit mechanics from the verified-owner
|
|
// implementation. Owner-CAS regressions opt back into the real helper.
|
|
deps,
|
|
});
|
|
}
|
|
|
|
type TestVerifySetupInferenceParams = Omit<
|
|
Parameters<typeof verifySetupInferenceImpl>[0],
|
|
"bindSession"
|
|
>;
|
|
|
|
function verifySetupInference(
|
|
params: TestVerifySetupInferenceParams & { bindSession: true },
|
|
): Promise<BoundVerifySetupInferenceResult>;
|
|
function verifySetupInference(
|
|
params: TestVerifySetupInferenceParams & { bindSession?: false },
|
|
): Promise<VerifySetupInferenceResult>;
|
|
function verifySetupInference(
|
|
params: TestVerifySetupInferenceParams & { bindSession?: boolean },
|
|
): Promise<VerifySetupInferenceResult | BoundVerifySetupInferenceResult> {
|
|
return verifySetupInferenceImpl({
|
|
...params,
|
|
deps: withSuiteTempDirs(params.deps),
|
|
} as never);
|
|
}
|
|
|
|
async function verifySetupInferenceConfig(
|
|
params: Parameters<typeof verifySetupInferenceConfigImpl>[0],
|
|
): ReturnType<typeof verifySetupInferenceConfigImpl> {
|
|
return verifySetupInferenceConfigImpl({ ...params, deps: withSuiteTempDirs(params.deps) });
|
|
}
|
|
|
|
type SuccessfulRunParams = {
|
|
onSuccessfulAuthBinding?: (binding: AgentExecutionAuthBinding) => void;
|
|
authProfileId?: string;
|
|
agentHarnessRuntimeOverride?: string;
|
|
config?: OpenClawConfig;
|
|
};
|
|
|
|
function successfulAgentHarnessBinding(params?: SuccessfulRunParams): AgentExecutionAuthBinding {
|
|
const requestedHarnessId = params?.agentHarnessRuntimeOverride?.trim();
|
|
const agentHarnessId =
|
|
!requestedHarnessId || requestedHarnessId === "auto" ? "openclaw" : requestedHarnessId;
|
|
return {
|
|
agentHarnessId,
|
|
...(agentHarnessId === "codex"
|
|
? {
|
|
runtimeOwnerKind: "plugin-harness",
|
|
runtimeOwnerId: agentHarnessId,
|
|
runtimeArtifactId: testCodexRuntimeArtifact.id,
|
|
runtimeArtifactFingerprint: testCodexRuntimeArtifact.fingerprint,
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function successfulRun(provider: string, model: string, params?: SuccessfulRunParams) {
|
|
params?.onSuccessfulAuthBinding?.(
|
|
provider.endsWith("-cli")
|
|
? {
|
|
runtimeOwnerFingerprint: "test-runtime-owner",
|
|
runtimeOwnerKind: "cli-runtime",
|
|
runtimeOwnerId: provider,
|
|
runtimeArtifactFingerprint: testCliRuntimeArtifactFingerprint,
|
|
runtimeArtifactId: provider,
|
|
...(params?.authProfileId ? { authProfileId: params.authProfileId } : {}),
|
|
}
|
|
: {
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint: "test-credential-owner",
|
|
...(params?.authProfileId ? { authProfileId: params.authProfileId } : {}),
|
|
},
|
|
);
|
|
return {
|
|
meta: {
|
|
finalAssistantVisibleText: "OK",
|
|
executionTrace: { winnerProvider: provider, winnerModel: model },
|
|
},
|
|
};
|
|
}
|
|
|
|
function successfulRunner(provider: string, model: string) {
|
|
return async (params: SuccessfulRunParams) => successfulRun(provider, model, params);
|
|
}
|
|
|
|
function createConfigTransformHarness(
|
|
sourceConfig: OpenClawConfig = {},
|
|
runtimeConfig: OpenClawConfig = sourceConfig,
|
|
) {
|
|
const state = {
|
|
sourceConfig: structuredClone(sourceConfig),
|
|
runtimeConfig: structuredClone(runtimeConfig),
|
|
};
|
|
const transform = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: {
|
|
snapshot: {
|
|
exists: true;
|
|
valid: true;
|
|
path: string;
|
|
config: OpenClawConfig;
|
|
sourceConfig: OpenClawConfig;
|
|
runtimeConfig: OpenClawConfig;
|
|
};
|
|
previousHash: string | null;
|
|
attempt: number;
|
|
},
|
|
) => Promise<{ nextConfig: OpenClawConfig }> | { nextConfig: OpenClawConfig };
|
|
}) => {
|
|
const transformed = await params.transform(state.sourceConfig, {
|
|
snapshot: {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
config: state.runtimeConfig,
|
|
sourceConfig: state.sourceConfig,
|
|
runtimeConfig: state.runtimeConfig,
|
|
},
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
state.sourceConfig = withoutPluginInstallRecords(transformed.nextConfig);
|
|
state.runtimeConfig = structuredClone(state.sourceConfig);
|
|
return { nextConfig: state.sourceConfig };
|
|
},
|
|
);
|
|
return {
|
|
transform,
|
|
current: () => structuredClone(state.sourceConfig),
|
|
};
|
|
}
|
|
|
|
describe("applySystemAgentModelSelection", () => {
|
|
it("pins a verified credential without putting the profile suffix in model metadata", async () => {
|
|
const result = await applySystemAgentModelSelection({
|
|
config: {},
|
|
model: "openai/gpt-5.5",
|
|
authProfileId: "openai:setup-123",
|
|
});
|
|
|
|
expect(result.agents?.defaults?.model).toBe("openai/gpt-5.5@openai:setup-123");
|
|
expect(result.agents?.defaults?.models).toBeUndefined();
|
|
});
|
|
|
|
it("overrides higher-priority runtime metadata on an inheriting default agent", async () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: { model: { primary: "openai/gpt-5.4" } },
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
|
|
const result = await applySystemAgentModelSelection({
|
|
config,
|
|
model: "openai/gpt-5.5",
|
|
agentRuntimeId: "codex",
|
|
});
|
|
|
|
expect(result.agents?.defaults?.model).toMatchObject({ primary: "openai/gpt-5.5" });
|
|
expect(result.agents?.list?.[0]).toMatchObject({
|
|
id: "ops",
|
|
models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } },
|
|
});
|
|
expect(config.agents.list[0]?.models["openai/gpt-5.5"]?.agentRuntime?.id).toBe("openclaw");
|
|
});
|
|
});
|
|
|
|
describe("detectSetupInference", () => {
|
|
it("preserves the shared inference candidate order", async () => {
|
|
const resolveManifestProviderAuthChoices = vi.fn(() => [
|
|
{
|
|
pluginId: "anthropic",
|
|
providerId: "anthropic",
|
|
methodId: "cli",
|
|
choiceId: "anthropic-cli",
|
|
choiceLabel: "Anthropic Claude CLI",
|
|
deprecatedChoiceIds: ["claude-cli"],
|
|
icon: "https://cdn.example.com/claude.svg",
|
|
website: "https://claude.example.com/download",
|
|
},
|
|
]);
|
|
const detection = await detectSetupInference({
|
|
resolveManifestProviderAuthChoices,
|
|
enablePluginInConfig: ((config: OpenClawConfig) => ({ enabled: true, config })) as never,
|
|
probeLocalCommand: vi.fn(async (command) => ({ command, found: false })),
|
|
});
|
|
expect(detection.candidates).toHaveLength(2);
|
|
expect(detection.candidates[0]).toMatchObject({
|
|
kind: "claude-cli",
|
|
recommended: false,
|
|
icon: "https://cdn.example.com/claude.svg",
|
|
website: "https://claude.example.com/download",
|
|
});
|
|
expect(detection.candidates[1]).toMatchObject({ kind: "codex-cli", recommended: false });
|
|
expect(detection.setupComplete).toBe(false);
|
|
expect(detection.recommendedInstalls).toHaveLength(9);
|
|
expect(detection.workspace.length).toBeGreaterThan(0);
|
|
expect(resolveManifestProviderAuthChoices).toHaveBeenCalledWith(
|
|
expect.objectContaining({ includeWorkspacePlugins: false }),
|
|
);
|
|
});
|
|
|
|
it("discovers provider-owned local inference and reports unsafe CLIs without running them", async () => {
|
|
const prepare = vi.fn();
|
|
const detect = vi.fn(async () => ({
|
|
modelRef: "local/qwen-tool",
|
|
detail: "qwen-tool at http://127.0.0.1:9999",
|
|
}));
|
|
const provider: ProviderPlugin = {
|
|
id: "local",
|
|
label: "Local Server",
|
|
pluginId: "local-plugin",
|
|
auth: [
|
|
{
|
|
id: "ambient",
|
|
label: "Local Server",
|
|
kind: "custom",
|
|
appGuidedSetup: { detect, prepare },
|
|
run: async () => ({ profiles: [] }),
|
|
},
|
|
],
|
|
};
|
|
const detection = await detectSetupInference({
|
|
detectInferenceBackends: async () => [
|
|
{
|
|
kind: "claude-cli",
|
|
modelRef: "claude-cli/claude-opus-4-8",
|
|
label: "Claude Code",
|
|
detail: "logged in",
|
|
credentials: true,
|
|
},
|
|
{
|
|
kind: "gemini-cli",
|
|
modelRef: "google-gemini-cli/gemini-3.1-pro-preview",
|
|
label: "Gemini CLI",
|
|
detail: "installed",
|
|
credentials: false,
|
|
},
|
|
],
|
|
probeLocalCommand: vi.fn(async (command) => ({
|
|
command,
|
|
found: command === "agy" || command === "pi" || command === "opencode",
|
|
})),
|
|
resolveManifestProviderAuthChoices: () => [
|
|
{
|
|
pluginId: "local-plugin",
|
|
providerId: "local",
|
|
methodId: "ambient",
|
|
choiceId: "local-model",
|
|
choiceLabel: "Local Server",
|
|
appGuidedDiscovery: true,
|
|
icon: "https://cdn.example.com/local.svg",
|
|
website: "https://local.example.com/download",
|
|
},
|
|
],
|
|
enablePluginInConfig: ((config: OpenClawConfig) => ({ enabled: true, config })) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
});
|
|
|
|
expect(detection.candidates).toEqual([
|
|
expect.objectContaining({ kind: "claude-cli" }),
|
|
{
|
|
kind: "provider-auto:local-model",
|
|
label: "Local Server",
|
|
detail: "qwen-tool at http://127.0.0.1:9999",
|
|
modelRef: "local/qwen-tool",
|
|
recommended: false,
|
|
credentials: true,
|
|
icon: "https://cdn.example.com/local.svg",
|
|
website: "https://local.example.com/download",
|
|
},
|
|
]);
|
|
expect(detection.unavailableCandidates).toEqual([
|
|
expect.objectContaining({ id: "gemini-cli" }),
|
|
expect.objectContaining({ id: "antigravity-cli" }),
|
|
expect.objectContaining({ id: "pi-cli" }),
|
|
expect.objectContaining({ id: "opencode-cli" }),
|
|
]);
|
|
expect(detect).toHaveBeenCalledOnce();
|
|
expect(prepare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("surfaces an invalid existing config instead of treating it as fresh", async () => {
|
|
const { readConfigFileSnapshot } = await import("../config/config.js");
|
|
vi.mocked(readConfigFileSnapshot).mockResolvedValueOnce({
|
|
exists: true,
|
|
valid: false,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [{ path: "agents.defaults.model", message: "Expected a model reference" }],
|
|
config: {},
|
|
} as never);
|
|
|
|
await expect(detectSetupInference()).rejects.toThrow(
|
|
"OpenClaw config /tmp/openclaw.json is invalid (agents.defaults.model: Expected a model reference)",
|
|
);
|
|
});
|
|
|
|
it("lists text-inference key and token methods from provider manifests", () => {
|
|
const choices: ProviderAuthChoiceMetadata[] = [
|
|
{
|
|
pluginId: "visuals",
|
|
providerId: "visuals",
|
|
methodId: "api-key",
|
|
choiceId: "visuals-api-key",
|
|
choiceLabel: "Visuals API key",
|
|
appGuidedSecret: true,
|
|
onboardingScopes: ["image-generation"],
|
|
},
|
|
{
|
|
pluginId: "zeta",
|
|
providerId: "zeta",
|
|
methodId: "oauth",
|
|
choiceId: "zeta-oauth",
|
|
choiceLabel: "Zeta OAuth",
|
|
},
|
|
{
|
|
pluginId: "zeta",
|
|
providerId: "zeta",
|
|
methodId: "direct-key",
|
|
choiceId: "zeta-api-key",
|
|
choiceLabel: "Zeta API key",
|
|
choiceHint: "Direct key",
|
|
icon: "https://cdn.example.com/zeta.svg",
|
|
website: "https://zeta.example.com/keys",
|
|
optionKey: "zetaApiKey",
|
|
cliOption: "--zeta-api-key <key>",
|
|
appGuidedSecret: true,
|
|
},
|
|
{
|
|
pluginId: "alpha",
|
|
providerId: "alpha",
|
|
methodId: "api-key",
|
|
choiceId: "alpha-api-key",
|
|
choiceLabel: "Alpha API key",
|
|
appGuidedSecret: true,
|
|
},
|
|
{
|
|
pluginId: "github-copilot",
|
|
providerId: "github-copilot",
|
|
methodId: "device",
|
|
choiceId: "github-copilot",
|
|
choiceLabel: "GitHub Copilot",
|
|
optionKey: "githubCopilotToken",
|
|
cliOption: "--github-copilot-token <token>",
|
|
appGuidedSecret: true,
|
|
},
|
|
];
|
|
|
|
expect(listSetupInferenceManualProviders(choices)).toEqual([
|
|
{
|
|
id: "alpha-api-key",
|
|
label: "Alpha API key",
|
|
},
|
|
{
|
|
id: "github-copilot",
|
|
label: "GitHub Copilot",
|
|
},
|
|
{
|
|
id: "zeta-api-key",
|
|
label: "Zeta API key",
|
|
hint: "Direct key",
|
|
icon: "https://cdn.example.com/zeta.svg",
|
|
website: "https://zeta.example.com/keys",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("lists provider-owned sign-ins in CLI order without compatibility aliases", () => {
|
|
const choices: ProviderAuthChoiceMetadata[] = [
|
|
{
|
|
pluginId: "google",
|
|
providerId: "google-gemini-cli",
|
|
methodId: "oauth",
|
|
choiceId: "google-gemini-cli",
|
|
choiceLabel: "Gemini CLI OAuth",
|
|
groupId: "google",
|
|
groupLabel: "Google",
|
|
onboardingFeatured: true,
|
|
appGuidedAuth: "oauth",
|
|
},
|
|
{
|
|
pluginId: "openrouter",
|
|
providerId: "openrouter",
|
|
methodId: "oauth",
|
|
choiceId: "openrouter-oauth",
|
|
choiceLabel: "OpenRouter OAuth",
|
|
groupId: "openrouter",
|
|
groupLabel: "OpenRouter",
|
|
onboardingFeatured: true,
|
|
appGuidedAuth: "oauth",
|
|
},
|
|
{
|
|
pluginId: "openai",
|
|
providerId: "openai",
|
|
methodId: "oauth",
|
|
choiceId: "openai",
|
|
choiceLabel: "ChatGPT Login",
|
|
choiceHint: "Browser sign-in",
|
|
icon: "https://cdn.example.com/openai.svg",
|
|
website: "https://openai.example.com/login",
|
|
groupLabel: "OpenAI",
|
|
onboardingFeatured: true,
|
|
appGuidedAuth: "oauth",
|
|
},
|
|
{
|
|
pluginId: "xai",
|
|
providerId: "xai",
|
|
methodId: "oauth",
|
|
choiceId: "xai-oauth",
|
|
choiceLabel: "xAI OAuth",
|
|
groupId: "xai",
|
|
groupLabel: "xAI (Grok)",
|
|
onboardingFeatured: true,
|
|
appGuidedAuth: "device-code",
|
|
},
|
|
{
|
|
pluginId: "github-copilot",
|
|
providerId: "github-copilot",
|
|
methodId: "device",
|
|
choiceId: "github-copilot",
|
|
choiceLabel: "GitHub Copilot",
|
|
appGuidedAuth: "device-code",
|
|
},
|
|
{
|
|
pluginId: "xai",
|
|
providerId: "xai",
|
|
methodId: "device-code",
|
|
choiceId: "xai-device-code",
|
|
choiceLabel: "xAI device code",
|
|
assistantVisibility: "manual-only",
|
|
appGuidedAuth: "device-code",
|
|
},
|
|
];
|
|
|
|
expect(listSetupInferenceAuthOptions(choices)).toEqual([
|
|
{
|
|
id: "openai",
|
|
label: "ChatGPT Login",
|
|
hint: "Browser sign-in",
|
|
groupLabel: "OpenAI",
|
|
icon: "https://cdn.example.com/openai.svg",
|
|
website: "https://openai.example.com/login",
|
|
kind: "oauth",
|
|
featured: true,
|
|
},
|
|
{
|
|
id: "openrouter-oauth",
|
|
label: "OpenRouter OAuth",
|
|
groupLabel: "OpenRouter",
|
|
kind: "oauth",
|
|
featured: true,
|
|
},
|
|
{
|
|
id: "xai-oauth",
|
|
label: "xAI OAuth",
|
|
groupLabel: "xAI (Grok)",
|
|
kind: "device-code",
|
|
featured: true,
|
|
},
|
|
{
|
|
id: "google-gemini-cli",
|
|
label: "Gemini CLI OAuth",
|
|
groupLabel: "Google",
|
|
kind: "oauth",
|
|
featured: true,
|
|
},
|
|
{
|
|
id: "github-copilot",
|
|
label: "GitHub Copilot",
|
|
kind: "device-code",
|
|
featured: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("marks a configured default-agent model as complete setup", async () => {
|
|
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([
|
|
{
|
|
kind: "existing-model",
|
|
modelRef: "openai/gpt-5.5",
|
|
label: "Current model",
|
|
detail: "already configured",
|
|
credentials: true,
|
|
},
|
|
]);
|
|
|
|
const detection = await detectSetupInference({
|
|
resolveManifestProviderAuthChoices: () => [],
|
|
probeLocalCommand: vi.fn(async (command) => ({ command, found: false })),
|
|
});
|
|
|
|
expect(detection).toMatchObject({
|
|
configuredModel: "openai/gpt-5.5",
|
|
setupComplete: true,
|
|
});
|
|
});
|
|
|
|
it("omits Gemini CLI because setup verification cannot hard-disable its tools", async () => {
|
|
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([
|
|
{
|
|
kind: "gemini-cli",
|
|
modelRef: "google-gemini-cli/gemini-3.1-pro-preview",
|
|
label: "Gemini CLI",
|
|
detail: "logged in",
|
|
credentials: true,
|
|
},
|
|
{
|
|
kind: "claude-cli",
|
|
modelRef: "claude-cli/claude-opus-4-8",
|
|
label: "Claude Code",
|
|
detail: "logged in",
|
|
credentials: true,
|
|
},
|
|
]);
|
|
|
|
const detection = await detectSetupInference({
|
|
resolveManifestProviderAuthChoices: () => [],
|
|
probeLocalCommand: vi.fn(async (command) => ({ command, found: false })),
|
|
});
|
|
|
|
expect(detection.candidates).toEqual([
|
|
expect.objectContaining({ kind: "claude-cli", recommended: false }),
|
|
]);
|
|
expect(detection.unavailableCandidates).toEqual([
|
|
expect.objectContaining({ id: "gemini-cli" }),
|
|
]);
|
|
});
|
|
|
|
it("reports installed Pi and OpenCode without offering them as setup inference routes", async () => {
|
|
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([]);
|
|
const probeLocalCommand = vi.fn(async (command: string) => ({
|
|
command,
|
|
found: command === "pi" || command === "opencode",
|
|
}));
|
|
|
|
const detection = await detectSetupInference({
|
|
resolveManifestProviderAuthChoices: () => [],
|
|
probeLocalCommand,
|
|
});
|
|
|
|
expect(detection.candidates).toEqual([]);
|
|
expect(detection.unavailableCandidates).toEqual([
|
|
{
|
|
id: "pi-cli",
|
|
label: "Pi CLI",
|
|
detail: "installed",
|
|
reason:
|
|
"Pi CLI is installed, but its whole-agent sessions require separate setup and are not a reusable guided-setup inference route.",
|
|
},
|
|
{
|
|
id: "opencode-cli",
|
|
label: "OpenCode CLI",
|
|
detail: "installed",
|
|
reason:
|
|
"OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.",
|
|
},
|
|
]);
|
|
expect(probeLocalCommand).toHaveBeenCalledWith("pi");
|
|
expect(probeLocalCommand).toHaveBeenCalledWith("opencode");
|
|
});
|
|
});
|
|
|
|
async function runCodexSetupWithFinalConfig(params: {
|
|
initialConfig?: OpenClawConfig;
|
|
currentConfig: OpenClawConfig;
|
|
currentRuntimeConfig?: OpenClawConfig;
|
|
sourceConfig: OpenClawConfig;
|
|
}) {
|
|
const initialConfig = params.initialConfig ?? params.sourceConfig;
|
|
let persistedConfig = structuredClone(params.currentConfig);
|
|
let committed = false;
|
|
const refreshPluginRegistry = vi.fn(async () => {});
|
|
const transformConfig = vi.fn(
|
|
async (input: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: {
|
|
snapshot: {
|
|
exists: true;
|
|
valid: true;
|
|
path: string;
|
|
config: OpenClawConfig;
|
|
sourceConfig: OpenClawConfig;
|
|
runtimeConfig: OpenClawConfig;
|
|
};
|
|
previousHash: string | null;
|
|
attempt: number;
|
|
},
|
|
) => Promise<{ nextConfig: OpenClawConfig }> | { nextConfig: OpenClawConfig };
|
|
}) => {
|
|
const runtimeConfig = params.currentRuntimeConfig ?? params.sourceConfig;
|
|
const transformed = await input.transform(persistedConfig, {
|
|
snapshot: {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
config: runtimeConfig,
|
|
sourceConfig: persistedConfig,
|
|
runtimeConfig,
|
|
},
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
persistedConfig = withoutPluginInstallRecords(transformed.nextConfig);
|
|
committed = true;
|
|
return { nextConfig: persistedConfig };
|
|
},
|
|
);
|
|
const readConfigFileSnapshot = vi.fn(async () => {
|
|
const runtimeConfig = committed ? persistedConfig : initialConfig;
|
|
const sourceConfig = committed ? persistedConfig : params.sourceConfig;
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
hash: committed ? "after-setup" : "before-setup",
|
|
issues: [],
|
|
config: runtimeConfig,
|
|
sourceConfig,
|
|
runtimeConfig,
|
|
};
|
|
});
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6-sol")) as never,
|
|
ensureCodexRuntimePlugin: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg,
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
return { result, persistedConfig, refreshPluginRegistry, transformConfig };
|
|
}
|
|
|
|
describe("activateSetupInference", () => {
|
|
it("omits the token cap when harness selection is automatic", () => {
|
|
expect(resolveSetupInferenceProbeStreamParams("auto")).toEqual({});
|
|
expect(resolveSetupInferenceProbeStreamParams("openclaw")).toEqual({
|
|
streamParams: { maxTokens: 32 },
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mocks.appendAudit.mockReset();
|
|
mocks.ensureSelectedAgentHarnessPlugin.mockReset().mockResolvedValue(undefined);
|
|
mocks.refreshPluginRegistryAfterConfigMutation.mockReset().mockResolvedValue(undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function createGroqSetupProvider(configPatch?: Partial<OpenClawConfig>): ProviderPlugin {
|
|
return {
|
|
id: "groq",
|
|
label: "Groq",
|
|
pluginId: "groq",
|
|
auth: [
|
|
{
|
|
id: "api-key",
|
|
label: "Groq API key",
|
|
kind: "api_key",
|
|
wizard: { choiceId: "groq-api-key" },
|
|
run: async (ctx) => ({
|
|
profiles: [
|
|
{
|
|
profileId: "groq:default",
|
|
credential: {
|
|
type: "api_key" as const,
|
|
provider: "groq",
|
|
key: ctx.opts?.token,
|
|
},
|
|
},
|
|
],
|
|
defaultModel: "groq/llama-3.3-70b-versatile",
|
|
...(configPatch !== undefined ? { configPatch } : {}),
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function groqSetupChoice(): ProviderAuthChoiceMetadata {
|
|
return {
|
|
pluginId: "groq",
|
|
providerId: "groq",
|
|
methodId: "api-key",
|
|
choiceId: "groq-api-key",
|
|
choiceLabel: "Groq API key",
|
|
appGuidedSecret: true,
|
|
};
|
|
}
|
|
|
|
it("surfaces an invalid existing config without probing or persisting", async () => {
|
|
const runEmbeddedAgent = vi.fn();
|
|
const transformConfig = vi.fn();
|
|
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: false,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [{ path: "gateway.port", message: "Expected a number" }],
|
|
config: {},
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"OpenClaw config /tmp/openclaw.json is invalid (gateway.port: Expected a number). Fix it before running setup.",
|
|
);
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports an audit warning without turning a committed setup into a failure", async () => {
|
|
mocks.appendAudit.mockRejectedValueOnce(new Error("audit directory is read-only"));
|
|
const error = vi.fn();
|
|
const configHarness = createConfigTransformHarness();
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime: { log: () => {}, error, exit: () => {} } as never,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
hash: "setup-config-hash",
|
|
config: {},
|
|
sourceConfig: {},
|
|
runtimeConfig: {},
|
|
})) as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
lines: [
|
|
"Inference verified: claude-cli/claude-opus-4-8",
|
|
"Inference setup completed, but OpenClaw could not record its audit entry: audit directory is read-only",
|
|
],
|
|
});
|
|
expect(error).toHaveBeenCalledWith(
|
|
"Inference setup completed, but OpenClaw could not record its audit entry: audit directory is read-only",
|
|
);
|
|
});
|
|
|
|
it("lets an enclosing persistent operation own the setup audit", async () => {
|
|
const configHarness = createConfigTransformHarness();
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
recordSetupAudit: false,
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
lines: ["Inference verified: claude-cli/claude-opus-4-8"],
|
|
});
|
|
expect(mocks.appendAudit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("persists inference only after the live test succeeds", async () => {
|
|
const initialConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir: "/tmp/openclaw-ops-agent",
|
|
params: { temperature: 0.2 },
|
|
tools: { allow: ["read"], deny: ["exec"] },
|
|
},
|
|
{
|
|
id: "openclaw",
|
|
params: { temperature: 1.7 },
|
|
tools: { allow: ["exec"] },
|
|
},
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const configHarness = createConfigTransformHarness(initialConfig);
|
|
const runCliAgent = vi.fn(successfulRunner("claude-cli", "claude-opus-4-8"));
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
runCliAgent: runCliAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.modelRef).toBe("claude-cli/claude-opus-4-8");
|
|
expect(result.lines).toEqual(["Inference verified: claude-cli/claude-opus-4-8"]);
|
|
}
|
|
expect(runCliAgent).toHaveBeenCalledOnce();
|
|
expect(runCliAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentId: "openclaw",
|
|
agentDir: "/tmp/openclaw-ops-agent",
|
|
executionMode: "side-question",
|
|
disableTools: true,
|
|
cleanupCliLiveSessionOnRunEnd: true,
|
|
}),
|
|
);
|
|
const probeConfig = runCliAgent.mock.calls[0]?.[0].config;
|
|
expect(probeConfig?.agents?.list?.find((agent) => agent.id === "openclaw")).toEqual({
|
|
id: "openclaw",
|
|
params: { temperature: 0.2 },
|
|
tools: { allow: ["read"], deny: ["exec"] },
|
|
});
|
|
expect(configHarness.current().agents?.list?.find((agent) => agent.id === "openclaw")).toEqual({
|
|
id: "openclaw",
|
|
params: { temperature: 1.7 },
|
|
tools: { allow: ["exec"] },
|
|
});
|
|
expect(configHarness.transform).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects an unattested successful candidate before persisting its model", async () => {
|
|
const configHarness = createConfigTransformHarness();
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: vi.fn(async () => successfulRun("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("did not report an owner"),
|
|
});
|
|
expect(configHarness.transform).not.toHaveBeenCalled();
|
|
expect(configHarness.current()).toEqual({});
|
|
});
|
|
|
|
it("rejects an unattested existing route before handing off to OpenClaw", async () => {
|
|
const config = {
|
|
agents: { defaults: { model: "openai/gpt-5.5" } },
|
|
} satisfies OpenClawConfig;
|
|
const configHarness = createConfigTransformHarness();
|
|
const result = await activateSetupInference({
|
|
kind: "existing-model",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({ exists: true, valid: true, config })) as never,
|
|
runEmbeddedAgent: vi.fn(async () => successfulRun("openai", "gpt-5.5")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("did not report an owner"),
|
|
});
|
|
expect(configHarness.transform).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps a committed success when temporary cleanup fails", async () => {
|
|
const configHarness = createConfigTransformHarness();
|
|
const runtimeLog = vi.fn();
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime: { log: runtimeLog, error: () => {}, exit: () => {} } as never,
|
|
deps: {
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: async () => "/tmp/openclaw-setup-cleanup-fixture",
|
|
removeTempDir: async () => {
|
|
throw new Error("simulated cleanup failure");
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "claude-cli/claude-opus-4-8" });
|
|
expect(runtimeLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("disposes the temporary auth database before Windows-style removal", async () => {
|
|
const tempDir = await makeTempDir();
|
|
const databasePath = path.join(tempDir, "agent", "openclaw-agent.sqlite");
|
|
let disposed = false;
|
|
const disposeDatabase = vi.fn((pathname: string) => {
|
|
expect(pathname).toBe(databasePath);
|
|
disposed = disposeOpenClawAgentDatabaseByPath(pathname);
|
|
return disposed;
|
|
});
|
|
const removeTempDir = vi.fn(async (dir: string) => {
|
|
if (!disposed) {
|
|
const error = new Error("file is in use") as NodeJS.ErrnoException;
|
|
error.code = "EBUSY";
|
|
throw error;
|
|
}
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "temporary-plaintext-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(async () => {
|
|
throw new Error("401 invalid_api_key");
|
|
}) as never,
|
|
disposeOpenClawAgentDatabaseByPath: disposeDatabase,
|
|
createTempDir: async () => tempDir,
|
|
removeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
expect(disposeDatabase).toHaveBeenCalledOnce();
|
|
expect(removeTempDir).toHaveBeenCalledWith(tempDir);
|
|
await expect(fs.stat(tempDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("reconciles a config write that committed before its writer threw", async () => {
|
|
let committedConfig: OpenClawConfig | undefined;
|
|
const readConfigFileSnapshot = vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: committedConfig ?? {},
|
|
runtimeConfig: committedConfig ?? {},
|
|
}));
|
|
const transformConfig = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: { snapshot: { config: OpenClawConfig; runtimeConfig: OpenClawConfig } },
|
|
) => Promise<{ nextConfig: OpenClawConfig }>;
|
|
}) => {
|
|
committedConfig = (
|
|
await params.transform({}, { snapshot: { config: {}, runtimeConfig: {} } })
|
|
).nextConfig;
|
|
throw new Error("simulated post-write failure");
|
|
},
|
|
);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "claude-cli/claude-opus-4-8" });
|
|
expect(committedConfig?.agents?.defaults?.model).toBe("claude-cli/claude-opus-4-8");
|
|
});
|
|
|
|
it("persists only the verified model before OpenClaw configures the rest", async () => {
|
|
const configHarness = createConfigTransformHarness();
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
workspace: "/tmp/not-persisted-yet",
|
|
surface: "cli",
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
modelRef: "claude-cli/claude-opus-4-8",
|
|
lines: ["Inference verified: claude-cli/claude-opus-4-8"],
|
|
});
|
|
const persistedConfig = configHarness.current();
|
|
expect(persistedConfig.agents?.defaults?.model).toBe("claude-cli/claude-opus-4-8");
|
|
expect(persistedConfig.agents?.defaults?.workspace).toBeUndefined();
|
|
expect(persistedConfig.gateway).toBeUndefined();
|
|
});
|
|
|
|
it("rechecks the exact provider model and activates it without storing credentials", async () => {
|
|
const modelRef = "lmstudio/qwen-local";
|
|
const detect = vi.fn(async () => ({ modelRef, detail: "qwen-local at localhost" }));
|
|
const prepare = vi.fn(async () => ({
|
|
profiles: [],
|
|
defaultModel: modelRef,
|
|
configPatch: {
|
|
models: {
|
|
mode: "merge" as const,
|
|
providers: {
|
|
lmstudio: {
|
|
baseUrl: "http://127.0.0.1:1234/v1",
|
|
api: "openai-completions" as const,
|
|
models: [
|
|
{
|
|
id: "qwen-local",
|
|
name: "Qwen Local",
|
|
reasoning: false,
|
|
input: ["text" as const],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 32768,
|
|
maxTokens: 4096,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
const provider: ProviderPlugin = {
|
|
id: "lmstudio",
|
|
label: "LM Studio",
|
|
pluginId: "lmstudio",
|
|
auth: [
|
|
{
|
|
id: "custom",
|
|
label: "LM Studio",
|
|
kind: "custom",
|
|
appGuidedSetup: { detect, prepare },
|
|
run: async () => ({ profiles: [] }),
|
|
},
|
|
],
|
|
};
|
|
const configHarness = createConfigTransformHarness();
|
|
const updateAuthStore = vi.fn();
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "provider-auto:lmstudio",
|
|
modelRef,
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "lmstudio",
|
|
providerId: "lmstudio",
|
|
methodId: "custom",
|
|
choiceId: "lmstudio",
|
|
choiceLabel: "LM Studio",
|
|
appGuidedDiscovery: true,
|
|
}),
|
|
resolvePluginProviders: () => [provider],
|
|
runEmbeddedAgent: vi.fn(successfulRunner("lmstudio", "qwen-local")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
updateAuthProfileStoreWithLock: updateAuthStore as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef });
|
|
expect(detect).not.toHaveBeenCalled();
|
|
expect(prepare).toHaveBeenCalledOnce();
|
|
expect(updateAuthStore).not.toHaveBeenCalled();
|
|
expect(configHarness.current()).toMatchObject({
|
|
agents: { defaults: { model: modelRef } },
|
|
models: {
|
|
providers: {
|
|
lmstudio: {
|
|
baseUrl: "http://127.0.0.1:1234/v1",
|
|
models: [expect.objectContaining({ id: "qwen-local" })],
|
|
},
|
|
},
|
|
},
|
|
plugins: { entries: { lmstudio: { enabled: true } } },
|
|
});
|
|
});
|
|
|
|
it("rebases model persistence on concurrent default-agent edits", async () => {
|
|
const probedConfig: OpenClawConfig = {
|
|
agents: { list: [{ id: "work", default: true, model: "openai/broken" }] },
|
|
};
|
|
const concurrentConfig: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{ id: "work", default: true, model: "openai/broken", name: "edited during probe" },
|
|
{ id: "new-agent", model: "anthropic/claude-opus-4-8" },
|
|
],
|
|
},
|
|
};
|
|
const configHarness = createConfigTransformHarness(concurrentConfig);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "cli",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: probedConfig,
|
|
})) as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
const persistedConfig = configHarness.current();
|
|
expect(persistedConfig.agents?.list).toEqual([
|
|
{
|
|
id: "work",
|
|
default: true,
|
|
model: "claude-cli/claude-opus-4-8",
|
|
name: "edited during probe",
|
|
models: { "claude-cli/claude-opus-4-8": {} },
|
|
},
|
|
{ id: "new-agent", model: "anthropic/claude-opus-4-8" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "default model",
|
|
concurrent: {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir: "/tmp/ops",
|
|
model: "anthropic/claude-opus-4-8",
|
|
},
|
|
{ id: "other", agentDir: "/tmp/other", model: "openai/broken" },
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
},
|
|
{
|
|
name: "default agent",
|
|
concurrent: {
|
|
agents: {
|
|
list: [
|
|
{ id: "ops", agentDir: "/tmp/ops", model: "openai/broken" },
|
|
{ id: "other", default: true, agentDir: "/tmp/other", model: "openai/broken" },
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
},
|
|
{
|
|
name: "default agent directory",
|
|
concurrent: {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir: "/tmp/ops-moved",
|
|
model: "openai/broken",
|
|
},
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
},
|
|
{
|
|
name: "default agent execution settings",
|
|
concurrent: {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir: "/tmp/ops",
|
|
model: "openai/broken",
|
|
params: { temperature: 0.9 },
|
|
tools: { deny: ["exec"] },
|
|
},
|
|
{ id: "other", agentDir: "/tmp/other", model: "openai/broken" },
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
},
|
|
])("rejects a changed $name after the live probe", async ({ concurrent }) => {
|
|
const probedConfig = {
|
|
agents: {
|
|
list: [
|
|
{ id: "ops", default: true, agentDir: "/tmp/ops", model: "openai/broken" },
|
|
{ id: "other", agentDir: "/tmp/other", model: "openai/broken" },
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const configHarness = createConfigTransformHarness(concurrent);
|
|
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "cli",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: probedConfig,
|
|
runtimeConfig: probedConfig,
|
|
})) as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("route changed during its live test");
|
|
|
|
expect(configHarness.current()).toEqual(concurrent);
|
|
});
|
|
|
|
it("rejects a concurrent edit to inactive target-model metadata", async () => {
|
|
const initialConfig = {
|
|
agents: {
|
|
defaults: {
|
|
model: "openai/gpt-5.4",
|
|
models: {
|
|
"anthropic/claude-opus-4-8": { agentRuntime: { id: "openclaw" } },
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const concurrentConfig = structuredClone(initialConfig);
|
|
concurrentConfig.agents!.defaults!.models!["anthropic/claude-opus-4-8"] = {
|
|
agentRuntime: { id: "codex" },
|
|
};
|
|
const configHarness = createConfigTransformHarness(concurrentConfig);
|
|
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("anthropic", "claude-opus-4-8")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("target model metadata changed");
|
|
|
|
expect(configHarness.current()).toEqual(concurrentConfig);
|
|
});
|
|
|
|
it("preserves authored provider rows instead of runtime-materialized metadata", async () => {
|
|
const sourceConfig = {
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
models: [
|
|
{
|
|
id: "gpt-5.6",
|
|
name: "GPT-5.6 authored",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 0.2 },
|
|
contextWindow: 200_000,
|
|
maxTokens: 64_000,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const runtimeConfig: OpenClawConfig = structuredClone(sourceConfig);
|
|
runtimeConfig.models!.providers!.openai!.models = [
|
|
{
|
|
id: "gpt-5.6",
|
|
name: "GPT-5.6",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 272_000,
|
|
maxTokens: 128_000,
|
|
},
|
|
];
|
|
const configHarness = createConfigTransformHarness(sourceConfig, runtimeConfig);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "openai-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: sourceConfig,
|
|
sourceConfig,
|
|
runtimeConfig,
|
|
})) as never,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.6" });
|
|
expect(configHarness.current().models?.providers?.openai?.models).toEqual(
|
|
sourceConfig.models.providers.openai.models,
|
|
);
|
|
});
|
|
|
|
it("rejects an existing route that changes after its live probe", async () => {
|
|
const initialConfig = {
|
|
agents: { defaults: { model: "openai/gpt-5.5" } },
|
|
} satisfies OpenClawConfig;
|
|
const changedConfig = {
|
|
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
|
|
} satisfies OpenClawConfig;
|
|
const readConfigFileSnapshot = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ exists: true, valid: true, config: initialConfig })
|
|
.mockResolvedValueOnce({ exists: true, valid: true, config: changedConfig });
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "existing-model",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.5")) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("route changed during its live test"),
|
|
});
|
|
});
|
|
|
|
it("revalidates a stable CLI runtime owner at the config commit boundary", async () => {
|
|
const configHarness = createConfigTransformHarness();
|
|
const resolveCliRuntimeOwnerFingerprint = vi.fn(async () => "test-runtime-owner");
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
resolveCliRuntimeOwnerFingerprint: resolveCliRuntimeOwnerFingerprint as never,
|
|
fingerprintPluginRuntimeArtifact: ({ pluginId }) => `${pluginId}-runtime-v1`,
|
|
createSystemAgentVerifiedInferenceBinding,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true });
|
|
expect(resolveCliRuntimeOwnerFingerprint).toHaveBeenCalledOnce();
|
|
expect(configHarness.current().agents?.defaults?.model).toBe("claude-cli/claude-opus-4-8");
|
|
});
|
|
|
|
it("rejects a CLI owner drift on an existing route before handoff", async () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: "claude-cli/claude-opus-4-8",
|
|
cliBackends: {
|
|
"claude-cli": { command: "claude" },
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const result = await activateSetupInference({
|
|
kind: "existing-model",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config,
|
|
runtimeConfig: config,
|
|
})) as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
resolveCliRuntimeOwnerFingerprint: vi.fn(async () => "changed-runtime-owner") as never,
|
|
createSystemAgentVerifiedInferenceBinding,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "auth",
|
|
error: expect.stringContaining("owner changed"),
|
|
});
|
|
});
|
|
|
|
it("rejects ambient credential drift before persisting a model", async () => {
|
|
const initialAuthFingerprint = fingerprintResolvedProviderAuth({
|
|
apiKey: "initial-env-key",
|
|
source: "env:ANTHROPIC_API_KEY",
|
|
mode: "api-key",
|
|
});
|
|
if (!initialAuthFingerprint) {
|
|
throw new Error("expected auth fingerprint");
|
|
}
|
|
const configHarness = createConfigTransformHarness();
|
|
const runEmbeddedAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
params.onSuccessfulAuthBinding?.({
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint: initialAuthFingerprint,
|
|
});
|
|
return successfulRun("anthropic", "claude-opus-4-8");
|
|
});
|
|
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
resolveApiKeyForProvider: vi.fn(async () => ({
|
|
apiKey: "rotated-env-key",
|
|
source: "env:ANTHROPIC_API_KEY",
|
|
mode: "api-key",
|
|
})) as never,
|
|
createSystemAgentVerifiedInferenceBinding,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("active route owner");
|
|
expect(configHarness.transform).toHaveBeenCalledOnce();
|
|
expect(configHarness.current()).toEqual({});
|
|
});
|
|
|
|
it("does not configure Codex while selecting Claude as the primary backend", async () => {
|
|
const sourceConfig = {} satisfies OpenClawConfig;
|
|
const configHarness = createConfigTransformHarness(sourceConfig);
|
|
const ensureCodexRuntimePlugin = vi.fn();
|
|
const runCliAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
expect(configHarness.transform).not.toHaveBeenCalled();
|
|
return successfulRun("claude-cli", "claude-opus-4-8", params);
|
|
});
|
|
const refreshPluginRegistry = vi.fn(async () => {});
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
sourceConfig,
|
|
config: sourceConfig,
|
|
runtimeConfig: sourceConfig,
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: ensureCodexRuntimePlugin as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
runCliAgent: runCliAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(runCliAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({ provider: "claude-cli", model: "claude-opus-4-8" }),
|
|
);
|
|
expect(ensureCodexRuntimePlugin).not.toHaveBeenCalled();
|
|
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
|
expect(configHarness.transform).toHaveBeenCalledOnce();
|
|
expect(configHarness.current()).toMatchObject({
|
|
agents: { defaults: { model: "claude-cli/claude-opus-4-8" } },
|
|
});
|
|
expect(configHarness.current().plugins?.entries?.codex).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"an explicitly disabled Codex plugin",
|
|
{ plugins: { entries: { codex: { enabled: false } } } } satisfies OpenClawConfig,
|
|
],
|
|
[
|
|
"an explicit supervision opt-out",
|
|
{
|
|
plugins: {
|
|
entries: { codex: { config: { supervision: { enabled: false } } } },
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
],
|
|
["plugin policy", { plugins: { deny: ["codex"] } } satisfies OpenClawConfig],
|
|
])("preserves %s while selecting another backend", async (_label, config) => {
|
|
const ensureCodexRuntimePlugin = vi.fn();
|
|
const configHarness = createConfigTransformHarness(config);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
sourceConfig: config,
|
|
config,
|
|
runtimeConfig: config,
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: ensureCodexRuntimePlugin as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
runCliAgent: vi.fn(successfulRunner("claude-cli", "claude-opus-4-8")) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(ensureCodexRuntimePlugin).not.toHaveBeenCalled();
|
|
expect(configHarness.transform).toHaveBeenCalledOnce();
|
|
expect(configHarness.current()).toMatchObject(config);
|
|
expect(configHarness.current()).toMatchObject({
|
|
agents: { defaults: { model: "claude-cli/claude-opus-4-8" } },
|
|
});
|
|
});
|
|
|
|
it("does not touch config when the live test fails", async () => {
|
|
const providerSecret = "gsk_abcdefghijklmnop";
|
|
const transformConfig = vi.fn();
|
|
const runCliAgent = vi.fn(async () => {
|
|
throw new Error(`401 invalid_api_key ${providerSecret}`);
|
|
});
|
|
const result = await activateSetupInference({
|
|
kind: "claude-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: runCliAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error).toContain("invalid_api_key");
|
|
expect(result.error).not.toContain(providerSecret);
|
|
}
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("treats an empty model reply as a failure with bounded probe identifiers", async () => {
|
|
const transformConfig = vi.fn();
|
|
const runEmbeddedAgent = vi.fn(async (_params: { runId?: string; sessionId?: string }) => ({
|
|
payloads: [],
|
|
}));
|
|
const result = await activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
expect(result).toMatchObject({ ok: false, status: "format" });
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
runId: expect.stringMatching(/^probe-setup-inference-/),
|
|
sessionId: expect.stringMatching(/^probe-setup-inference-/),
|
|
sessionKey: expect.stringMatching(/^temp:setup-inference:probe-setup-inference-/),
|
|
lane: "session:probe-setup-inference:anthropic",
|
|
}),
|
|
);
|
|
const probeCall = runEmbeddedAgent.mock.calls[0]?.[0];
|
|
expect(probeCall).toBeDefined();
|
|
expect(probeCall?.sessionId).toBe(probeCall?.runId);
|
|
expect(probeCall?.sessionId).toHaveLength(58);
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "error payload",
|
|
result: {
|
|
payloads: [{ text: "Blocked by before-run policy.", isError: true }],
|
|
meta: { finalAssistantVisibleText: "Blocked by before-run policy." },
|
|
},
|
|
},
|
|
{
|
|
name: "terminal metadata error",
|
|
result: {
|
|
payloads: [{ text: "Agent could not complete the turn." }],
|
|
meta: {
|
|
finalAssistantVisibleText: "Agent could not complete the turn.",
|
|
error: { kind: "incomplete_turn", message: "Agent could not complete the turn." },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "blocked liveness state",
|
|
result: {
|
|
payloads: [{ text: "Run stopped before completion." }],
|
|
meta: {
|
|
finalAssistantVisibleText: "Run stopped before completion.",
|
|
livenessState: "blocked",
|
|
},
|
|
},
|
|
},
|
|
])("does not persist inference for a non-throwing $name", async ({ result: runResult }) => {
|
|
const transformConfig = vi.fn();
|
|
const result = await activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runEmbeddedAgent: vi.fn(async () => runResult) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "unknown" });
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "missing winner metadata",
|
|
runResult: { meta: { finalAssistantVisibleText: "OK" } },
|
|
error: "did not report which provider and model",
|
|
},
|
|
{
|
|
name: "model-routing override",
|
|
runResult: successfulRun("openai", "gpt-5.5"),
|
|
error: "instead of the requested anthropic/claude-opus-4-8",
|
|
},
|
|
])("does not persist inference after a $name", async ({ runResult, error }) => {
|
|
const transformConfig = vi.fn();
|
|
const result = await activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
runEmbeddedAgent: vi.fn(async () => runResult) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "format",
|
|
error: expect.stringContaining(error),
|
|
});
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "provider-level CLI runtime",
|
|
providerConfig: {
|
|
baseUrl: "https://api.anthropic.com",
|
|
models: [],
|
|
agentRuntime: { id: "claude-cli" as const },
|
|
},
|
|
},
|
|
{
|
|
name: "model-definition CLI runtime",
|
|
providerConfig: {
|
|
baseUrl: "https://api.anthropic.com",
|
|
models: [
|
|
{
|
|
id: "claude-opus-4-8",
|
|
name: "Claude Opus 4.8",
|
|
reasoning: true,
|
|
input: ["text" as const],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200_000,
|
|
maxTokens: 8192,
|
|
agentRuntime: { id: "claude-cli" as const },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
])("pins a built-in API candidate over a stale $name", async ({ providerConfig }) => {
|
|
const initialConfig = {
|
|
models: { providers: { anthropic: providerConfig } },
|
|
agents: {
|
|
defaults: { model: { primary: "openai/gpt-5.4" } },
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
model: { primary: "openai/gpt-5.4" },
|
|
},
|
|
],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const runEmbeddedAgent = vi.fn(successfulRunner("anthropic", "claude-opus-4-8"));
|
|
const configHarness = createConfigTransformHarness(initialConfig);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "anthropic-api-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "anthropic/claude-opus-4-8" });
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentId: "openclaw",
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-8",
|
|
agentHarnessRuntimeOverride: "openclaw",
|
|
config: expect.objectContaining({
|
|
agents: expect.objectContaining({
|
|
list: [
|
|
expect.objectContaining({
|
|
id: "ops",
|
|
model: { primary: "anthropic/claude-opus-4-8" },
|
|
models: {
|
|
"anthropic/claude-opus-4-8": {
|
|
agentRuntime: { id: "openclaw" },
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
expect(configHarness.transform).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects manual activation without a supported provider", async () => {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "definitely-not-a-provider",
|
|
apiKey: "sk-test",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
createTempDir: makeTempDir,
|
|
resolveManifestProviderAuthChoice: () => undefined,
|
|
resolvePluginProviders: () => [],
|
|
},
|
|
});
|
|
expect(result).toMatchObject({ ok: false, status: "unavailable" });
|
|
});
|
|
|
|
it("persists provider OAuth when runtime defaults are absent from source config", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
const runtimeConfig = {
|
|
agents: {
|
|
...initialConfig.agents,
|
|
defaults: { models: { "openai/gpt-5.4": {} } },
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const runAuth = vi.fn(async () => ({
|
|
profiles: [
|
|
{
|
|
profileId: "openai:default",
|
|
credential: {
|
|
type: "oauth" as const,
|
|
provider: "openai",
|
|
access: "access-token",
|
|
refresh: "refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
},
|
|
},
|
|
],
|
|
defaultModel: "openai/gpt-5.5",
|
|
configPatch: {
|
|
agents: { defaults: { models: { "openai/gpt-5.5": {} } } },
|
|
},
|
|
}));
|
|
const provider: ProviderPlugin = {
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
pluginId: "openai",
|
|
auth: [{ id: "oauth", label: "OAuth", kind: "oauth", run: runAuth }],
|
|
};
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: SuccessfulRunParams & { authProfileId?: string }) =>
|
|
successfulRun("openai", "gpt-5.5", params),
|
|
);
|
|
const configHarness = createConfigTransformHarness(initialConfig, runtimeConfig);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "provider-auth",
|
|
authChoice: "openai",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
prompter: { note: vi.fn(async () => {}) } as never,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "openai",
|
|
providerId: "openai",
|
|
methodId: "oauth",
|
|
choiceId: "openai",
|
|
choiceLabel: "ChatGPT Login",
|
|
appGuidedAuth: "oauth",
|
|
}),
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.5" });
|
|
expect(runAuth).toHaveBeenCalledOnce();
|
|
const activatedProfileId = runEmbeddedAgent.mock.calls[0]?.[0].authProfileId;
|
|
if (!activatedProfileId) {
|
|
throw new Error("expected setup auth profile");
|
|
}
|
|
expect(activatedProfileId).toMatch(/^openai:setup-/);
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles[activatedProfileId]).toMatchObject({
|
|
type: "oauth",
|
|
provider: "openai",
|
|
access: "access-token",
|
|
});
|
|
expect(configHarness.current()).toMatchObject({
|
|
agents: { defaults: { model: `openai/gpt-5.5@${activatedProfileId}` } },
|
|
});
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("does not probe or persist an interactive login after session cancellation", async () => {
|
|
const runAuth = vi.fn(async () => ({ profiles: [], defaultModel: "openai/gpt-5.5" }));
|
|
const runEmbeddedAgent = vi.fn();
|
|
const provider: ProviderPlugin = {
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
pluginId: "openai",
|
|
auth: [{ id: "oauth", label: "OAuth", kind: "oauth", run: runAuth }],
|
|
};
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "provider-auth",
|
|
authChoice: "openai",
|
|
surface: "gateway",
|
|
runtime,
|
|
prompter: {} as never,
|
|
isCancelled: () => true,
|
|
deps: {
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "openai",
|
|
providerId: "openai",
|
|
methodId: "oauth",
|
|
choiceId: "openai",
|
|
choiceLabel: "ChatGPT Login",
|
|
appGuidedAuth: "oauth",
|
|
}),
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, error: "Provider login was cancelled." });
|
|
expect(runAuth).not.toHaveBeenCalled();
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ name: "API-key", authKind: "api_key" as const, credentialType: "api_key" as const },
|
|
{ name: "token", authKind: "token" as const, credentialType: "token" as const },
|
|
])(
|
|
"uses a provider-owned $name method and persists it after a passing test",
|
|
async ({ authKind, credentialType }) => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
auth: {
|
|
profiles: {
|
|
"groq:legacy": { provider: "groq", mode: credentialType },
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
// Custom agent directories must be bound to their configured owner before
|
|
// the shared per-agent database is created.
|
|
resolveAgentDir(initialConfig, "main");
|
|
await upsertAuthProfileWithLock({
|
|
profileId: "groq:legacy",
|
|
credential:
|
|
credentialType === "api_key"
|
|
? { type: "api_key", provider: "groq", key: "legacy-key" }
|
|
: { type: "token", provider: "groq", token: "legacy-key" },
|
|
agentDir,
|
|
});
|
|
await updateAuthProfileStoreWithLock({
|
|
agentDir,
|
|
updater: (store) => {
|
|
store.order = { groq: ["groq:legacy"] };
|
|
return true;
|
|
},
|
|
});
|
|
const runAuth = vi.fn(async (ctx: { opts?: { token?: string } }) => ({
|
|
profiles: [
|
|
{
|
|
profileId: "groq:default",
|
|
credential:
|
|
credentialType === "api_key"
|
|
? { type: "api_key" as const, provider: "groq", key: ctx.opts?.token }
|
|
: { type: "token" as const, provider: "groq", token: ctx.opts?.token ?? "" },
|
|
},
|
|
],
|
|
defaultModel: "groq/llama-3.3-70b-versatile",
|
|
configPatch: { agents: { defaults: { models: { "groq/llama-3.3-70b-versatile": {} } } } },
|
|
}));
|
|
const provider: ProviderPlugin = {
|
|
id: "groq",
|
|
label: "Groq",
|
|
pluginId: "groq",
|
|
auth: [
|
|
{
|
|
id: "api-key",
|
|
label: "Groq API key",
|
|
kind: authKind,
|
|
wizard: { choiceId: "groq-api-key" },
|
|
run: runAuth as never,
|
|
},
|
|
],
|
|
};
|
|
const resolvePluginProviders = vi.fn(() => [provider]);
|
|
const enablePluginInConfig = vi.fn((config: OpenClawConfig, pluginId: string) => ({
|
|
config: {
|
|
...config,
|
|
plugins: { entries: { [pluginId]: { enabled: true } } },
|
|
},
|
|
enabled: true,
|
|
}));
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: SuccessfulRunParams & { authProfileId?: string }) =>
|
|
successfulRun("groq", "llama-3.3-70b-versatile", params),
|
|
);
|
|
const configHarness = createConfigTransformHarness(initialConfig);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "test-groq-key",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders,
|
|
enablePluginInConfig: enablePluginInConfig as never,
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "groq",
|
|
providerId: "groq",
|
|
methodId: "api-key",
|
|
choiceId: "groq-api-key",
|
|
choiceLabel: "Groq API key",
|
|
appGuidedSecret: true,
|
|
}),
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "groq/llama-3.3-70b-versatile" });
|
|
expect(resolvePluginProviders).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: expect.objectContaining({
|
|
plugins: { entries: { groq: { enabled: true } } },
|
|
}),
|
|
onlyPluginIds: ["groq"],
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
}),
|
|
);
|
|
expect(runAuth).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
opts: expect.objectContaining({ token: "test-groq-key", tokenProvider: "groq" }),
|
|
allowSecretRefPrompt: false,
|
|
secretInputMode: "plaintext",
|
|
}),
|
|
);
|
|
const activatedProfileId = runEmbeddedAgent.mock.calls[0]?.[0].authProfileId;
|
|
if (!activatedProfileId) {
|
|
throw new Error("expected setup auth profile");
|
|
}
|
|
expect(activatedProfileId).toMatch(/^groq:setup-/);
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentId: "openclaw",
|
|
provider: "groq",
|
|
model: "llama-3.3-70b-versatile",
|
|
authProfileId: activatedProfileId,
|
|
agentDir: expect.stringContaining("setup-inference-test-"),
|
|
authProfileStateMode: "read-only",
|
|
}),
|
|
);
|
|
expect(configHarness.current()).toMatchObject({
|
|
plugins: { entries: { groq: { enabled: true } } },
|
|
agents: {
|
|
defaults: {
|
|
model: `groq/llama-3.3-70b-versatile@${activatedProfileId}`,
|
|
},
|
|
},
|
|
auth: {
|
|
profiles: {
|
|
[activatedProfileId]: { provider: "groq", mode: credentialType },
|
|
},
|
|
},
|
|
});
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles[activatedProfileId]).toMatchObject(
|
|
credentialType === "api_key"
|
|
? { type: "api_key", provider: "groq", key: "test-groq-key" }
|
|
: { type: "token", provider: "groq", token: "test-groq-key" },
|
|
);
|
|
expect(readAuthProfileStoreForTest(agentDir).order?.groq).toEqual(["groq:legacy"]);
|
|
expect(
|
|
(await resolveSystemAgentConfiguredRouteFromConfig(configHarness.current()))
|
|
?.authProfileId,
|
|
).toBe(activatedProfileId);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("rejects a manual probe that reports a different credential owner", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const transformConfig = vi.fn();
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: SuccessfulRunParams & { authProfileId?: string }) => {
|
|
params.onSuccessfulAuthBinding?.({
|
|
authProfileId: "groq:fallback",
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint: "fallback-owner",
|
|
});
|
|
return successfulRun("groq", "llama-3.3-70b-versatile");
|
|
},
|
|
);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "auth",
|
|
error: expect.stringContaining('used profile "groq:fallback"'),
|
|
});
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles).toEqual({});
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("scopes provider setup to the selected inference route and one credential", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
gateway: { port: 18_789 },
|
|
channels: { discord: { enabled: false } },
|
|
agents: {
|
|
defaults: { workspace: "/operator/workspace" },
|
|
list: [{ id: "main", default: true, agentDir }],
|
|
},
|
|
auth: {
|
|
profiles: { "operator:existing": { provider: "operator", mode: "api_key" } },
|
|
order: { operator: ["operator:existing"] },
|
|
},
|
|
models: {
|
|
providers: {
|
|
aux: { baseUrl: "https://aux.example.test/v1", models: [] },
|
|
},
|
|
},
|
|
plugins: {
|
|
entries: { operator: { enabled: true, config: { revision: "initial" } } },
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const concurrentConfig = structuredClone(initialConfig);
|
|
concurrentConfig.gateway = { port: 19_000 };
|
|
concurrentConfig.agents!.defaults!.workspace = "/operator/concurrent";
|
|
concurrentConfig.models!.providers!.aux!.baseUrl = "https://concurrent.example.test/v1";
|
|
resolveAgentDir(initialConfig, "main");
|
|
const provider: ProviderPlugin = {
|
|
id: "groq",
|
|
label: "Groq",
|
|
pluginId: "groq",
|
|
auth: [
|
|
{
|
|
id: "api-key",
|
|
label: "Groq API key",
|
|
kind: "api_key",
|
|
wizard: { choiceId: "groq-api-key" },
|
|
run: async (ctx) => ({
|
|
profiles: [
|
|
{
|
|
profileId: "groq:selected",
|
|
credential: {
|
|
type: "api_key" as const,
|
|
provider: "groq",
|
|
key: ctx.opts?.token,
|
|
},
|
|
},
|
|
{
|
|
profileId: "other:unselected",
|
|
credential: {
|
|
type: "api_key" as const,
|
|
provider: "other",
|
|
key: "must-not-persist",
|
|
},
|
|
},
|
|
],
|
|
defaultModel: "groq/llama-3.3-70b-versatile",
|
|
configPatch: {
|
|
gateway: { port: 99_999 },
|
|
channels: { discord: { enabled: true, token: "must-not-persist" } },
|
|
agents: {
|
|
defaults: {
|
|
workspace: "/provider/workspace",
|
|
models: {
|
|
"groq/llama-3.3-70b-versatile": { alias: "selected-groq" },
|
|
"other/unrelated": { alias: "must-not-persist" },
|
|
},
|
|
},
|
|
},
|
|
auth: { order: { other: ["other:unselected"] } },
|
|
models: {
|
|
providers: {
|
|
groq: {
|
|
baseUrl: "https://selected.groq.example.test/v1",
|
|
models: [],
|
|
},
|
|
aux: { baseUrl: "https://provider-overwrite.example.test/v1", models: [] },
|
|
},
|
|
},
|
|
plugins: {
|
|
entries: {
|
|
groq: { enabled: true, config: { endpoint: "selected" } },
|
|
operator: { enabled: false, config: { revision: "provider-overwrite" } },
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
const enablePluginInConfig = (config: OpenClawConfig, pluginId: string) => ({
|
|
enabled: true as const,
|
|
config: {
|
|
...config,
|
|
plugins: {
|
|
...config.plugins,
|
|
entries: {
|
|
...config.plugins?.entries,
|
|
[pluginId]: { ...config.plugins?.entries?.[pluginId], enabled: true },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: SuccessfulRunParams & { config: OpenClawConfig }) =>
|
|
successfulRun("groq", "llama-3.3-70b-versatile", params),
|
|
);
|
|
const configHarness = createConfigTransformHarness(concurrentConfig);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "selected-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
enablePluginInConfig: enablePluginInConfig as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true });
|
|
const probeConfig = runEmbeddedAgent.mock.calls[0]![0].config;
|
|
expect(probeConfig.gateway?.port).toBe(18_789);
|
|
expect(probeConfig.agents?.defaults?.workspace).toBe("/operator/workspace");
|
|
expect(probeConfig.channels?.discord).toEqual({ enabled: false });
|
|
expect(probeConfig.models?.providers?.groq?.baseUrl).toBe(
|
|
"https://selected.groq.example.test/v1",
|
|
);
|
|
expect(probeConfig.models?.providers?.aux?.baseUrl).toBe("https://aux.example.test/v1");
|
|
expect(probeConfig.agents?.defaults?.models).toEqual({
|
|
"groq/llama-3.3-70b-versatile": {
|
|
alias: "selected-groq",
|
|
},
|
|
});
|
|
expect(probeConfig.agents?.list?.[0]?.models).toMatchObject({
|
|
"groq/llama-3.3-70b-versatile": { agentRuntime: { id: "openclaw" } },
|
|
});
|
|
expect(probeConfig.plugins?.entries?.groq).toEqual({
|
|
enabled: true,
|
|
config: { endpoint: "selected" },
|
|
});
|
|
expect(probeConfig.plugins?.entries?.operator).toEqual(
|
|
initialConfig.plugins.entries.operator,
|
|
);
|
|
expect(Object.keys(probeConfig.auth?.profiles ?? {})).toEqual(
|
|
expect.arrayContaining(["operator:existing"]),
|
|
);
|
|
expect(Object.keys(probeConfig.auth?.profiles ?? {})).not.toContain("other:unselected");
|
|
expect(probeConfig.auth?.order).toEqual(initialConfig.auth.order);
|
|
|
|
const persisted = configHarness.current();
|
|
expect(persisted.gateway?.port).toBe(19_000);
|
|
expect(persisted.agents?.defaults?.workspace).toBe("/operator/concurrent");
|
|
expect(persisted.channels?.discord).toEqual({ enabled: false });
|
|
expect(persisted.models?.providers?.aux?.baseUrl).toBe("https://concurrent.example.test/v1");
|
|
expect(persisted.models?.providers?.groq?.baseUrl).toBe(
|
|
"https://selected.groq.example.test/v1",
|
|
);
|
|
expect(persisted.plugins?.entries?.operator).toEqual(initialConfig.plugins.entries.operator);
|
|
expect(persisted.plugins?.entries?.groq).toEqual({
|
|
enabled: true,
|
|
config: { endpoint: "selected" },
|
|
});
|
|
const setupProfileIds = Object.keys(persisted.auth?.profiles ?? {}).filter((id) =>
|
|
id.includes(":setup-"),
|
|
);
|
|
expect(setupProfileIds).toHaveLength(1);
|
|
expect(setupProfileIds[0]).toMatch(/^groq:setup-/);
|
|
expect(Object.keys(readAuthProfileStoreForTest(agentDir).profiles)).toEqual(setupProfileIds);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("rolls back manual auth when the real-store owner differs at commit", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const configHarness = createConfigTransformHarness(initialConfig);
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (
|
|
params: SuccessfulRunParams & {
|
|
authProfileId?: string;
|
|
},
|
|
) => {
|
|
const profileId = params.authProfileId;
|
|
if (!profileId) {
|
|
throw new Error("expected setup profile");
|
|
}
|
|
const authFingerprint = fingerprintResolvedProviderAuth({
|
|
apiKey: "submitted-key",
|
|
profileId,
|
|
source: `profile:${profileId}`,
|
|
mode: "api-key",
|
|
});
|
|
if (!authFingerprint) {
|
|
throw new Error("expected setup fingerprint");
|
|
}
|
|
params.onSuccessfulAuthBinding?.({
|
|
authProfileId: profileId,
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint,
|
|
});
|
|
return successfulRun("groq", "llama-3.3-70b-versatile");
|
|
},
|
|
);
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "submitted-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
resolveApiKeyForProvider: vi.fn(async (params: { profileId?: string }) => ({
|
|
apiKey: "different-real-store-key",
|
|
profileId: params.profileId,
|
|
source: `profile:${params.profileId}`,
|
|
mode: "api-key",
|
|
})) as never,
|
|
createSystemAgentVerifiedInferenceBinding,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("active route owner");
|
|
|
|
expect(configHarness.current()).toEqual(initialConfig);
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toEqual([]);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("rolls back a staged key when the config commit fails", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
auth: { profiles: { "groq:default": { provider: "groq", mode: "api_key" } } },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const provider: ProviderPlugin = {
|
|
id: "groq",
|
|
label: "Groq",
|
|
pluginId: "groq",
|
|
auth: [
|
|
{
|
|
id: "api-key",
|
|
label: "Groq API key",
|
|
kind: "api_key",
|
|
wizard: { choiceId: "groq-api-key" },
|
|
run: async (ctx) => ({
|
|
profiles: [
|
|
{
|
|
profileId: "groq:default",
|
|
credential: {
|
|
type: "api_key" as const,
|
|
provider: "groq",
|
|
key: ctx.opts?.token,
|
|
},
|
|
},
|
|
],
|
|
defaultModel: "groq/llama-3.3-70b-versatile",
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
await upsertAuthProfileWithLock({
|
|
profileId: "groq:default",
|
|
credential: { type: "api_key", provider: "groq", key: "existing-key" },
|
|
agentDir,
|
|
});
|
|
const transformConfig = vi.fn(async (params: { transform: Function }) => {
|
|
await params.transform(initialConfig, {
|
|
snapshot: { config: initialConfig, runtimeConfig: initialConfig },
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
throw new Error("simulated config commit failure");
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "replacement-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "groq",
|
|
providerId: "groq",
|
|
methodId: "api-key",
|
|
choiceId: "groq-api-key",
|
|
choiceLabel: "Groq API key",
|
|
appGuidedSecret: true,
|
|
}),
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("simulated config commit failure");
|
|
|
|
const store = readAuthProfileStoreForTest(agentDir);
|
|
expect(store.profiles["groq:default"]).toMatchObject({ key: "existing-key" });
|
|
expect(Object.keys(store.profiles).filter((id) => id.startsWith("groq:setup-"))).toEqual([]);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("does not delete an identical credential that existed before activation", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const credential = {
|
|
type: "api_key" as const,
|
|
provider: "groq",
|
|
key: "submitted-key",
|
|
};
|
|
let existingProfileId: string | undefined;
|
|
let preexistingCredentialDeleted = false;
|
|
let realStoreUpdates = 0;
|
|
const profiles = new Proxy<Record<string, typeof credential>>(
|
|
{},
|
|
{
|
|
get: (target, property) => {
|
|
if (typeof property === "string" && property.startsWith("groq:setup-")) {
|
|
existingProfileId ??= property;
|
|
return credential;
|
|
}
|
|
return Reflect.get(target, property);
|
|
},
|
|
deleteProperty: (target, property) => {
|
|
if (property === existingProfileId) {
|
|
preexistingCredentialDeleted = true;
|
|
}
|
|
return Reflect.deleteProperty(target, property);
|
|
},
|
|
},
|
|
);
|
|
const preexistingStore = { version: 1, profiles };
|
|
const updateAuthProfileStore = vi.fn(
|
|
async (params: Parameters<typeof updateAuthProfileStoreWithLock>[0]) => {
|
|
if (params.agentDir !== agentDir) {
|
|
return await updateAuthProfileStoreWithLock(params);
|
|
}
|
|
realStoreUpdates += 1;
|
|
params.updater(preexistingStore);
|
|
return preexistingStore;
|
|
},
|
|
);
|
|
const transformConfig = vi.fn(async () => {
|
|
throw new Error("simulated config commit failure");
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "submitted-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
updateAuthProfileStoreWithLock: updateAuthProfileStore as never,
|
|
loadPersistedAuthProfileStore: vi.fn((candidateAgentDir?: string) => {
|
|
const resolvedAgentDir = candidateAgentDir ?? resolveAgentDir(initialConfig, "main");
|
|
return resolvedAgentDir === agentDir
|
|
? preexistingStore
|
|
: readAuthProfileStoreForTest(resolvedAgentDir);
|
|
}) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("simulated config commit failure");
|
|
|
|
expect(existingProfileId).toMatch(/^groq:setup-/);
|
|
expect(realStoreUpdates).toBe(1);
|
|
expect(preexistingCredentialDeleted).toBe(false);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("rolls back before reconciliation when the config transform rejects the candidate", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
const concurrentConfig = {
|
|
...initialConfig,
|
|
agents: {
|
|
...initialConfig.agents,
|
|
defaults: { model: "openai/gpt-5.5" },
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const readConfigFileSnapshot = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})
|
|
.mockRejectedValue(new Error("simulated reconciliation read failure"));
|
|
const transformConfig = vi.fn(async (params: { transform: Function }) => {
|
|
await params.transform(concurrentConfig, {
|
|
snapshot: {
|
|
config: concurrentConfig,
|
|
sourceConfig: concurrentConfig,
|
|
runtimeConfig: concurrentConfig,
|
|
},
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
throw new Error("unreachable config commit");
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("default-agent inference route changed during its live test");
|
|
|
|
expect(readConfigFileSnapshot).toHaveBeenCalledOnce();
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toEqual([]);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("retains a credential when a post-write concurrent edit still references it", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
let currentConfig: OpenClawConfig = initialConfig;
|
|
const readConfigFileSnapshot = vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: currentConfig,
|
|
sourceConfig: currentConfig,
|
|
runtimeConfig: currentConfig,
|
|
}));
|
|
const transformConfig = vi.fn(async (params: { transform: Function }) => {
|
|
const transformed = await params.transform(initialConfig, {
|
|
snapshot: {
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
},
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
currentConfig = {
|
|
...transformed.nextConfig,
|
|
agents: {
|
|
...transformed.nextConfig.agents,
|
|
defaults: {
|
|
...transformed.nextConfig.agents?.defaults,
|
|
params: { temperature: 0.25 },
|
|
},
|
|
},
|
|
};
|
|
throw new Error("simulated post-write failure after concurrent edit");
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("credential was retained because the current config may reference it");
|
|
|
|
const profileId = Object.keys(readAuthProfileStoreForTest(agentDir).profiles).find((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
);
|
|
expect(profileId).toBeDefined();
|
|
expect(currentConfig.auth?.profiles?.[profileId!]).toMatchObject({ provider: "groq" });
|
|
expect(currentConfig.agents?.defaults?.model).toContain(`@${profileId}`);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("confirms rollback from the locked update when independent readback fails", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const transformConfig = vi.fn(async (params: { transform: Function }) => {
|
|
await params.transform(initialConfig, {
|
|
snapshot: { config: initialConfig, runtimeConfig: initialConfig },
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
throw new Error("simulated config commit failure");
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "replacement-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
loadPersistedAuthProfileStore: vi.fn(() => {
|
|
throw new Error("simulated auth read failure");
|
|
}),
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).rejects.toThrow("simulated config commit failure");
|
|
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toEqual([]);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{ name: "returns no store", failure: "null" as const },
|
|
{ name: "throws", failure: "throw" as const },
|
|
])("reports an indeterminate activation when rollback $name", async ({ failure }) => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const concurrentConfig = {
|
|
...initialConfig,
|
|
agents: {
|
|
...initialConfig.agents,
|
|
defaults: { model: "openai/gpt-5.5" },
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
let realStoreWrites = 0;
|
|
const updateAuthProfileStore = vi.fn(async (params) => {
|
|
if (params.agentDir === agentDir) {
|
|
realStoreWrites += 1;
|
|
if (realStoreWrites > 1) {
|
|
if (failure === "throw") {
|
|
throw new Error("simulated rollback write failure");
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
return await updateAuthProfileStoreWithLock(params);
|
|
});
|
|
const transformConfig = vi.fn(async (params: { transform: Function }) => {
|
|
await params.transform(concurrentConfig, {
|
|
snapshot: {
|
|
config: concurrentConfig,
|
|
sourceConfig: concurrentConfig,
|
|
runtimeConfig: concurrentConfig,
|
|
},
|
|
previousHash: null,
|
|
attempt: 0,
|
|
});
|
|
throw new Error("unreachable config commit");
|
|
});
|
|
|
|
try {
|
|
const error = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
updateAuthProfileStoreWithLock: updateAuthProfileStore as never,
|
|
loadPersistedAuthProfileStore: vi.fn(() => {
|
|
throw new Error("simulated auth read failure");
|
|
}),
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}).catch((caught: unknown) => caught);
|
|
|
|
expect(error).toBeInstanceOf(SetupInferenceActivationIndeterminateError);
|
|
expect(error).toMatchObject({
|
|
message: expect.stringContaining("could not confirm removal"),
|
|
});
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toHaveLength(1);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("rolls back an uncertain auth write without accumulating profiles on retry", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const transformConfig = vi.fn();
|
|
let realStoreUpdates = 0;
|
|
const updateAuthProfileStore = vi.fn(async (params) => {
|
|
const updated = await updateAuthProfileStoreWithLock(params);
|
|
if (params.agentDir !== agentDir) {
|
|
return updated;
|
|
}
|
|
realStoreUpdates += 1;
|
|
return realStoreUpdates % 2 === 1 ? null : updated;
|
|
});
|
|
|
|
try {
|
|
const activate = () =>
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
updateAuthProfileStoreWithLock: updateAuthProfileStore as never,
|
|
loadPersistedAuthProfileStore: vi.fn(() => {
|
|
throw new Error("simulated auth read failure");
|
|
}),
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
await expect(activate()).resolves.toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("rolled back"),
|
|
});
|
|
await expect(activate()).resolves.toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("rolled back"),
|
|
});
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(realStoreUpdates).toBe(4);
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toHaveLength(0);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("ignores an unrelated provider patch and preserves a concurrent operator edit", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const auxProvider = {
|
|
baseUrl: "https://aux.example.test/v1",
|
|
apiKey: "base-key",
|
|
models: [],
|
|
};
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
models: { providers: { aux: auxProvider } },
|
|
} satisfies OpenClawConfig;
|
|
const concurrentConfig: OpenClawConfig = {
|
|
...initialConfig,
|
|
models: {
|
|
providers: {
|
|
aux: { ...auxProvider, apiKey: "operator-key" },
|
|
},
|
|
},
|
|
};
|
|
resolveAgentDir(initialConfig, "main");
|
|
const configHarness = createConfigTransformHarness(concurrentConfig);
|
|
const provider = createGroqSetupProvider({
|
|
models: {
|
|
providers: {
|
|
aux: {
|
|
baseUrl: "https://aux.example.test/v1",
|
|
apiKey: { source: "env", provider: "default", id: "AUX_API_KEY" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
|
|
const persisted = configHarness.current();
|
|
expect(persisted.models?.providers?.aux).toEqual(concurrentConfig.models?.providers?.aux);
|
|
expect(persisted.agents?.defaults?.model).toMatch(
|
|
/^groq\/llama-3\.3-70b-versatile@groq:setup-/,
|
|
);
|
|
expect(
|
|
Object.keys(readAuthProfileStoreForTest(agentDir).profiles).filter((id) =>
|
|
id.startsWith("groq:setup-"),
|
|
),
|
|
).toHaveLength(1);
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("resolves the config transformer before persisting a verified credential", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const initialConfig = {
|
|
agents: { list: [{ id: "main", default: true, agentDir }] },
|
|
} satisfies OpenClawConfig;
|
|
resolveAgentDir(initialConfig, "main");
|
|
const authWriteDirs: string[] = [];
|
|
const deps = {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [createGroqSetupProvider()],
|
|
resolveManifestProviderAuthChoice: groqSetupChoice,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("groq", "llama-3.3-70b-versatile")) as never,
|
|
updateAuthProfileStoreWithLock: vi.fn(async (params) => {
|
|
authWriteDirs.push(params.agentDir ?? "");
|
|
return await updateAuthProfileStoreWithLock(params);
|
|
}),
|
|
createTempDir: makeTempDir,
|
|
};
|
|
Object.defineProperty(deps, "transformConfigWithPendingPluginInstalls", {
|
|
get: () => {
|
|
throw new Error("simulated transformer resolution failure");
|
|
},
|
|
});
|
|
|
|
try {
|
|
await expect(
|
|
activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "candidate-key",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: deps as never,
|
|
}),
|
|
).rejects.toThrow("simulated transformer resolution failure");
|
|
|
|
expect(authWriteDirs).not.toContain(agentDir);
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles).toEqual({});
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "uses a provider starter model instead of an unrelated existing default",
|
|
existingModel: "openai/gpt-5.2",
|
|
starterModel: "github-copilot/claude-sonnet-4.5",
|
|
},
|
|
{
|
|
name: "accepts an unchanged provider-owned dynamic model",
|
|
existingModel: "github-copilot/claude-sonnet-4.5",
|
|
starterModel: undefined,
|
|
},
|
|
])("$name without starting interactive login", async ({ existingModel, starterModel }) => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const runInteractive = vi.fn();
|
|
const runNonInteractive = vi.fn(
|
|
async (ctx: {
|
|
agentDir?: string;
|
|
opts: { githubCopilotToken?: unknown };
|
|
config: OpenClawConfig;
|
|
}) => {
|
|
const token =
|
|
typeof ctx.opts.githubCopilotToken === "string" ? ctx.opts.githubCopilotToken : "";
|
|
await upsertAuthProfileWithLock({
|
|
profileId: "github-copilot:github",
|
|
credential: { type: "token", provider: "github-copilot", token },
|
|
agentDir: ctx.agentDir,
|
|
});
|
|
return {
|
|
...ctx.config,
|
|
agents: {
|
|
...ctx.config.agents,
|
|
defaults: {
|
|
...ctx.config.agents?.defaults,
|
|
model: ctx.config.agents?.defaults?.model ?? {
|
|
primary: "github-copilot/claude-sonnet-4.5",
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
},
|
|
);
|
|
const provider: ProviderPlugin = {
|
|
id: "github-copilot",
|
|
label: "GitHub Copilot",
|
|
pluginId: "github-copilot",
|
|
auth: [
|
|
{
|
|
id: "device",
|
|
label: "GitHub device login",
|
|
kind: "device_code",
|
|
...(starterModel ? { starterModel } : {}),
|
|
run: runInteractive as never,
|
|
runNonInteractive: runNonInteractive as never,
|
|
},
|
|
],
|
|
};
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: SuccessfulRunParams & { authProfileId?: string }) =>
|
|
successfulRun("github-copilot", "claude-sonnet-4.5", params),
|
|
);
|
|
const initialConfig = {
|
|
gateway: { port: 18789 },
|
|
agents: {
|
|
defaults: { model: { primary: existingModel } },
|
|
list: [{ id: "main", default: true, agentDir }],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const concurrentConfig: OpenClawConfig = {
|
|
gateway: { port: 19000 },
|
|
agents: {
|
|
defaults: { model: { primary: existingModel } },
|
|
list: [{ id: "main", default: true, agentDir }],
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const configHarness = createConfigTransformHarness(concurrentConfig);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "github-copilot",
|
|
apiKey: "github-token",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "github-copilot",
|
|
providerId: "github-copilot",
|
|
methodId: "device",
|
|
choiceId: "github-copilot",
|
|
choiceLabel: "GitHub Copilot",
|
|
optionKey: "githubCopilotToken",
|
|
cliOption: "--github-copilot-token <token>",
|
|
appGuidedSecret: true,
|
|
}),
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
modelRef: "github-copilot/claude-sonnet-4.5",
|
|
});
|
|
expect(runInteractive).not.toHaveBeenCalled();
|
|
expect(runNonInteractive).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
opts: expect.objectContaining({ githubCopilotToken: "github-token" }),
|
|
}),
|
|
);
|
|
const activatedProfileId = runEmbeddedAgent.mock.calls[0]?.[0].authProfileId;
|
|
if (!activatedProfileId) {
|
|
throw new Error("expected setup auth profile");
|
|
}
|
|
expect(activatedProfileId).toMatch(/^github-copilot:setup-/);
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentId: "openclaw",
|
|
agentDir: expect.stringContaining("setup-inference-test-"),
|
|
authProfileId: activatedProfileId,
|
|
provider: "github-copilot",
|
|
model: "claude-sonnet-4.5",
|
|
}),
|
|
);
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles[activatedProfileId]).toMatchObject({
|
|
type: "token",
|
|
provider: "github-copilot",
|
|
token: "github-token",
|
|
});
|
|
const persistedConfig = configHarness.current();
|
|
expect(persistedConfig.gateway?.port).toBe(19000);
|
|
expect(persistedConfig.agents?.defaults?.model).toEqual({
|
|
primary: `github-copilot/claude-sonnet-4.5@${activatedProfileId}`,
|
|
});
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("does not persist a provider key after a failed live test", async () => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const provider: ProviderPlugin = {
|
|
id: "groq",
|
|
label: "Groq",
|
|
pluginId: "groq",
|
|
auth: [
|
|
{
|
|
id: "api-key",
|
|
label: "Groq API key",
|
|
kind: "api_key",
|
|
wizard: { choiceId: "groq-api-key" },
|
|
run: async (ctx) => ({
|
|
profiles: [
|
|
{
|
|
profileId: "groq:default",
|
|
credential: { type: "api_key", provider: "groq", key: ctx.opts?.token },
|
|
},
|
|
],
|
|
defaultModel: "groq/llama-3.3-70b-versatile",
|
|
}),
|
|
},
|
|
],
|
|
};
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "api-key",
|
|
authChoice: "groq-api-key",
|
|
apiKey: "bad-groq-key",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
resolvePluginProviders: () => [provider],
|
|
resolveManifestProviderAuthChoice: () => ({
|
|
pluginId: "groq",
|
|
providerId: "groq",
|
|
methodId: "api-key",
|
|
choiceId: "groq-api-key",
|
|
choiceLabel: "Groq API key",
|
|
appGuidedSecret: true,
|
|
}),
|
|
runEmbeddedAgent: vi.fn(async () => {
|
|
throw new Error("401 rejected credential bad-groq-key");
|
|
}) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
if (!result.ok) {
|
|
expect(result.error).toContain("401 rejected credential [redacted]");
|
|
expect(result.error).not.toContain("bad-groq-key");
|
|
}
|
|
expect(readAuthProfileStoreForTest(agentDir).profiles["groq:default"]).toBeUndefined();
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("installs the codex runtime independently of a custom OpenAI route", async () => {
|
|
const events: string[] = [];
|
|
const runtimeLog = vi.fn();
|
|
const initialConfig = {
|
|
gateway: { port: 18789 },
|
|
agents: {
|
|
defaults: { model: { primary: "openai/gpt-5.4" } },
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
model: {
|
|
primary: "anthropic/claude-opus-4-8",
|
|
fallbacks: ["google/gemini-3.1-pro-preview"],
|
|
},
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://proxy.example.test/v1",
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
plugins: {
|
|
entries: {
|
|
codex: {
|
|
config: { appServer: { command: "codex", mode: "yolo" } },
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const ensureCodex = vi.fn(async (params: { cfg: OpenClawConfig }) => {
|
|
events.push("install-plugin");
|
|
return {
|
|
cfg: {
|
|
...params.cfg,
|
|
plugins: {
|
|
...params.cfg.plugins,
|
|
entries: {
|
|
...params.cfg.plugins?.entries,
|
|
codex: {
|
|
...params.cfg.plugins?.entries?.codex,
|
|
enabled: true,
|
|
},
|
|
},
|
|
installs: {
|
|
...params.cfg.plugins?.installs,
|
|
codex: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
};
|
|
});
|
|
const runEmbeddedAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
events.push("live-test");
|
|
return successfulRun("openai", "gpt-5.6-sol", params);
|
|
});
|
|
let persistedConfig: OpenClawConfig = {
|
|
...initialConfig,
|
|
gateway: { port: 19000 },
|
|
};
|
|
let activationCommitted = false;
|
|
const pendingCodexInstalls: unknown[] = [];
|
|
const transformConfig = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: {
|
|
snapshot: {
|
|
config: OpenClawConfig;
|
|
sourceConfig: OpenClawConfig;
|
|
runtimeConfig: OpenClawConfig;
|
|
};
|
|
},
|
|
) => Promise<{ nextConfig: OpenClawConfig }> | { nextConfig: OpenClawConfig };
|
|
}) => {
|
|
const transformed = (
|
|
await params.transform(persistedConfig, {
|
|
snapshot: {
|
|
config: persistedConfig,
|
|
sourceConfig: persistedConfig,
|
|
runtimeConfig: persistedConfig,
|
|
},
|
|
})
|
|
).nextConfig;
|
|
const configuredRuntime =
|
|
transformed.agents?.defaults?.models?.["openai/gpt-5.6-sol"]?.agentRuntime?.id ??
|
|
transformed.agents?.list?.find((agent) => agent.id === "ops")?.models?.[
|
|
"openai/gpt-5.6-sol"
|
|
]?.agentRuntime?.id;
|
|
events.push(configuredRuntime === "codex" ? "persist-plugin-config" : "unexpected-write");
|
|
pendingCodexInstalls.push(transformed.plugins?.installs?.codex);
|
|
persistedConfig = withoutPluginInstallRecords(transformed);
|
|
activationCommitted = true;
|
|
return { nextConfig: persistedConfig };
|
|
},
|
|
);
|
|
const refreshPluginRegistry = vi.fn(async () => {
|
|
events.push("refresh-plugin-registry");
|
|
if (refreshPluginRegistry.mock.calls.length > 1) {
|
|
throw new Error("simulated registry refresh failure");
|
|
}
|
|
});
|
|
const markRetainedInstall = vi.fn(async () => {
|
|
events.push("retain-plugin-install");
|
|
return true;
|
|
});
|
|
const ensureRegistryLoaded = vi.fn(() => {
|
|
events.push("reload-active-registry");
|
|
});
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime: { log: runtimeLog, error: () => {}, exit: () => {} } as never,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => {
|
|
const config = activationCommitted ? persistedConfig : initialConfig;
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config,
|
|
sourceConfig: config,
|
|
runtimeConfig: config,
|
|
};
|
|
}) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
ensureCodexRuntimePlugin: ensureCodex as never,
|
|
markRetainedManagedNpmInstall: markRetainedInstall,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
ensurePluginRegistryLoaded: ensureRegistryLoaded,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
expect(runtimeLog).not.toHaveBeenCalled();
|
|
expect(ensureCodex).toHaveBeenCalledOnce();
|
|
expect(ensureCodex).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
cfg: expect.objectContaining({
|
|
agents: {
|
|
defaults: { model: { primary: "openai/gpt-5.4" } },
|
|
list: [
|
|
expect.objectContaining({
|
|
id: "ops",
|
|
model: {
|
|
primary: "openai/gpt-5.6-sol",
|
|
fallbacks: ["google/gemini-3.1-pro-preview"],
|
|
},
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
|
|
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
|
},
|
|
}),
|
|
],
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: { baseUrl: "https://proxy.example.test/v1", models: [] },
|
|
},
|
|
},
|
|
}),
|
|
model: "openai/gpt-5.6-sol",
|
|
agentId: "ops",
|
|
}),
|
|
);
|
|
expect(mocks.ensureSelectedAgentHarnessPlugin).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "openai",
|
|
modelId: "gpt-5.6-sol",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
}),
|
|
);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
reason: "source-changed",
|
|
policyPluginIds: ["codex"],
|
|
traceCommand: "openclaw-setup-probe",
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
}),
|
|
);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledTimes(2);
|
|
expect(events).toEqual([
|
|
"install-plugin",
|
|
"retain-plugin-install",
|
|
"refresh-plugin-registry",
|
|
"live-test",
|
|
"persist-plugin-config",
|
|
"refresh-plugin-registry",
|
|
"reload-active-registry",
|
|
]);
|
|
expect(transformConfig).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
afterWrite: { mode: "auto" },
|
|
}),
|
|
);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledWith({
|
|
config: persistedConfig,
|
|
reason: "source-changed",
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
logger: expect.objectContaining({ warn: expect.any(Function) }),
|
|
});
|
|
expect(ensureRegistryLoaded).toHaveBeenCalledWith({
|
|
scope: "all",
|
|
config: persistedConfig,
|
|
activationSourceConfig: persistedConfig,
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
});
|
|
// Harness selection: codex tests run embedded with the codex harness.
|
|
expect(runEmbeddedAgent.mock.calls[0]?.[0]).toMatchObject({
|
|
agentId: "openclaw",
|
|
agentDir: resolveAgentDir(initialConfig, "ops"),
|
|
provider: "openai",
|
|
authProfileStateMode: "read-only",
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-5.4" },
|
|
},
|
|
list: [
|
|
expect.objectContaining({
|
|
id: "ops",
|
|
model: {
|
|
primary: "openai/gpt-5.6-sol",
|
|
fallbacks: ["google/gemini-3.1-pro-preview"],
|
|
},
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
|
|
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
|
},
|
|
}),
|
|
],
|
|
},
|
|
plugins: {
|
|
entries: {
|
|
codex: {
|
|
enabled: true,
|
|
config: {
|
|
appServer: {
|
|
command: "codex",
|
|
mode: "yolo",
|
|
transport: "stdio",
|
|
homeScope: "agent",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
installs: {
|
|
codex: expect.objectContaining({ installPath: "/tmp/plugins/codex" }),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(runEmbeddedAgent.mock.calls[0]?.[0]).toMatchObject({
|
|
agentHarnessRuntimeOverride: "codex",
|
|
});
|
|
expect(runEmbeddedAgent.mock.calls[0]?.[0]).not.toHaveProperty("streamParams");
|
|
expect(persistedConfig).toMatchObject({
|
|
gateway: { port: 19000 },
|
|
models: {
|
|
providers: {
|
|
openai: { baseUrl: "https://proxy.example.test/v1" },
|
|
},
|
|
},
|
|
agents: {
|
|
defaults: { model: { primary: "openai/gpt-5.4" } },
|
|
list: [
|
|
expect.objectContaining({
|
|
id: "ops",
|
|
model: {
|
|
primary: "openai/gpt-5.6-sol",
|
|
fallbacks: ["google/gemini-3.1-pro-preview"],
|
|
},
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
|
|
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
|
},
|
|
}),
|
|
],
|
|
},
|
|
plugins: {
|
|
entries: {
|
|
codex: {
|
|
enabled: true,
|
|
config: {
|
|
appServer: {
|
|
command: "codex",
|
|
mode: "yolo",
|
|
transport: "stdio",
|
|
homeScope: "agent",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(persistedConfig.plugins?.installs).toBeUndefined();
|
|
expect(pendingCodexInstalls[0]).toMatchObject({
|
|
source: "npm",
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex",
|
|
});
|
|
expect(pendingCodexInstalls).toHaveLength(1);
|
|
});
|
|
|
|
it("probes and persists an exact non-default model through the Codex route", async () => {
|
|
const initialConfig: OpenClawConfig = {};
|
|
const configHarness = createConfigTransformHarness(initialConfig);
|
|
const ensureCodex = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
entries: {
|
|
...cfg.plugins?.entries,
|
|
codex: { ...cfg.plugins?.entries?.codex, enabled: true },
|
|
},
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
}));
|
|
const ensureSelectedAgentHarnessPlugin = vi.fn(async () => {});
|
|
const refreshPluginRegistryAfterConfigMutation = vi.fn(
|
|
async (params: { logger?: { warn?: (message: string) => void } }) => {
|
|
params.logger?.warn?.("best-effort refresh warning");
|
|
},
|
|
);
|
|
const runEmbeddedAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
expect(refreshPluginRegistryAfterConfigMutation).toHaveBeenCalledOnce();
|
|
expect(ensureSelectedAgentHarnessPlugin).toHaveBeenCalledOnce();
|
|
return successfulRun("openai", "gpt-5.4", params);
|
|
});
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
modelRef: "openai/gpt-5.4",
|
|
workspace: "/tmp/work",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: initialConfig,
|
|
sourceConfig: initialConfig,
|
|
runtimeConfig: initialConfig,
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: ensureCodex as never,
|
|
ensureSelectedAgentHarnessPlugin: ensureSelectedAgentHarnessPlugin as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistryAfterConfigMutation as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: configHarness.transform as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.4" });
|
|
expect(ensureCodex).toHaveBeenCalledWith(expect.objectContaining({ model: "openai/gpt-5.4" }));
|
|
expect(ensureSelectedAgentHarnessPlugin).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
}),
|
|
);
|
|
expect(refreshPluginRegistryAfterConfigMutation).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
reason: "source-changed",
|
|
policyPluginIds: ["codex"],
|
|
traceCommand: "openclaw-setup-probe",
|
|
workspaceDir: "/tmp/work",
|
|
}),
|
|
);
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentHarnessRuntimeOverride: "codex",
|
|
provider: "openai",
|
|
model: "gpt-5.4",
|
|
config: expect.objectContaining({
|
|
agents: expect.objectContaining({
|
|
defaults: expect.objectContaining({
|
|
model: "openai/gpt-5.4",
|
|
}),
|
|
list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "main",
|
|
models: {
|
|
"openai/gpt-5.4": { agentRuntime: { id: "codex" } },
|
|
},
|
|
}),
|
|
]),
|
|
}),
|
|
plugins: expect.objectContaining({
|
|
entries: expect.objectContaining({
|
|
codex: expect.objectContaining({
|
|
enabled: true,
|
|
config: expect.objectContaining({
|
|
appServer: expect.objectContaining({
|
|
transport: "stdio",
|
|
homeScope: "agent",
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
expect(configHarness.current()).toMatchObject({
|
|
agents: expect.objectContaining({
|
|
defaults: expect.objectContaining({ model: "openai/gpt-5.4" }),
|
|
list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "main",
|
|
models: { "openai/gpt-5.4": { agentRuntime: { id: "codex" } } },
|
|
}),
|
|
]),
|
|
}),
|
|
plugins: {
|
|
entries: {
|
|
codex: {
|
|
enabled: true,
|
|
config: { appServer: { transport: "stdio", homeScope: "agent" } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("commits only the refreshed codex record when authored install metadata is stale", async () => {
|
|
const staleAuthoredRecords = {
|
|
codex: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@1.0.0",
|
|
installPath: "/tmp/plugins/codex-v1",
|
|
},
|
|
unrelated: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/unrelated@1.0.0",
|
|
installPath: "/tmp/plugins/unrelated-v1",
|
|
},
|
|
};
|
|
const canonicalRecords = {
|
|
codex: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@2.0.0",
|
|
installPath: "/tmp/plugins/codex-v2",
|
|
},
|
|
unrelated: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/unrelated@2.0.0",
|
|
installPath: "/tmp/plugins/unrelated-v2",
|
|
},
|
|
};
|
|
const refreshedCodexRecord = {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@3.0.0",
|
|
installPath: "/tmp/plugins/codex-v3",
|
|
};
|
|
const sourceConfig = {
|
|
plugins: { installs: staleAuthoredRecords },
|
|
} satisfies OpenClawConfig;
|
|
const runtimeConfig = {
|
|
plugins: { installs: canonicalRecords },
|
|
} satisfies OpenClawConfig;
|
|
const ensureCodex = vi.fn(async (params: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...params.cfg,
|
|
plugins: {
|
|
...params.cfg.plugins,
|
|
installs: { codex: refreshedCodexRecord },
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
}));
|
|
let persistedConfig: OpenClawConfig = sourceConfig;
|
|
let installIndex: Record<string, PluginInstallRecord> = structuredClone(canonicalRecords);
|
|
const pendingInstallRecords: unknown[] = [];
|
|
const transformConfig = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: {
|
|
snapshot: {
|
|
config: OpenClawConfig;
|
|
sourceConfig: OpenClawConfig;
|
|
runtimeConfig: OpenClawConfig;
|
|
};
|
|
},
|
|
) => Promise<{ nextConfig: OpenClawConfig }> | { nextConfig: OpenClawConfig };
|
|
}) => {
|
|
const transformed = (
|
|
await params.transform(persistedConfig, {
|
|
snapshot: { config: runtimeConfig, sourceConfig, runtimeConfig },
|
|
})
|
|
).nextConfig;
|
|
const pending = transformed.plugins?.installs;
|
|
pendingInstallRecords.push(pending);
|
|
installIndex = { ...installIndex, ...pending };
|
|
persistedConfig = withoutPluginInstallRecords(transformed);
|
|
return { nextConfig: persistedConfig };
|
|
},
|
|
);
|
|
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: sourceConfig,
|
|
runtimeConfig,
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: ensureCodex as never,
|
|
markRetainedManagedNpmInstall: vi.fn(async () => true),
|
|
runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6-sol")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(ensureCodex).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
cfg: expect.not.objectContaining({
|
|
plugins: expect.objectContaining({ installs: expect.anything() }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(pendingInstallRecords).toStrictEqual([{ codex: refreshedCodexRecord }]);
|
|
expect(installIndex).toStrictEqual({
|
|
codex: refreshedCodexRecord,
|
|
unrelated: canonicalRecords.unrelated,
|
|
});
|
|
expect(persistedConfig.plugins?.installs).toBeUndefined();
|
|
});
|
|
|
|
it("does not run or persist when the codex runtime install fails", async () => {
|
|
const runEmbeddedAgent = vi.fn();
|
|
const transformConfig = vi.fn();
|
|
const refreshPluginRegistry = vi.fn();
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
ensureCodexRuntimePlugin: vi.fn(async () => ({
|
|
cfg: {},
|
|
required: true,
|
|
installed: false,
|
|
status: "failed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "unavailable" });
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed before inference when the staged Codex package cannot be retained", async () => {
|
|
const installRecord: PluginInstallRecord = {
|
|
source: "npm",
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex-unretained",
|
|
};
|
|
const runEmbeddedAgent = vi.fn();
|
|
const transformConfig = vi.fn();
|
|
const markRetainedInstall = vi.fn().mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
const clearInstallRecords = vi.fn();
|
|
const clearMetadata = vi.fn();
|
|
const clearDiscovery = vi.fn(async () => {});
|
|
const refreshPluginRegistry = vi.fn(async () => {});
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: {},
|
|
runtimeConfig: {},
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...cfg,
|
|
plugins: { ...cfg.plugins, installs: { codex: installRecord } },
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
markRetainedManagedNpmInstall: markRetainedInstall,
|
|
clearLoadInstalledPluginIndexInstallRecordsCache: clearInstallRecords,
|
|
clearPluginMetadataLifecycleCaches: clearMetadata,
|
|
invalidatePluginRuntimeDiscoveryAfterConfigMutation: clearDiscovery as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unavailable",
|
|
error: expect.stringContaining("retain the staged Codex runtime safely"),
|
|
});
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(markRetainedInstall).toHaveBeenCalledTimes(2);
|
|
expect(clearInstallRecords).toHaveBeenCalledTimes(2);
|
|
expect(clearMetadata).toHaveBeenCalledTimes(2);
|
|
expect(clearDiscovery).toHaveBeenCalledTimes(2);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledWith({
|
|
config: {},
|
|
reason: "source-changed",
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
logger: expect.objectContaining({ warn: expect.any(Function) }),
|
|
});
|
|
});
|
|
|
|
it("reports an indeterminate activation when final Codex retention fails", async () => {
|
|
const installRecord: PluginInstallRecord = {
|
|
source: "npm",
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex-final-retention-failure",
|
|
};
|
|
const markRetainedInstall = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
|
const refreshPluginRegistry = vi.fn(async () => {});
|
|
let tempDir: string | undefined;
|
|
const activation = activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: {},
|
|
runtimeConfig: {},
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...cfg,
|
|
plugins: { ...cfg.plugins, installs: { codex: installRecord } },
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: vi.fn(async () => {
|
|
throw new Error("401 invalid_api_key");
|
|
}) as never,
|
|
transformConfigWithPendingPluginInstalls: vi.fn() as never,
|
|
markRetainedManagedNpmInstall: markRetainedInstall,
|
|
clearLoadInstalledPluginIndexInstallRecordsCache: vi.fn(),
|
|
clearPluginMetadataLifecycleCaches: vi.fn(),
|
|
invalidatePluginRuntimeDiscoveryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: async () => {
|
|
tempDir = await makeTempDir();
|
|
return tempDir;
|
|
},
|
|
},
|
|
});
|
|
|
|
await expect(activation).rejects.toThrow(
|
|
"stopped before its Codex runtime package could be retained safely",
|
|
);
|
|
expect(markRetainedInstall).toHaveBeenCalledTimes(2);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledTimes(2);
|
|
expect(tempDir).toBeDefined();
|
|
await expect(fs.stat(tempDir!)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("restores the active registry from persisted config after a failed Codex probe", async () => {
|
|
resetPluginRuntimeStateForTest();
|
|
const installRecord: PluginInstallRecord = {
|
|
source: "npm",
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex-staged-registry",
|
|
};
|
|
const persistedConfig = { plugins: { enabled: false } } satisfies OpenClawConfig;
|
|
const stagedRegistry = createEmptyPluginRegistry();
|
|
stagedRegistry.plugins.push({
|
|
id: "codex",
|
|
name: "Codex",
|
|
status: "loaded",
|
|
format: "bundle",
|
|
imported: true,
|
|
} as never);
|
|
let snapshotRead = 0;
|
|
const ensureRegistryLoaded = vi.fn(
|
|
(options: Parameters<typeof ensurePluginRegistryLoaded>[0]) =>
|
|
ensurePluginRegistryLoaded(options),
|
|
);
|
|
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
workspace: "/tmp/openclaw-workspace",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => {
|
|
const config = snapshotRead++ === 0 ? {} : persistedConfig;
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config,
|
|
sourceConfig: config,
|
|
runtimeConfig: config,
|
|
};
|
|
}) as never,
|
|
ensureCodexRuntimePlugin: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...cfg,
|
|
plugins: { ...cfg.plugins, installs: { codex: installRecord } },
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: vi.fn(async () => {
|
|
setActivePluginRegistry(
|
|
stagedRegistry,
|
|
"staged-codex-registry",
|
|
"default",
|
|
"/tmp/setup-probe",
|
|
);
|
|
throw new Error("401 invalid_api_key");
|
|
}) as never,
|
|
transformConfigWithPendingPluginInstalls: vi.fn() as never,
|
|
markRetainedManagedNpmInstall: vi.fn(async () => true),
|
|
clearLoadInstalledPluginIndexInstallRecordsCache: vi.fn(),
|
|
clearPluginMetadataLifecycleCaches: vi.fn(),
|
|
invalidatePluginRuntimeDiscoveryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
refreshPluginRegistryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
ensurePluginRegistryLoaded: ensureRegistryLoaded,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
expect(ensureRegistryLoaded).toHaveBeenCalledWith({
|
|
scope: "all",
|
|
config: persistedConfig,
|
|
activationSourceConfig: persistedConfig,
|
|
workspaceDir: "/tmp/openclaw-workspace",
|
|
});
|
|
expect(getActivePluginRegistry()).not.toBe(stagedRegistry);
|
|
expect(getActivePluginRegistry()?.plugins.some((plugin) => plugin.id === "codex")).toBe(
|
|
false,
|
|
);
|
|
expect(getActivePluginRegistryKey()).not.toBe("staged-codex-registry");
|
|
expect(getActivePluginRegistryWorkspaceDir()).toBe("/tmp/openclaw-workspace");
|
|
} finally {
|
|
resetPluginRuntimeStateForTest();
|
|
}
|
|
});
|
|
|
|
it("does not install codex when plugin policy blocks it", async () => {
|
|
const ensureCodex = vi.fn();
|
|
const runEmbeddedAgent = vi.fn();
|
|
const transformConfig = vi.fn();
|
|
const refreshPluginRegistry = vi.fn();
|
|
const blockedConfig: OpenClawConfig = { plugins: { allow: ["other"] } };
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [],
|
|
config: blockedConfig,
|
|
runtimeConfig: blockedConfig,
|
|
})) as never,
|
|
ensureCodexRuntimePlugin: ensureCodex as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unavailable",
|
|
error: expect.stringContaining("blocked by allowlist"),
|
|
});
|
|
expect(ensureCodex).not.toHaveBeenCalled();
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("marks an unowned Codex package generation retained when the live test fails", async () => {
|
|
const installProjectDir = await makeTempDir();
|
|
const packageDir = path.join(installProjectDir, "node_modules", "@openclaw", "codex");
|
|
await fs.mkdir(packageDir, { recursive: true });
|
|
const transformConfig = vi.fn();
|
|
const refreshPluginRegistry = vi.fn();
|
|
const runEmbeddedAgent = vi.fn(async () => {
|
|
throw new Error("401 invalid_api_key");
|
|
});
|
|
try {
|
|
const result = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
ensureCodexRuntimePlugin: vi.fn(async (params: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...params.cfg,
|
|
plugins: {
|
|
...params.cfg.plugins,
|
|
installs: {
|
|
...params.cfg.plugins?.installs,
|
|
codex: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex",
|
|
installPath: packageDir,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
readPersistedInstalledPluginIndexInstallRecords: vi.fn(async () => ({})),
|
|
refreshPluginRegistryAfterConfigMutation: refreshPluginRegistry as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: expect.objectContaining({
|
|
plugins: expect.objectContaining({
|
|
installs: {
|
|
codex: expect.objectContaining({ installPath: packageDir }),
|
|
},
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
await expect(fs.stat(packageDir)).resolves.toBeDefined();
|
|
expect(hasRetainedManagedNpmInstallMarker(packageDir)).toBe(true);
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
expect(refreshPluginRegistry).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
await fs.rm(installProjectDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("clears transient Codex install caches before and after a failed probe", async () => {
|
|
const installRecords = [
|
|
{
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@generation-1",
|
|
installPath: "/tmp/plugins/codex-generation-1",
|
|
},
|
|
{
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@generation-2",
|
|
installPath: "/tmp/plugins/codex-generation-2",
|
|
},
|
|
];
|
|
const createdRecords: PluginInstallRecord[] = [];
|
|
let installedRecordCache: PluginInstallRecord | undefined;
|
|
let metadataCache: PluginInstallRecord | undefined;
|
|
let discoveryCache: PluginInstallRecord | undefined;
|
|
const ensureCodex = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
const cachedRecord = installedRecordCache ?? metadataCache ?? discoveryCache;
|
|
if (cachedRecord) {
|
|
return {
|
|
cfg,
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
};
|
|
}
|
|
const record = installRecords[createdRecords.length];
|
|
if (!record) {
|
|
throw new Error("unexpected Codex install generation");
|
|
}
|
|
createdRecords.push(record);
|
|
installedRecordCache = record;
|
|
metadataCache = record;
|
|
discoveryCache = record;
|
|
return {
|
|
cfg: {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
installs: { ...cfg.plugins?.installs, codex: record },
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
};
|
|
});
|
|
const runEmbeddedAgent = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error("401 invalid_api_key"))
|
|
.mockImplementationOnce(successfulRunner("openai", "gpt-5.6-sol"));
|
|
const clearInstallRecords = vi.fn(() => {
|
|
installedRecordCache = undefined;
|
|
});
|
|
const clearMetadata = vi.fn(() => {
|
|
metadataCache = undefined;
|
|
});
|
|
const clearDiscovery = vi.fn(async () => {
|
|
discoveryCache = undefined;
|
|
});
|
|
const markRetained = vi.fn(async () => true);
|
|
const committedInstallRecords: PluginInstallRecord[] = [];
|
|
const transformConfig = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: { snapshot: { config: OpenClawConfig; runtimeConfig: OpenClawConfig } },
|
|
) => Promise<{ nextConfig: OpenClawConfig }>;
|
|
}) => {
|
|
const transformed = await params.transform(
|
|
{},
|
|
{ snapshot: { config: {}, runtimeConfig: {} } },
|
|
);
|
|
const record = transformed.nextConfig.plugins?.installs?.codex;
|
|
if (record) {
|
|
committedInstallRecords.push(record);
|
|
}
|
|
return { nextConfig: withoutPluginInstallRecords(transformed.nextConfig) };
|
|
},
|
|
);
|
|
const deps = {
|
|
ensureCodexRuntimePlugin: ensureCodex as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
refreshPluginRegistryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
readPersistedInstalledPluginIndexInstallRecords: vi.fn(async () => ({})),
|
|
markRetainedManagedNpmInstall: markRetained,
|
|
clearLoadInstalledPluginIndexInstallRecordsCache: clearInstallRecords,
|
|
clearPluginMetadataLifecycleCaches: clearMetadata,
|
|
invalidatePluginRuntimeDiscoveryAfterConfigMutation: clearDiscovery as never,
|
|
createTempDir: makeTempDir,
|
|
};
|
|
|
|
const first = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps,
|
|
});
|
|
const second = await activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps,
|
|
});
|
|
|
|
expect(first).toMatchObject({ ok: false, status: "auth" });
|
|
expect(second).toMatchObject({ ok: true, modelRef: "openai/gpt-5.6-sol" });
|
|
expect(createdRecords).toStrictEqual(installRecords);
|
|
expect(markRetained).toHaveBeenNthCalledWith(1, {
|
|
packageDir: expectDefined(installRecords[0], "installRecords[0] test invariant").installPath,
|
|
pluginId: "codex",
|
|
reason: "openclaw-inference-activation-not-committed",
|
|
});
|
|
expect(markRetained).toHaveBeenNthCalledWith(2, {
|
|
packageDir: expectDefined(installRecords[0], "installRecords[0] test invariant").installPath,
|
|
pluginId: "codex",
|
|
reason: "openclaw-inference-activation-not-committed",
|
|
});
|
|
expect(markRetained).toHaveBeenNthCalledWith(3, {
|
|
packageDir: expectDefined(installRecords[1], "installRecords[1] test invariant").installPath,
|
|
pluginId: "codex",
|
|
reason: "openclaw-inference-activation-not-committed",
|
|
});
|
|
expect(clearInstallRecords).toHaveBeenCalledTimes(3);
|
|
expect(clearMetadata).toHaveBeenCalledTimes(3);
|
|
expect(clearDiscovery).toHaveBeenCalledTimes(3);
|
|
expect(transformConfig).toHaveBeenCalledOnce();
|
|
expect(committedInstallRecords).toStrictEqual([installRecords[1]]);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "missing", installRecords: {} as Record<string, PluginInstallRecord>, succeeds: false },
|
|
{
|
|
name: "mismatched",
|
|
installRecords: {
|
|
codex: {
|
|
source: "npm" as const,
|
|
spec: "@openclaw/codex@other",
|
|
installPath: "/tmp/plugins/codex-other",
|
|
},
|
|
},
|
|
succeeds: false,
|
|
},
|
|
{
|
|
name: "exact",
|
|
installRecords: undefined as Record<string, PluginInstallRecord> | undefined,
|
|
succeeds: true,
|
|
},
|
|
])("reconciles a post-write Codex error only with an $name install record", async (testCase) => {
|
|
const installRecord: PluginInstallRecord = {
|
|
source: "npm",
|
|
spec: "@openclaw/codex",
|
|
installPath: "/tmp/plugins/codex",
|
|
};
|
|
const installRecords = testCase.installRecords ?? { codex: installRecord };
|
|
let committedConfig: OpenClawConfig | undefined;
|
|
const readConfigFileSnapshot = vi.fn(async () => {
|
|
const sourceConfig = committedConfig ?? {};
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
config: sourceConfig,
|
|
sourceConfig,
|
|
runtimeConfig: sourceConfig,
|
|
};
|
|
});
|
|
const transformConfig = vi.fn(
|
|
async (params: {
|
|
transform: (
|
|
config: OpenClawConfig,
|
|
context: { snapshot: { config: OpenClawConfig; runtimeConfig: OpenClawConfig } },
|
|
) => Promise<{ nextConfig: OpenClawConfig }>;
|
|
}) => {
|
|
const transformed = await params.transform(
|
|
{},
|
|
{ snapshot: { config: {}, runtimeConfig: {} } },
|
|
);
|
|
committedConfig = withoutPluginInstallRecords(transformed.nextConfig);
|
|
throw new Error("simulated post-write failure");
|
|
},
|
|
);
|
|
const readInstallRecords = vi.fn(async () => installRecords);
|
|
const markRetainedInstall = vi.fn(async () => true);
|
|
|
|
const activation = activateSetupInference({
|
|
kind: "codex-cli",
|
|
surface: "gateway",
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
ensureCodexRuntimePlugin: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
|
cfg: {
|
|
...cfg,
|
|
plugins: {
|
|
...cfg.plugins,
|
|
installs: { ...cfg.plugins?.installs, codex: installRecord },
|
|
},
|
|
},
|
|
required: true,
|
|
installed: true,
|
|
status: "installed" as const,
|
|
})) as never,
|
|
runEmbeddedAgent: vi.fn(successfulRunner("openai", "gpt-5.6-sol")) as never,
|
|
transformConfigWithPendingPluginInstalls: transformConfig as never,
|
|
readPersistedInstalledPluginIndexInstallRecords: readInstallRecords,
|
|
markRetainedManagedNpmInstall: markRetainedInstall,
|
|
refreshPluginRegistryAfterConfigMutation: vi.fn(async () => {}) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
if (testCase.succeeds) {
|
|
await expect(activation).resolves.toMatchObject({
|
|
ok: true,
|
|
modelRef: "openai/gpt-5.6-sol",
|
|
});
|
|
} else {
|
|
await expect(activation).rejects.toThrow("simulated post-write failure");
|
|
}
|
|
expect(readInstallRecords).toHaveBeenCalledOnce();
|
|
expect(markRetainedInstall).toHaveBeenCalledTimes(testCase.succeeds ? 1 : 2);
|
|
});
|
|
});
|
|
|
|
describe("resolvePersistentApplyInference", () => {
|
|
function createBinding(
|
|
proofKind: "runtime-owner" | "credential" = "runtime-owner",
|
|
): SystemAgentVerifiedInferenceBinding {
|
|
const execution = {
|
|
runner: "embedded" as const,
|
|
runConfig: { agents: { defaults: { model: "openai/gpt-5.5" } } },
|
|
modelLabel: "openai/gpt-5.5",
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
agentDir: "/tmp/openclaw-agent",
|
|
agentId: "main",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
};
|
|
return {
|
|
configuredRoute: {
|
|
runner: "embedded",
|
|
modelLabel: execution.modelLabel,
|
|
provider: execution.provider,
|
|
model: execution.model,
|
|
agentDir: execution.agentDir,
|
|
agentId: execution.agentId,
|
|
agentHarnessRuntimeOverride: execution.agentHarnessRuntimeOverride,
|
|
},
|
|
execution,
|
|
executionFingerprint: {
|
|
route: { provider: "openai", model: "gpt-5.5" },
|
|
defaultSelection: { explicitIds: [] },
|
|
auth: {},
|
|
models: {},
|
|
defaults: {},
|
|
plugins: {},
|
|
ownerPluginRuntimes: [],
|
|
},
|
|
ownerPluginIds: ["codex"],
|
|
ownerPluginArtifacts: [{ pluginId: "codex", fingerprint: "codex-runtime-v1" }],
|
|
auth:
|
|
proofKind === "runtime-owner"
|
|
? {
|
|
authFingerprint: "runtime-owner-fingerprint",
|
|
proofKind,
|
|
runtimeOwnerKind: "plugin-harness",
|
|
runtimeOwnerId: "codex",
|
|
}
|
|
: { authFingerprint: "strict-credential-fingerprint" },
|
|
};
|
|
}
|
|
|
|
function currentOwnerPluginArtifactDeps() {
|
|
return { hasCurrentOwnerPluginArtifacts: vi.fn(async () => true) };
|
|
}
|
|
|
|
it("skips a live turn for a strict credential", async () => {
|
|
const binding = createBinding("credential");
|
|
const resolveVerifiedInferenceRoute = vi.fn(async () => binding.execution);
|
|
const verifyBoundInference = vi.fn();
|
|
|
|
const route = await resolvePersistentApplyInference({
|
|
binding,
|
|
runtime,
|
|
deps: {
|
|
...currentOwnerPluginArtifactDeps(),
|
|
resolveVerifiedInferenceRoute,
|
|
verifyBoundInference,
|
|
},
|
|
});
|
|
|
|
expect(route).toBe(binding.execution);
|
|
expect(resolveVerifiedInferenceRoute).toHaveBeenCalledOnce();
|
|
expect(verifyBoundInference).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blocks a strict-credential write when its static owner binding is stale", async () => {
|
|
const binding = createBinding("credential");
|
|
const resolveVerifiedInferenceRoute = vi.fn(async () => null);
|
|
const verifyBoundInference = vi.fn();
|
|
|
|
const route = await resolvePersistentApplyInference({
|
|
binding,
|
|
runtime,
|
|
deps: {
|
|
...currentOwnerPluginArtifactDeps(),
|
|
resolveVerifiedInferenceRoute,
|
|
verifyBoundInference,
|
|
},
|
|
});
|
|
|
|
expect(route).toBeNull();
|
|
expect(resolveVerifiedInferenceRoute).toHaveBeenCalledOnce();
|
|
expect(verifyBoundInference).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects an opaque runtime when its liveness turn fails auth", async () => {
|
|
const binding = createBinding();
|
|
const resolveVerifiedInferenceRoute = vi.fn(async () => binding.execution);
|
|
const verifyBoundInference = vi.fn(async () => ({
|
|
ok: false as const,
|
|
status: "auth" as const,
|
|
error: "logged out",
|
|
}));
|
|
|
|
await expect(
|
|
resolvePersistentApplyInference({
|
|
binding,
|
|
runtime,
|
|
deps: {
|
|
...currentOwnerPluginArtifactDeps(),
|
|
resolveVerifiedInferenceRoute,
|
|
verifyBoundInference,
|
|
},
|
|
}),
|
|
).resolves.toBeNull();
|
|
expect(resolveVerifiedInferenceRoute).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects a liveness turn that used a different actual harness", async () => {
|
|
const binding = createBinding();
|
|
const changedBinding = structuredClone(binding);
|
|
if (changedBinding.execution.runner !== "embedded") {
|
|
throw new Error("expected embedded fixture");
|
|
}
|
|
changedBinding.execution.agentHarnessRuntimeOverride = "openclaw";
|
|
const resolveVerifiedInferenceRoute = vi.fn(async () => binding.execution);
|
|
|
|
await expect(
|
|
resolvePersistentApplyInference({
|
|
binding,
|
|
runtime,
|
|
deps: {
|
|
...currentOwnerPluginArtifactDeps(),
|
|
resolveVerifiedInferenceRoute,
|
|
verifyBoundInference: vi.fn(async () => ({
|
|
ok: true as const,
|
|
modelRef: changedBinding.execution.modelLabel,
|
|
latencyMs: 1,
|
|
binding: changedBinding,
|
|
})),
|
|
},
|
|
}),
|
|
).resolves.toBeNull();
|
|
expect(resolveVerifiedInferenceRoute).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects route drift after an opaque liveness turn", async () => {
|
|
const binding = createBinding();
|
|
const resolveVerifiedInferenceRoute = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(binding.execution)
|
|
.mockResolvedValueOnce(null);
|
|
|
|
await expect(
|
|
resolvePersistentApplyInference({
|
|
binding,
|
|
runtime,
|
|
deps: {
|
|
...currentOwnerPluginArtifactDeps(),
|
|
resolveVerifiedInferenceRoute,
|
|
verifyBoundInference: vi.fn(async () => ({
|
|
ok: true as const,
|
|
modelRef: binding.execution.modelLabel,
|
|
latencyMs: 1,
|
|
binding,
|
|
})),
|
|
},
|
|
}),
|
|
).resolves.toBeNull();
|
|
expect(resolveVerifiedInferenceRoute).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("activateSetupInference Codex configuration", () => {
|
|
it.each([
|
|
{
|
|
name: "omitted",
|
|
config: {} satisfies OpenClawConfig,
|
|
expectedSupervision: undefined,
|
|
},
|
|
{
|
|
name: "an empty object",
|
|
config: {
|
|
plugins: {
|
|
entries: { codex: { config: { supervision: {} } } },
|
|
},
|
|
} satisfies OpenClawConfig,
|
|
expectedSupervision: {},
|
|
},
|
|
])("does not add Codex supervision when it is $name", async (testCase) => {
|
|
const { result, persistedConfig, refreshPluginRegistry, transformConfig } =
|
|
await runCodexSetupWithFinalConfig({
|
|
currentConfig: testCase.config,
|
|
sourceConfig: testCase.config,
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(persistedConfig.plugins?.entries?.codex).toMatchObject({
|
|
enabled: true,
|
|
config: { appServer: { transport: "stdio", homeScope: "agent" } },
|
|
});
|
|
expect(persistedConfig.plugins?.entries?.codex?.config?.supervision).toEqual(
|
|
testCase.expectedSupervision,
|
|
);
|
|
expect(transformConfig).toHaveBeenCalledOnce();
|
|
expect(refreshPluginRegistry).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("preserves an explicit Codex supervision opt-out from the latest config", async () => {
|
|
const config = {
|
|
plugins: {
|
|
entries: {
|
|
codex: {
|
|
enabled: false,
|
|
config: {
|
|
discovery: { enabled: true },
|
|
supervision: { enabled: false, allowRawTranscripts: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
|
|
const { result, persistedConfig } = await runCodexSetupWithFinalConfig({
|
|
currentConfig: config,
|
|
sourceConfig: config,
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(persistedConfig.plugins?.entries?.codex).toMatchObject({
|
|
enabled: true,
|
|
config: {
|
|
appServer: { transport: "stdio", homeScope: "agent" },
|
|
discovery: { enabled: true },
|
|
supervision: { enabled: false, allowRawTranscripts: true },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("preserves a normalized Codex supervision opt-out", async () => {
|
|
const config = {
|
|
plugins: {
|
|
allow: [" CODEX "],
|
|
entries: {
|
|
" CODEX ": {
|
|
config: {
|
|
appServer: { transport: "websocket", url: "ws://127.0.0.1:4500" },
|
|
supervision: { enabled: false },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
|
|
const { result, persistedConfig } = await runCodexSetupWithFinalConfig({
|
|
currentConfig: config,
|
|
sourceConfig: config,
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(persistedConfig.plugins?.allow).toEqual(["codex"]);
|
|
expect(persistedConfig.plugins?.entries).toMatchObject({
|
|
codex: {
|
|
enabled: true,
|
|
config: {
|
|
appServer: { transport: "stdio", url: "ws://127.0.0.1:4500", homeScope: "agent" },
|
|
supervision: { enabled: false },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("preserves an include-owned Codex supervision opt-out without copying it to root", async () => {
|
|
const resolvedSource = {
|
|
plugins: {
|
|
entries: {
|
|
codex: { config: { supervision: { enabled: false } } },
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
|
|
const { result, persistedConfig } = await runCodexSetupWithFinalConfig({
|
|
initialConfig: resolvedSource,
|
|
currentConfig: {},
|
|
currentRuntimeConfig: resolvedSource,
|
|
sourceConfig: {},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(persistedConfig.plugins?.entries?.codex).toMatchObject({
|
|
enabled: true,
|
|
config: { appServer: { transport: "stdio", homeScope: "agent" } },
|
|
});
|
|
});
|
|
|
|
it("fails closed when effective plugin policy changes before the success commit", async () => {
|
|
const denied = { plugins: { deny: ["codex"] } } satisfies OpenClawConfig;
|
|
const { result, refreshPluginRegistry, transformConfig } = await runCodexSetupWithFinalConfig({
|
|
initialConfig: {},
|
|
currentConfig: denied,
|
|
sourceConfig: denied,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unavailable",
|
|
error: expect.stringContaining("blocked by denylist"),
|
|
});
|
|
// The Codex probe loads staged policy, then restores the persisted denied
|
|
// policy after the pre-commit rejection.
|
|
expect(refreshPluginRegistry).toHaveBeenCalledTimes(2);
|
|
expect(transformConfig).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("verifySetupInference", () => {
|
|
function configuredSnapshot() {
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
config: {
|
|
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
|
},
|
|
};
|
|
}
|
|
|
|
it.each([
|
|
["missing config", { exists: false, valid: true, config: {} }],
|
|
["missing default-agent model", { exists: true, valid: true, config: {} }],
|
|
])("rejects %s before starting a model", async (_label, snapshot) => {
|
|
const runEmbeddedAgent = vi.fn();
|
|
const createTempDir = vi.fn(makeTempDir);
|
|
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => snapshot) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "unavailable" });
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(createTempDir).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports invalid config without starting a live check", async () => {
|
|
const runEmbeddedAgent = vi.fn();
|
|
const createTempDir = vi.fn(makeTempDir);
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({
|
|
exists: true,
|
|
valid: false,
|
|
path: "/tmp/openclaw.json",
|
|
issues: [{ path: "agents.defaults.model", message: "Expected a model reference" }],
|
|
config: {},
|
|
})) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "format",
|
|
error: expect.stringContaining("agents.defaults.model: Expected a model reference"),
|
|
});
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
expect(createTempDir).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns a passing live check without persisting setup", async () => {
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => configuredSnapshot()) as never,
|
|
runEmbeddedAgent: vi.fn(async () => successfulRun("openai", "gpt-5.5")) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.5" });
|
|
});
|
|
|
|
it("locks the exact winning profile into a bound OpenClaw session", async () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-5.5" },
|
|
models: { "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } } },
|
|
},
|
|
},
|
|
auth: {
|
|
profiles: {
|
|
"openai:p1": { provider: "openai", mode: "api_key" },
|
|
"openai:p2": { provider: "openai", mode: "api_key" },
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const profiles = {
|
|
"openai:p1": { type: "api_key" as const, provider: "openai", key: "key-1" },
|
|
"openai:p2": { type: "api_key" as const, provider: "openai", key: "key-2" },
|
|
};
|
|
const verifiedAuth = {
|
|
apiKey: "key-2",
|
|
profileId: "openai:p2",
|
|
source: "profile:openai:p2",
|
|
mode: "api-key" as const,
|
|
};
|
|
const verifiedAuthFingerprint = fingerprintResolvedProviderAuth(verifiedAuth);
|
|
if (!verifiedAuthFingerprint) {
|
|
throw new Error("missing test auth fingerprint");
|
|
}
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: {
|
|
authProfileId?: string;
|
|
authProfileIdSource?: string;
|
|
onSuccessfulAuthBinding?: (binding: {
|
|
authProfileId?: string;
|
|
agentHarnessId?: string;
|
|
authFingerprint?: string;
|
|
}) => void;
|
|
}) => {
|
|
params.onSuccessfulAuthBinding?.({
|
|
authProfileId: "openai:p2",
|
|
agentHarnessId: "openclaw",
|
|
authFingerprint: verifiedAuthFingerprint,
|
|
});
|
|
return successfulRun("openai", "gpt-5.5");
|
|
},
|
|
);
|
|
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
bindSession: true,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({ exists: true, valid: true, config })) as never,
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({ version: 1, profiles })) as never,
|
|
ensureAuthProfileStore: vi.fn(() => ({ version: 1, profiles })) as never,
|
|
resolveApiKeyForProvider: vi.fn(async () => verifiedAuth),
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
binding: {
|
|
auth: { authProfileId: "openai:p2", authFingerprint: verifiedAuthFingerprint },
|
|
execution: { authProfileId: "openai:p2", modelLabel: "openai/gpt-5.5" },
|
|
},
|
|
});
|
|
expect(runEmbeddedAgent).toHaveBeenCalledTimes(2);
|
|
expect(runEmbeddedAgent.mock.calls[0]?.[0].authProfileId).toBeUndefined();
|
|
expect(runEmbeddedAgent.mock.calls[1]?.[0]).toMatchObject({
|
|
authProfileId: "openai:p2",
|
|
authProfileIdSource: "user",
|
|
});
|
|
});
|
|
|
|
it("rejects an owner plugin replacement during the live inference turn", async () => {
|
|
const profileId = "openai:verified";
|
|
const credential = {
|
|
type: "api_key" as const,
|
|
provider: "openai",
|
|
key: "verified-key",
|
|
};
|
|
const authFingerprint = fingerprintAuthProfileCredential({ profileId, credential });
|
|
if (!authFingerprint) {
|
|
throw new Error("missing test auth fingerprint");
|
|
}
|
|
const config = {
|
|
agents: { defaults: { model: `openai/gpt-5.5@${profileId}` } },
|
|
auth: { profiles: { [profileId]: { provider: "openai", mode: "api_key" } } },
|
|
} satisfies OpenClawConfig;
|
|
const captureSystemAgentOwnerPluginArtifacts = vi.fn(() => ({
|
|
ownerPluginIds: ["openai"],
|
|
ownerPluginArtifacts: [{ pluginId: "openai", fingerprint: "openai-runtime-v1" }],
|
|
}));
|
|
const createChangedVerifiedInferenceBinding = vi.fn(async () => ({
|
|
ownerPluginIds: ["openai"],
|
|
ownerPluginArtifacts: [{ pluginId: "openai", fingerprint: "openai-runtime-v2" }],
|
|
})) as never;
|
|
const runEmbeddedAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
params.onSuccessfulAuthBinding?.({
|
|
authProfileId: profileId,
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint,
|
|
});
|
|
return successfulRun("openai", "gpt-5.5");
|
|
});
|
|
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
bindSession: true,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => ({ exists: true, valid: true, config })) as never,
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({
|
|
version: 1,
|
|
profiles: { [profileId]: credential },
|
|
})) as never,
|
|
ensureAuthProfileStore: vi.fn(() => ({
|
|
version: 1,
|
|
profiles: { [profileId]: credential },
|
|
})) as never,
|
|
resolveApiKeyForProvider: vi.fn(async () => ({
|
|
apiKey: credential.key,
|
|
profileId,
|
|
source: `profile:${profileId}`,
|
|
mode: "api-key" as const,
|
|
})),
|
|
captureSystemAgentOwnerPluginArtifacts,
|
|
createSystemAgentVerifiedInferenceBinding: createChangedVerifiedInferenceBinding,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "auth",
|
|
error: expect.stringContaining("owner changed"),
|
|
});
|
|
expect(captureSystemAgentOwnerPluginArtifacts).toHaveBeenCalledOnce();
|
|
expect(createChangedVerifiedInferenceBinding).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("binds a runtime-only Codex profile after activation and runs the first OpenClaw turn", async () => {
|
|
const stateDir = await makeTempDir();
|
|
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
|
const profileId = "openai:default";
|
|
const credential = {
|
|
type: "oauth" as const,
|
|
provider: "openai",
|
|
access: "test-access",
|
|
refresh: "test-refresh",
|
|
expires: Date.now() + 3_600_000,
|
|
};
|
|
const authFingerprint = fingerprintAuthProfileCredential({ profileId, credential });
|
|
if (!authFingerprint) {
|
|
throw new Error("missing external Codex auth fingerprint");
|
|
}
|
|
// This is the authored route produced by a successful Codex activation:
|
|
// model + harness persist, while the CLI-owned credential stays runtime-only.
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-5.6-sol" },
|
|
models: {
|
|
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
|
},
|
|
},
|
|
},
|
|
plugins: { entries: { codex: { enabled: true } } },
|
|
} satisfies OpenClawConfig;
|
|
const externalStore = vi.fn(
|
|
(_agentDir?: string, options?: { externalCliProviderIds?: Iterable<string> }) => {
|
|
const exposeCodexProfile = Array.from(options?.externalCliProviderIds ?? []).includes(
|
|
"openai",
|
|
);
|
|
return {
|
|
version: 1,
|
|
profiles: exposeCodexProfile ? { [profileId]: credential } : {},
|
|
runtimeExternalProfileIds: exposeCodexProfile ? [profileId] : [],
|
|
runtimeExternalProfileIdsAuthoritative: true,
|
|
};
|
|
},
|
|
);
|
|
const runEmbeddedAgent = vi.fn(async (params: SuccessfulRunParams) => {
|
|
params.onSuccessfulAuthBinding?.({
|
|
authProfileId: profileId,
|
|
...successfulAgentHarnessBinding(params),
|
|
authFingerprint,
|
|
});
|
|
return successfulRun("openai", "gpt-5.6-sol");
|
|
});
|
|
const readConfigFileSnapshot = vi.fn(async () => ({
|
|
exists: true,
|
|
valid: true,
|
|
config,
|
|
runtimeConfig: config,
|
|
sourceConfig: config,
|
|
}));
|
|
const validateAgentHarnessRuntimeArtifact = vi.fn(async () => true);
|
|
const createVerifiedInferenceBinding = vi.fn(
|
|
(params: Parameters<typeof createSystemAgentVerifiedInferenceBinding>[0]) =>
|
|
createSystemAgentVerifiedInferenceBinding({
|
|
...params,
|
|
deps: { ...params.deps, validateAgentHarnessRuntimeArtifact },
|
|
}),
|
|
);
|
|
|
|
try {
|
|
const verification = await verifySetupInference({
|
|
runtime,
|
|
bindSession: true,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
loadAuthProfileStoreForRuntime: externalStore as never,
|
|
ensureAuthProfileStore: externalStore as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createSystemAgentVerifiedInferenceBinding: createVerifiedInferenceBinding,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
expect(verification).toMatchObject({
|
|
ok: true,
|
|
modelRef: "openai/gpt-5.6-sol",
|
|
binding: {
|
|
auth: { authProfileId: profileId, authFingerprint },
|
|
execution: {
|
|
authProfileId: profileId,
|
|
agentHarnessRuntimeOverride: "codex",
|
|
},
|
|
},
|
|
});
|
|
if (!verification.ok) {
|
|
throw new Error(verification.error);
|
|
}
|
|
|
|
const session = createSystemAgentSession(verification.binding);
|
|
try {
|
|
const reply = await runSystemAgentTurnWithDeps(
|
|
{
|
|
input: "continue setup",
|
|
overview: { defaultModel: "openai/gpt-5.6-sol" } as never,
|
|
surface: "gateway",
|
|
approvalArmed: false,
|
|
session,
|
|
},
|
|
{
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
ensureAuthProfileStore: externalStore as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
validateAgentHarnessRuntimeArtifact,
|
|
},
|
|
);
|
|
expect(reply).toMatchObject({ text: "OK", modelLabel: "openai/gpt-5.6-sol" });
|
|
} finally {
|
|
await cleanupSystemAgentSession(session);
|
|
}
|
|
|
|
expect(runEmbeddedAgent).toHaveBeenCalledTimes(3);
|
|
expect(runEmbeddedAgent.mock.calls[0]?.[0].authProfileId).toBeUndefined();
|
|
expect(runEmbeddedAgent.mock.calls[1]?.[0]).toMatchObject({
|
|
authProfileId: profileId,
|
|
authProfileIdSource: "user",
|
|
});
|
|
expect(runEmbeddedAgent.mock.calls[2]?.[0]).toMatchObject({
|
|
authProfileId: profileId,
|
|
authProfileIdSource: "user",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
agentId: "openclaw",
|
|
toolsAllow: ["openclaw"],
|
|
});
|
|
const systemAgentTurnParams = runEmbeddedAgent.mock.calls[2]?.[0];
|
|
expect(systemAgentTurnParams).toBeDefined();
|
|
expect((systemAgentTurnParams as { config?: OpenClawConfig }).config).toBe(
|
|
verification.binding.execution.runConfig,
|
|
);
|
|
expect(validateAgentHarnessRuntimeArtifact).toHaveBeenCalledWith({
|
|
harnessId: "codex",
|
|
artifact: testCodexRuntimeArtifact,
|
|
});
|
|
expect(externalStore).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
readOnly: true,
|
|
allowKeychainPrompt: false,
|
|
config,
|
|
externalCliProviderIds: ["openai"],
|
|
}),
|
|
);
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("does not repeat an unbound verification after automatic profile selection", async () => {
|
|
const config = {
|
|
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
|
auth: {
|
|
profiles: {
|
|
"openai:p1": { provider: "openai", mode: "api_key" },
|
|
"openai:p2": { provider: "openai", mode: "api_key" },
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const profiles = {
|
|
"openai:p1": { type: "api_key" as const, provider: "openai", key: "key-1" },
|
|
"openai:p2": { type: "api_key" as const, provider: "openai", key: "key-2" },
|
|
};
|
|
const runEmbeddedAgent = vi.fn(
|
|
async (params: {
|
|
onSuccessfulAuthBinding?: (binding: { authProfileId?: string }) => void;
|
|
}) => {
|
|
params.onSuccessfulAuthBinding?.({ authProfileId: "openai:p2" });
|
|
return successfulRun("openai", "gpt-5.5");
|
|
},
|
|
);
|
|
|
|
const result = await verifySetupInferenceConfig({
|
|
config,
|
|
runtime,
|
|
deps: {
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({ version: 1, profiles })) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.5" });
|
|
expect(runEmbeddedAgent).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects a configured route that changes during its live check", async () => {
|
|
const initialConfig = {
|
|
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
|
} satisfies OpenClawConfig;
|
|
const changedConfig = {
|
|
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-8" } } },
|
|
} satisfies OpenClawConfig;
|
|
const readConfigFileSnapshot = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ exists: true, valid: true, config: initialConfig })
|
|
.mockResolvedValueOnce({ exists: true, valid: true, config: changedConfig });
|
|
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
deps: {
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runEmbeddedAgent: vi.fn(async () => successfulRun("openai", "gpt-5.5")) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unknown",
|
|
error: expect.stringContaining("route changed during its live test"),
|
|
});
|
|
expect(readConfigFileSnapshot).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("probes the configured default agent's exact embedded runtime", async () => {
|
|
const runEmbeddedAgent = vi.fn(async () => successfulRun("openai", "gpt-5.5"));
|
|
|
|
const result = await verifySetupInferenceConfig({
|
|
config: {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
model: { primary: "openai/gpt-5.5" },
|
|
models: {
|
|
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
runtime,
|
|
deps: {
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, modelRef: "openai/gpt-5.5" });
|
|
expect(runEmbeddedAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
agentId: "openclaw",
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
authProfileStateMode: "read-only",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("probes the configured default agent CLI auth owner", async () => {
|
|
const agentDir = "/configured/ops-agent";
|
|
const runCliAgent = vi.fn(async () => successfulRun("claude-cli", "claude-opus-4-8"));
|
|
|
|
const result = await verifySetupInferenceConfig({
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
cliBackends: {
|
|
"claude-cli": { command: "claude" },
|
|
},
|
|
},
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir,
|
|
model: { primary: "claude-cli/claude-opus-4-8@claude-cli:ops" },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: runCliAgent as never,
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({
|
|
version: 1,
|
|
profiles: {
|
|
"claude-cli:ops": {
|
|
type: "oauth",
|
|
provider: "claude-cli",
|
|
access: "test-access",
|
|
refresh: "test-refresh",
|
|
expires: Date.now() + 3_600_000,
|
|
},
|
|
},
|
|
})) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
modelRef: "claude-cli/claude-opus-4-8",
|
|
});
|
|
expect(runCliAgent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider: "claude-cli",
|
|
model: "claude-opus-4-8",
|
|
agentDir,
|
|
authProfileId: "claude-cli:ops",
|
|
executionMode: "side-question",
|
|
disableTools: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "missing", profiles: {} },
|
|
{
|
|
name: "wrong-owner",
|
|
profiles: {
|
|
"openai:locked": {
|
|
type: "api_key" as const,
|
|
provider: "anthropic",
|
|
key: "test-key",
|
|
},
|
|
},
|
|
},
|
|
])("rejects a $name embedded profile before inference", async ({ profiles }) => {
|
|
const runEmbeddedAgent = vi.fn();
|
|
const result = await verifySetupInferenceConfig({
|
|
config: {
|
|
agents: { defaults: { model: "openai/gpt-5.5@openai:locked" } },
|
|
},
|
|
runtime,
|
|
deps: {
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({ version: 1, profiles })) as never,
|
|
runEmbeddedAgent: runEmbeddedAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ name: "missing", profiles: {} },
|
|
{
|
|
name: "wrong-owner",
|
|
profiles: {
|
|
"claude-cli:locked": {
|
|
type: "api_key" as const,
|
|
provider: "openai",
|
|
key: "test-key",
|
|
},
|
|
},
|
|
},
|
|
])("rejects a $name CLI profile before inference", async ({ profiles }) => {
|
|
const runCliAgent = vi.fn();
|
|
const result = await verifySetupInferenceConfig({
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
model: "claude-cli/claude-opus-4-8@claude-cli:locked",
|
|
cliBackends: { "claude-cli": { command: "claude" } },
|
|
},
|
|
},
|
|
},
|
|
runtime,
|
|
deps: {
|
|
loadAuthProfileStoreForRuntime: vi.fn(() => ({ version: 1, profiles })) as never,
|
|
runCliAgent: runCliAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
expect(runCliAgent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "Gemini CLI OAuth",
|
|
profileId: "google-gemini-cli:user@example.test",
|
|
profileProvider: "google-gemini-cli",
|
|
credential: {
|
|
type: "oauth" as const,
|
|
provider: "google-gemini-cli",
|
|
access: "test-access",
|
|
refresh: "test-refresh",
|
|
expires: Date.now() + 3_600_000,
|
|
email: "user@example.test",
|
|
},
|
|
},
|
|
{
|
|
name: "canonical Google API key fallback",
|
|
profileId: "google:default",
|
|
profileProvider: "google",
|
|
credential: {
|
|
type: "api_key" as const,
|
|
provider: "google",
|
|
key: "test-google-key",
|
|
},
|
|
},
|
|
])("resolves $name but rejects Gemini CLI as a setup verifier", async (testCase) => {
|
|
const stateDir = await makeTempDir();
|
|
const agentDir = path.join(stateDir, "agent");
|
|
const runCliAgent = vi.fn(async () =>
|
|
successfulRun("google-gemini-cli", "gemini-3.1-pro-preview"),
|
|
);
|
|
const modelRef = "google/gemini-3.1-pro-preview";
|
|
const config: OpenClawConfig = {
|
|
auth: {
|
|
order: { [testCase.profileProvider]: [testCase.profileId] },
|
|
},
|
|
agents: {
|
|
defaults: { cliBackends: { "google-gemini-cli": { command: "gemini" } } },
|
|
list: [
|
|
{
|
|
id: "ops",
|
|
default: true,
|
|
agentDir,
|
|
model: { primary: modelRef },
|
|
models: {
|
|
[modelRef]: { agentRuntime: { id: "google-gemini-cli" } },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
resolveAgentDir(config, "ops");
|
|
await upsertAuthProfileWithLock({
|
|
profileId: testCase.profileId,
|
|
credential: testCase.credential,
|
|
agentDir,
|
|
});
|
|
|
|
try {
|
|
const route = await resolveSystemAgentConfiguredRouteFromConfig(config);
|
|
expect(route).toMatchObject({
|
|
runner: "cli",
|
|
provider: "google-gemini-cli",
|
|
authProfileId: testCase.profileId,
|
|
});
|
|
|
|
const result = await verifySetupInferenceConfig({
|
|
config,
|
|
runtime,
|
|
deps: {
|
|
runCliAgent: runCliAgent as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
status: "unavailable",
|
|
error: expect.stringContaining("no hard tool-free mode"),
|
|
});
|
|
expect(runCliAgent).not.toHaveBeenCalled();
|
|
} finally {
|
|
await removeOAuthTestTempRoot(stateDir);
|
|
}
|
|
});
|
|
|
|
it("redacts live-check failures without writing config or auth", async () => {
|
|
const secret = "sk-verifysetupsecret123"; // pragma: allowlist secret
|
|
const result = await verifySetupInference({
|
|
runtime,
|
|
timeoutMs: 50,
|
|
deps: {
|
|
readConfigFileSnapshot: vi.fn(async () => configuredSnapshot()) as never,
|
|
runEmbeddedAgent: vi.fn(async () => {
|
|
throw new Error(`401 invalid_api_key OPENAI_API_KEY=${secret}`);
|
|
}) as never,
|
|
createTempDir: makeTempDir,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: false, status: "auth" });
|
|
if (!result.ok) {
|
|
expect(result.error).not.toContain(secret);
|
|
expect(result.error).toContain("OPENAI_API_KEY=");
|
|
}
|
|
});
|
|
});
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|