Files
openclaw/src/system-agent/setup-apply.test.ts
Peter Steinberger a6a0716486 feat(setup): rename Crestodian to OpenClaw system agent
User-facing name is now OpenClaw (the system speaks); internal code name is
system-agent. Gateway methods crestodian.* -> openclaw.chat/openclaw.setup.*,
agent tool -> openclaw, reserved agent ids openclaw + retired crestodian.
openclaw setup routes: onboarding flags -> onboard, -m/--yes -> system agent,
bare configured interactive -> OpenClaw chat, unconfigured -> onboarding.
Hidden crestodian CLI and /crestodian TUI aliases kept; docs moved to
docs/cli/openclaw.md with redirect stub. macOS/Android strings in lockstep.

Refs #107237
2026-07-14 11:03:02 -07:00

857 lines
29 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import * as configModule from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { RuntimeEnv } from "../runtime.js";
import { projectDefaultInferenceRoute } from "./inference-route.js";
type ConfigSnapshot = {
exists: boolean;
valid: boolean;
path: string;
hash: string | null;
config: OpenClawConfig;
sourceConfig: OpenClawConfig;
runtimeConfig?: OpenClawConfig;
issues: Array<{ path?: string; message: string }>;
};
type CommitTransform = (
currentConfig: OpenClawConfig,
context: {
previousHash: string | null;
snapshot: ConfigSnapshot;
attempt: number;
},
) =>
| { nextConfig: OpenClawConfig; result?: unknown }
| Promise<{ nextConfig: OpenClawConfig; result?: unknown }>;
const mocks = vi.hoisted(() => ({
state: {
initialSnapshot: {} as ConfigSnapshot,
commitConfig: {} as OpenClawConfig,
commitSnapshot: {} as ConfigSnapshot,
commitPreviousHash: "probe" as string | null,
persistedConfig: undefined as OpenClawConfig | undefined,
},
events: [] as string[],
readSnapshot: vi.fn<() => Promise<ConfigSnapshot>>(),
readVerifiedSnapshot: vi.fn<() => Promise<ConfigSnapshot>>(),
readVerifiedSnapshotWithPluginMetadata: vi.fn(),
commit: vi.fn(),
configureGateway: vi.fn(),
ensureWorkspace: vi.fn(),
ensureGatewayService: vi.fn(),
refreshPluginRegistry: vi.fn(),
updateExecApprovals: vi.fn(),
}));
vi.mock("../config/config.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config/config.js")>()),
readConfigFileSnapshot: mocks.readVerifiedSnapshot,
readConfigFileSnapshotWithPluginMetadata: mocks.readVerifiedSnapshotWithPluginMetadata,
}));
vi.mock("../wizard/setup.shared.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../wizard/setup.shared.js")>()),
readSetupConfigFileSnapshot: mocks.readSnapshot,
}));
vi.mock("../commands/onboard-helpers.js", () => ({
applyWizardMetadata: (config: OpenClawConfig) => ({
...config,
wizard: {
...config.wizard,
lastRunAt: "2026-07-10T00:00:00.000Z",
lastRunVersion: "test",
lastRunCommand: "onboard",
lastRunMode: "local",
},
}),
ensureWorkspaceAndSessions: mocks.ensureWorkspace,
resolveLocalControlUiProbeLinks: ({ port }: { port: number }) => ({
wsUrl: `ws://127.0.0.1:${port}`,
}),
waitForGatewayReachable: vi.fn(async () => ({ ok: true })),
}));
vi.mock("../plugins/install-record-commit.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../plugins/install-record-commit.js")>()),
transformConfigWithPendingPluginInstalls: mocks.commit,
}));
vi.mock("../wizard/setup.gateway-config.js", () => ({
configureGatewayForSetup: mocks.configureGateway,
}));
vi.mock("../wizard/setup.finalize.js", () => ({
ensureGatewayServiceForOnboarding: mocks.ensureGatewayService,
}));
vi.mock("../plugins/registry-refresh.js", () => ({
refreshPluginRegistryAfterConfigMutation: mocks.refreshPluginRegistry,
}));
vi.mock("../infra/exec-approvals.js", () => ({
updateExecApprovals: mocks.updateExecApprovals,
}));
vi.mock("../agents/agent-scope.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../agents/agent-scope.js")>()),
resolveAgentDir: (config: OpenClawConfig, agentId: string) =>
config.agents?.list?.find((agent) => agent.id === agentId)?.agentDir ?? `/agents/${agentId}`,
}));
import { applySystemAgentModelSelection, applySystemAgentSetup } from "./setup-apply.js";
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
function snapshot(hash: string | null, config: OpenClawConfig): ConfigSnapshot {
return {
exists: hash !== null,
valid: true,
path: "/tmp/openclaw.json",
hash,
config,
sourceConfig: config,
runtimeConfig: config,
issues: [],
};
}
function codexPluginMetadataSnapshot(homeScope: "agent" | "user") {
return {
manifestRegistry: {
diagnostics: [],
plugins: [
{
id: "codex",
origin: "global",
channels: [],
providers: [],
cliBackends: [],
skills: [],
settingsFiles: [],
hooks: [],
rootDir: "/tmp/codex",
source: "/tmp/codex/index.js",
manifestPath: "/tmp/codex/openclaw.plugin.json",
configSchema: {
type: "object",
additionalProperties: false,
properties: {
codexDynamicToolsLoading: { type: "string", default: "searchable" },
appServer: {
type: "object",
additionalProperties: false,
properties: {
transport: { type: "string", default: "stdio" },
homeScope: { type: "string", default: homeScope },
requestTimeoutMs: { type: "number", default: 60_000 },
},
},
},
},
},
],
},
} as never;
}
function materializePluginDefaults(
config: OpenClawConfig,
pluginMetadataSnapshot: ReturnType<typeof codexPluginMetadataSnapshot>,
): OpenClawConfig {
const result = configModule.validateConfigObjectWithPlugins(config, { pluginMetadataSnapshot });
if (!result.ok) {
throw new Error(result.issues[0]?.message ?? "test config failed validation");
}
return result.config;
}
function baseParams(overrides: Partial<Parameters<typeof applySystemAgentSetup>[0]> = {}) {
return {
workspace: "/tmp/openclaw-workspace",
surface: "gateway" as const,
runtime,
...overrides,
};
}
describe("applySystemAgentModelSelection", () => {
it("clears stale harness pins in both model scopes for a native route", async () => {
const config = {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
list: [
{
id: "work",
default: true,
model: "openai/gpt-5.5",
models: {
"openai/gpt-5.5": {
alias: "primary",
agentRuntime: { id: "codex" },
},
},
},
],
},
} satisfies OpenClawConfig;
const result = await applySystemAgentModelSelection({
config,
model: "openai/gpt-5.5",
});
expect(result.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime).toBeUndefined();
expect(result.agents?.list?.[0]?.models?.["openai/gpt-5.5"]).toEqual({ alias: "primary" });
expect(result.agents?.list?.[0]?.model).toBe("openai/gpt-5.5");
});
it("pins the verified credential without creating a global visibility map", async () => {
const result = await applySystemAgentModelSelection({
config: { agents: { defaults: { model: "openai/gpt-5.5" } } },
model: "openai/gpt-5.5",
authProfileId: "openai:verified",
});
expect(result.agents?.defaults?.model).toBe("openai/gpt-5.5@openai:verified");
expect(result.agents?.defaults?.models).toBeUndefined();
});
});
describe("applySystemAgentSetup transaction boundaries", () => {
beforeEach(() => {
vi.resetAllMocks();
mocks.events.length = 0;
const config: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
};
mocks.state.initialSnapshot = snapshot("probe", config);
mocks.state.commitConfig = structuredClone(config);
mocks.state.commitSnapshot = snapshot("probe", config);
mocks.state.commitPreviousHash = "probe";
mocks.state.persistedConfig = undefined;
mocks.readSnapshot.mockImplementation(async () => mocks.state.initialSnapshot);
mocks.readVerifiedSnapshot.mockImplementation(async () => mocks.state.initialSnapshot);
mocks.readVerifiedSnapshotWithPluginMetadata.mockImplementation(async () => ({
snapshot: await mocks.readVerifiedSnapshot(),
}));
mocks.commit.mockImplementation(async (params: { transform: CommitTransform }) => {
const result = await params.transform(structuredClone(mocks.state.commitConfig), {
previousHash: mocks.state.commitPreviousHash,
snapshot: mocks.state.commitSnapshot,
attempt: 0,
});
mocks.events.push("commit");
mocks.state.persistedConfig = result.nextConfig;
return {
nextConfig: result.nextConfig,
path: "/tmp/openclaw.json",
previousHash: mocks.state.commitPreviousHash,
persistedHash: "persisted",
result: result.result,
};
});
mocks.configureGateway.mockImplementation(
async ({
nextConfig,
quickstartGateway,
}: {
nextConfig: OpenClawConfig;
quickstartGateway: {
authMode: "token" | "password";
bind: "loopback" | "lan";
customBindHost?: string;
port: number;
token?: string;
};
}) => ({
nextConfig,
settings: {
authMode: quickstartGateway.authMode,
bind: quickstartGateway.bind,
customBindHost: quickstartGateway.customBindHost,
gatewayToken: quickstartGateway.token,
port: quickstartGateway.port,
},
}),
);
mocks.ensureWorkspace.mockImplementation(async () => {
mocks.events.push("workspace");
});
mocks.ensureGatewayService.mockResolvedValue({ installDaemon: false });
mocks.refreshPluginRegistry.mockResolvedValue(undefined);
mocks.updateExecApprovals.mockResolvedValue(undefined);
});
it.each([
{ expected: null, actual: "present" },
{ expected: "probe", actual: "different" },
])(
"rejects initial $expected -> $actual revision drift before writing",
async ({ expected, actual }) => {
mocks.state.initialSnapshot = snapshot(actual, {});
await expect(
applySystemAgentSetup(baseParams({ expectedConfigHash: expected })),
).rejects.toThrow("config changed while AI access was being tested");
expect(mocks.commit).not.toHaveBeenCalled();
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
},
);
it("preserves fresh-setup behavior for an explicitly verified absent revision", async () => {
const absent = snapshot(null, {});
mocks.state.initialSnapshot = absent;
mocks.state.commitConfig = {};
mocks.state.commitSnapshot = absent;
mocks.state.commitPreviousHash = null;
const result = await applySystemAgentSetup(baseParams({ expectedConfigHash: null }));
expect(result.configHashBefore).toBeNull();
expect(mocks.state.persistedConfig).toMatchObject({
agents: { defaults: { workspace: "/tmp/openclaw-workspace" } },
});
});
it("rejects invalid config before any setup mutation", async () => {
mocks.state.initialSnapshot = {
...snapshot("invalid", {}),
valid: false,
issues: [{ path: "agents", message: "bad agent config" }],
};
await expect(applySystemAgentSetup(baseParams())).rejects.toThrow("bad agent config");
expect(mocks.commit).not.toHaveBeenCalled();
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
});
it("rejects a configured user agent that collides with the privileged id", async () => {
const config = {
agents: {
defaults: { model: "openai/gpt-5.5" },
list: [{ id: "OpenClaw" }],
},
} satisfies OpenClawConfig;
mocks.state.initialSnapshot = snapshot("reserved", config);
await expect(applySystemAgentSetup(baseParams())).rejects.toThrow(
'Agent id "openclaw" is reserved',
);
expect(mocks.commit).not.toHaveBeenCalled();
});
it("rejects a configured user agent with the retired id", async () => {
const config = {
agents: {
defaults: { model: "openai/gpt-5.5" },
list: [{ id: "crestodian" }], // reserved retired id
},
} satisfies OpenClawConfig;
mocks.state.initialSnapshot = snapshot("reserved-retired", config);
await expect(applySystemAgentSetup(baseParams())).rejects.toThrow(
'Agent id "crestodian" is reserved', // reserved retired id
);
expect(mocks.commit).not.toHaveBeenCalled();
});
it("rechecks the probed revision inside the final transform", async () => {
mocks.state.commitPreviousHash = "concurrent";
await expect(
applySystemAgentSetup(baseParams({ expectedConfigHash: "probe" })),
).rejects.toThrow("config changed while AI access was being tested");
expect(mocks.state.persistedConfig).toBeUndefined();
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
});
it.each([
{
name: "default agent",
runtimeConfig: {
agents: {
defaults: { model: { primary: "openai/gpt-5.5" } },
list: [{ id: "other", default: true }],
},
},
error: "default agent changed",
},
{
name: "default model",
runtimeConfig: {
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-6" } } },
},
error: "default model changed",
},
])("rechecks the probed $name inside the final transform", async ({ runtimeConfig, error }) => {
mocks.state.commitSnapshot = snapshot("probe", runtimeConfig);
await expect(
applySystemAgentSetup(
baseParams({
expectedConfigHash: "probe",
expectedAgentId: "main",
expectedModelRef: "openai/gpt-5.5",
}),
),
).rejects.toThrow(error);
expect(mocks.state.persistedConfig).toBeUndefined();
});
it("rejects same-revision agent credential directory drift in the final snapshot", async () => {
mocks.state.commitSnapshot = snapshot("probe", {
agents: {
defaults: { model: { primary: "openai/gpt-5.5" } },
list: [{ id: "main", default: true, agentDir: "/agents/moved" }],
},
});
await expect(
applySystemAgentSetup(
baseParams({
expectedConfigHash: "probe",
expectedAgentId: "main",
expectedAgentDir: "/agents/main",
}),
),
).rejects.toThrow("agent credential location changed");
expect(mocks.state.persistedConfig).toBeUndefined();
});
it("folds plugin and auth config into one commit while preserving concurrent edits", async () => {
mocks.state.commitConfig = {
...mocks.state.commitConfig,
logging: { level: "debug" },
};
mocks.state.commitSnapshot = snapshot("probe", mocks.state.commitConfig);
const result = await applySystemAgentSetup(
baseParams({
expectedConfigHash: "probe",
expectedAgentId: "main",
expectedModelRef: "openai/gpt-5.5",
enablePluginId: "codex",
configPatch: { agents: { defaults: { maxConcurrent: 7 } } },
}),
);
expect(mocks.commit).toHaveBeenCalledOnce();
expect(mocks.state.persistedConfig).toMatchObject({
agents: {
defaults: {
workspace: "/tmp/openclaw-workspace",
maxConcurrent: 7,
model: { primary: "openai/gpt-5.5" },
},
},
logging: { level: "debug" },
plugins: { entries: { codex: { enabled: true } } },
});
expect(result.configPath).toBe("/tmp/openclaw.json");
});
it("rejects route drift before opening the config transaction", async () => {
const current = {
agents: { defaults: { model: "openai/gpt-5.5" } },
} satisfies OpenClawConfig;
const verified = {
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
} satisfies OpenClawConfig;
mocks.state.initialSnapshot = snapshot("probe", current);
mocks.readVerifiedSnapshot.mockResolvedValue(snapshot("probe", current));
await expect(
applySystemAgentSetup(
baseParams({ expectedInferenceRoute: await projectDefaultInferenceRoute(verified) }),
),
).rejects.toThrow("changed before setup could start");
expect(mocks.commit).not.toHaveBeenCalled();
});
it("rejects resolved source drift hidden behind an unchanged root hash", async () => {
const stale = {
agents: { defaults: { model: "openai/gpt-5.5" } },
gateway: { port: 18789 },
} satisfies OpenClawConfig;
const current = {
...stale,
gateway: { port: 19000 },
} satisfies OpenClawConfig;
mocks.state.initialSnapshot = snapshot("same-root", stale);
mocks.readVerifiedSnapshot.mockResolvedValue(snapshot("same-root", current));
await expect(
applySystemAgentSetup(
baseParams({ expectedInferenceRoute: await projectDefaultInferenceRoute(current) }),
),
).rejects.toThrow("changed before setup could start");
expect(mocks.commit).not.toHaveBeenCalled();
});
it("rejects a setup candidate that changes the exact verified route identity", async () => {
const initial = {
agents: { defaults: { model: "openai/gpt-5.5" } },
} satisfies OpenClawConfig;
const initialSnapshot = snapshot("probe", initial);
mocks.state.initialSnapshot = initialSnapshot;
mocks.state.commitConfig = initial;
mocks.state.commitSnapshot = initialSnapshot;
mocks.readVerifiedSnapshot.mockResolvedValue(initialSnapshot);
await expect(
applySystemAgentSetup(
baseParams({
model: "anthropic/claude-opus-4-8",
expectedInferenceRoute: await projectDefaultInferenceRoute(initial),
}),
),
).rejects.toThrow("no longer preserves the exact verified inference route");
expect(mocks.state.persistedConfig).toBeUndefined();
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
});
it("rebuilds Gateway settings from the snapshot that wins a transaction retry", async () => {
const initial = {
agents: { defaults: { model: "openai/gpt-5.5" } },
gateway: {
port: 18789,
bind: "loopback",
auth: { mode: "token", token: "initial-token" },
},
} satisfies OpenClawConfig;
const concurrent = {
...initial,
gateway: {
...initial.gateway,
port: 19000,
bind: "lan",
auth: { mode: "token" as const, token: "concurrent-token" },
},
} satisfies OpenClawConfig;
const initialSnapshot = snapshot("hash-1", initial);
const concurrentSnapshot = snapshot("hash-2", concurrent);
mocks.state.initialSnapshot = initialSnapshot;
mocks.state.commitConfig = initial;
mocks.state.commitSnapshot = initialSnapshot;
let setupReads = 0;
mocks.readSnapshot.mockImplementation(async () => {
if (setupReads++ === 0) {
return initialSnapshot;
}
return snapshot("persisted", mocks.state.persistedConfig ?? concurrent);
});
let verifiedReads = 0;
mocks.readVerifiedSnapshot.mockImplementation(async () => {
verifiedReads += 1;
if (verifiedReads <= 2) {
return initialSnapshot;
}
if (verifiedReads === 3) {
return concurrentSnapshot;
}
return snapshot("persisted", mocks.state.persistedConfig ?? concurrent);
});
mocks.commit.mockImplementationOnce(async (params: { transform: CommitTransform }) => {
await params.transform(initial, {
previousHash: "hash-1",
snapshot: initialSnapshot,
attempt: 0,
});
const result = await params.transform(concurrent, {
previousHash: "hash-2",
snapshot: concurrentSnapshot,
attempt: 1,
});
mocks.events.push("commit");
mocks.state.persistedConfig = result.nextConfig;
return {
nextConfig: result.nextConfig,
path: "/tmp/openclaw.json",
previousHash: "hash-2",
persistedHash: "persisted",
result: result.result,
};
});
const expectedInferenceRoute = await projectDefaultInferenceRoute(initial);
await applySystemAgentSetup(baseParams({ expectedInferenceRoute, surface: "cli" }));
expect(mocks.configureGateway).toHaveBeenCalledTimes(2);
expect(mocks.configureGateway).toHaveBeenLastCalledWith(
expect.objectContaining({
baseConfig: concurrent,
localPort: 19000,
quickstartGateway: expect.objectContaining({ port: 19000, bind: "lan" }),
}),
);
expect(mocks.ensureGatewayService).toHaveBeenCalledWith(
expect.objectContaining({
settings: expect.objectContaining({
port: 19000,
bind: "lan",
gatewayToken: "concurrent-token",
}),
}),
);
});
it("revalidates the verified route after the config write", async () => {
const initial = {
agents: { defaults: { model: "openai/gpt-5.5" } },
} satisfies OpenClawConfig;
const drifted = {
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
} satisfies OpenClawConfig;
const initialSnapshot = snapshot("probe", initial);
const driftedSnapshot = snapshot("persisted", drifted);
mocks.state.initialSnapshot = initialSnapshot;
mocks.state.commitConfig = initial;
mocks.state.commitSnapshot = initialSnapshot;
mocks.readSnapshot
.mockResolvedValueOnce(initialSnapshot)
.mockResolvedValueOnce(driftedSnapshot);
mocks.readVerifiedSnapshot
.mockResolvedValueOnce(initialSnapshot)
.mockResolvedValueOnce(initialSnapshot)
.mockResolvedValueOnce(driftedSnapshot);
mocks.commit.mockImplementationOnce(async (params: { transform: CommitTransform }) => {
const result = await params.transform(initial, {
previousHash: "probe",
snapshot: initialSnapshot,
attempt: 0,
});
mocks.state.persistedConfig = drifted;
return {
nextConfig: drifted,
path: "/tmp/openclaw.json",
previousHash: "probe",
persistedHash: "persisted",
result: result.result,
};
});
await expect(
applySystemAgentSetup(
baseParams({ expectedInferenceRoute: await projectDefaultInferenceRoute(initial) }),
),
).rejects.toThrow("changed after the config write");
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
});
it("accepts persisted plugin defaults that match the verified runtime route", async () => {
const pluginMetadataSnapshot = codexPluginMetadataSnapshot("agent");
const sourceConfig = {
agents: { defaults: { model: "openai/gpt-5.5" } },
plugins: {
entries: {
codex: {
enabled: true,
config: { appServer: { transport: "stdio", homeScope: "agent" } },
},
},
},
} satisfies OpenClawConfig;
const initialSnapshot = {
...snapshot("probe", sourceConfig),
runtimeConfig: materializePluginDefaults(sourceConfig, pluginMetadataSnapshot),
};
const persistedSnapshot = () => {
const persisted = mocks.state.persistedConfig ?? sourceConfig;
return {
...snapshot("persisted", persisted),
runtimeConfig: materializePluginDefaults(persisted, pluginMetadataSnapshot),
};
};
mocks.state.initialSnapshot = initialSnapshot;
mocks.state.commitConfig = sourceConfig;
mocks.state.commitSnapshot = initialSnapshot;
mocks.readVerifiedSnapshot
.mockResolvedValueOnce(initialSnapshot)
.mockResolvedValueOnce(initialSnapshot)
.mockImplementation(async () => persistedSnapshot());
mocks.readVerifiedSnapshotWithPluginMetadata.mockImplementation(async () => ({
snapshot: persistedSnapshot(),
pluginMetadataSnapshot,
}));
await applySystemAgentSetup(
baseParams({
expectedInferenceRoute: await projectDefaultInferenceRoute(initialSnapshot.runtimeConfig),
}),
);
expect(mocks.ensureWorkspace).toHaveBeenCalledOnce();
});
it("rejects a materialized route that differs from the inference proof", async () => {
const sourceConfig = {
agents: { defaults: { model: "openai/gpt-5.5" } },
} satisfies OpenClawConfig;
const materializedConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
} satisfies OpenClawConfig;
const verifiedSnapshot = snapshot("probe", sourceConfig);
const persistedSnapshot = () => {
const persisted = mocks.state.persistedConfig ?? sourceConfig;
return {
...snapshot("persisted", persisted),
runtimeConfig: materializedConfig,
};
};
mocks.state.initialSnapshot = verifiedSnapshot;
mocks.state.commitConfig = sourceConfig;
mocks.state.commitSnapshot = verifiedSnapshot;
mocks.readVerifiedSnapshot
.mockResolvedValueOnce(verifiedSnapshot)
.mockResolvedValueOnce(verifiedSnapshot)
.mockImplementation(async () => persistedSnapshot());
mocks.readVerifiedSnapshotWithPluginMetadata.mockImplementation(async () => ({
snapshot: persistedSnapshot(),
}));
const validate = vi
.spyOn(configModule, "validateConfigObjectWithPlugins")
.mockReturnValue({ ok: true, config: materializedConfig, warnings: [] });
try {
await expect(
applySystemAgentSetup(
baseParams({
expectedInferenceRoute: await projectDefaultInferenceRoute(sourceConfig),
}),
),
).rejects.toThrow("materialized inference route");
} finally {
validate.mockRestore();
}
expect(mocks.ensureWorkspace).not.toHaveBeenCalled();
});
it("stops stale continuation before the next persistent effect", async () => {
const initial = {
agents: { defaults: { model: "openai/gpt-5.5" } },
auth: { order: { openai: ["openai:verified"] } },
} satisfies OpenClawConfig;
const initialSnapshot = snapshot("probe", initial);
const expectedInferenceRoute = await projectDefaultInferenceRoute(initial);
let currentConfig: OpenClawConfig = initial;
let currentHash = "probe";
mocks.state.initialSnapshot = initialSnapshot;
mocks.state.commitConfig = initial;
mocks.state.commitSnapshot = initialSnapshot;
let setupReads = 0;
mocks.readSnapshot.mockImplementation(async () =>
setupReads++ === 0 ? initialSnapshot : snapshot(currentHash, currentConfig),
);
mocks.readVerifiedSnapshot.mockImplementation(async () => snapshot(currentHash, currentConfig));
mocks.commit.mockImplementationOnce(async (params: { transform: CommitTransform }) => {
const result = await params.transform(currentConfig, {
previousHash: currentHash,
snapshot: snapshot(currentHash, currentConfig),
attempt: 0,
});
currentConfig = result.nextConfig;
currentHash = "persisted";
mocks.state.persistedConfig = result.nextConfig;
mocks.events.push("commit");
return {
nextConfig: result.nextConfig,
path: "/tmp/openclaw.json",
previousHash: "probe",
persistedHash: currentHash,
result: result.result,
};
});
mocks.ensureWorkspace.mockImplementationOnce(async () => {
currentConfig = {
...currentConfig,
auth: { order: { openai: ["openai:rotated"] } },
};
authorityValid = false;
});
let guardCalls = 0;
let authorityValid = true;
const authorityCommit = async <T>(effect: () => Promise<T> | T): Promise<T> => {
guardCalls += 1;
if (!authorityValid) {
throw new Error("verified inference binding changed");
}
return await effect();
};
await expect(
applySystemAgentSetup(baseParams({ expectedInferenceRoute }), { commit: authorityCommit }),
).rejects.toThrow("verified inference binding changed");
expect(guardCalls).toBe(3);
expect(mocks.ensureWorkspace).toHaveBeenCalledOnce();
expect(mocks.updateExecApprovals).not.toHaveBeenCalled();
});
it("finalizes setup against the source config held by the commit lock", async () => {
const sourceConfig = {
plugins: { entries: { codex: { config: { supervision: { enabled: false } } } } },
} satisfies OpenClawConfig;
mocks.state.commitSnapshot = {
...snapshot("probe", mocks.state.commitConfig),
sourceConfig,
};
const finalizeConfig = vi.fn((config: OpenClawConfig, source: OpenClawConfig) => ({
...config,
plugins: source.plugins,
}));
await applySystemAgentSetup(baseParams({ expectedConfigHash: "probe", finalizeConfig }));
expect(finalizeConfig).toHaveBeenCalledWith(expect.any(Object), sourceConfig);
expect(mocks.state.persistedConfig?.plugins).toEqual(sourceConfig.plugins);
});
it("returns visible post-commit workspace, approval, registry, and service failures", async () => {
mocks.ensureWorkspace.mockRejectedValueOnce(new Error("workspace exploded"));
mocks.updateExecApprovals.mockRejectedValueOnce(new Error("approval exploded"));
mocks.refreshPluginRegistry.mockRejectedValueOnce(new Error("registry exploded"));
mocks.ensureGatewayService.mockRejectedValueOnce(new Error("service exploded"));
const result = await applySystemAgentSetup(
baseParams({
expectedConfigHash: "probe",
enablePluginId: "codex",
refreshPluginRegistry: true,
surface: "cli",
}),
);
expect(mocks.events).toEqual(["commit"]);
expect(result.lines).toEqual(
expect.arrayContaining([
"Workspace files: workspace exploded",
"OpenClaw exec approval: approval exploded; local model harnesses may ask again.",
"Plugin registry refresh failed: registry exploded",
"Gateway service: service exploded",
]),
);
});
});