Files
openclaw/src/system-agent/operations.setup.test.ts
T
Peter Steinberger c2e0ce7b68 fix(ci): restore main validation gates (#107697)
* fix(ci): allow Unreleased notes for main validation

* test(ci): cover release smoke changelog intent

* test(ci): require release changelog policy propagation

* docs(agents): require review init after pushes

* chore(ci): prune stale max-lines baseline

* refactor(system-agent): split operation modules

* refactor(bonjour): keep ciao classification type private

* fix(bonjour): remove stale local classification alias
2026-07-14 12:11:25 -07:00

1027 lines
37 KiB
TypeScript

// OpenClaw operation tests cover rescue operation planning and execution.
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
import { SystemAgentInferenceUnavailableError } from "./inference-error.js";
import { executeSystemAgentOperation, isPersistentSystemAgentOperation } from "./operations.js";
import { createSystemAgentTestRuntime } from "./system-agent.test-helpers.js";
type TestConfig = Record<string, unknown>;
function parseLastJsonLine(raw: string): unknown {
const lastLine = raw.trim().split("\n").at(-1);
if (!lastLine) {
throw new Error("Expected audit log to contain at least one JSON line");
}
return JSON.parse(lastLine) as unknown;
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null) {
throw new Error(`${label} was not an object`);
}
return value as Record<string, unknown>;
}
function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
for (const [key, value] of Object.entries(fields)) {
expect(record[key]).toEqual(value);
}
}
function expectAuditRecord(
audit: unknown,
fields: Record<string, unknown>,
detailFields: Record<string, unknown>,
) {
const auditRecord = requireRecord(audit, "audit record");
expectRecordFields(auditRecord, fields);
expectRecordFields(requireRecord(auditRecord.details, "audit details"), detailFields);
}
const mockConfig = vi.hoisted(() => {
const initial = {};
const state = {
path: "/tmp/openclaw.json",
exists: true,
config: initial as TestConfig,
hash: "mock-hash-0" as string | undefined,
};
const cloneConfig = () => structuredClone(state.config);
const snapshot = () => {
const config = cloneConfig();
return {
path: state.path,
exists: state.exists,
raw: state.exists ? `${JSON.stringify(config)}\n` : null,
parsed: state.exists ? config : undefined,
sourceConfig: config,
resolved: config,
valid: state.exists,
runtimeConfig: config,
config,
hash: state.hash,
issues: state.exists ? [] : [{ path: "", message: "missing config" }],
warnings: [],
legacyIssues: [],
};
};
return {
reset() {
state.path = "/tmp/openclaw.json";
state.exists = true;
state.config = {};
state.hash = "mock-hash-0";
},
missing(pathLocal: string) {
state.path = pathLocal;
state.exists = false;
state.config = {};
state.hash = undefined;
},
currentConfig() {
return cloneConfig();
},
setConfig(config: TestConfig) {
state.config = structuredClone(config);
},
readConfigFileSnapshot: vi.fn(async () => snapshot()),
mutateConfigFile: vi.fn(
async (params: {
writeOptions?: {
preCommitRuntimePreflight?: (sourceConfig: TestConfig) => Promise<unknown>;
};
mutate: (
draft: TestConfig,
context: { snapshot: ReturnType<typeof snapshot> },
) => Promise<void> | void;
}) => {
const before = snapshot();
const draft = cloneConfig();
await params.mutate(draft, { snapshot: before });
await params.writeOptions?.preCommitRuntimePreflight?.(structuredClone(draft));
state.exists = true;
state.config = draft;
state.hash = "mock-hash-1";
return {
path: state.path,
previousHash: before.hash ?? null,
persistedHash: before.hash ?? null,
snapshot: before,
nextConfig: cloneConfig(),
result: undefined,
};
},
),
};
});
vi.mock("./probes.js", () => ({
probeLocalCommand: vi.fn(async (command: string) => ({
command,
found: false,
error: "not found",
})),
probeGatewayUrl: vi.fn(async (url: string) => ({ reachable: false, url, error: "offline" })),
}));
vi.mock("./overview.js", () => ({
formatSystemAgentOverview: () => "Default model: openai/gpt-5.5",
loadSystemAgentOverview: vi.fn(async () => ({
defaultAgentId: "main",
defaultModel: undefined,
agents: [
{ id: "main", isDefault: true },
{ id: "work", isDefault: false, model: "openai/gpt-5.2" },
],
config: { path: "/tmp/openclaw.json", exists: true, valid: true, issues: [], hash: null },
tools: {
codex: { command: "codex", found: false, error: "not found" },
claude: { command: "claude", found: false, error: "not found" },
gemini: { command: "gemini", found: false, error: "not found" },
apiKeys: { openai: true, anthropic: false },
},
gateway: {
url: "ws://127.0.0.1:18789",
source: "local loopback",
reachable: false,
error: "offline",
},
references: {
docsUrl: "https://docs.openclaw.ai",
sourceUrl: "https://github.com/openclaw/openclaw",
},
})),
}));
vi.mock("../config/config.js", () => ({
mutateConfigFile: mockConfig.mutateConfigFile,
readConfigFileSnapshot: mockConfig.readConfigFileSnapshot,
}));
const opTempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("parseSystemAgentOperation", () => {
let stateDirSnapshot: ReturnType<typeof captureEnv> | undefined;
beforeEach(() => {
mockConfig.reset();
stateDirSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
});
afterEach(() => {
stateDirSnapshot?.restore();
vi.unstubAllEnvs();
});
it("runs setup bootstrap only after approval and audits it", async () => {
const tempDir = opTempDirs.make("openclaw-setup-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const { runtime, lines } = createSystemAgentTestRuntime();
mockConfig.setConfig({ agents: { defaults: { model: { primary: "openai/gpt-5.5" } } } });
const applySetup = vi.fn(async () => ({
configPath: path.join(tempDir, "openclaw.json"),
configHashBefore: "mock-hash-0",
configHashAfter: "mock-hash-1",
lines: ["Workspace: /tmp/work"],
}));
const deps = {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
verifyInferenceConfig: vi.fn(async () => ({
ok: true as const,
modelRef: "openai/gpt-5.5",
latencyMs: 12,
})),
};
const plan = await executeSystemAgentOperation(
{ kind: "setup", workspace: "/tmp/work" },
runtime,
{ deps },
);
expectRecordFields(plan as unknown as Record<string, unknown>, {
applied: false,
});
expect(lines.join("\n")).toContain("Model choice: keep verified default openai/gpt-5.5.");
expect(applySetup).not.toHaveBeenCalled();
const result = await executeSystemAgentOperation(
{ kind: "setup", workspace: "/tmp/work" },
runtime,
{
approved: true,
auditDetails: { rescue: true },
deps,
},
);
expect(result.applied).toBe(true);
expect(lines.join("\n")).toContain("[openclaw] done: openclaw.setup");
expect(applySetup).toHaveBeenCalledWith(
{
workspace: "/tmp/work",
expectedInferenceRoute: expect.any(Object),
surface: "cli",
runtime,
},
{ commit: expect.any(Function) },
);
expect(lines.join("\n")).toContain("Default model: openai/gpt-5.5 (verified and kept)");
const auditPath = path.join(tempDir, "audit", "system-agent.jsonl");
const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim());
expectAuditRecord(
audit,
{
operation: "openclaw.setup",
summary: "Bootstrapped setup workspace",
},
{
rescue: true,
workspace: "/tmp/work",
model: "openai/gpt-5.5",
modelSource: "live-verified default model",
inferenceLatencyMs: 12,
},
);
});
it("rejects setup without a default model before any workspace or Gateway write", async () => {
const tempDir = opTempDirs.make("openclaw-no-inference-setup-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const { runtime, lines } = createSystemAgentTestRuntime();
const applySetup = vi.fn();
const deps = {
applySetup,
setupSurface: "gateway" as const,
loadOverview: async () => ({ defaultModel: undefined }) as never,
};
await expect(
executeSystemAgentOperation({ kind: "setup", workspace: "/tmp/work" }, runtime, {
approved: true,
deps,
}),
).rejects.toThrow("requires working inference first");
expect(applySetup).not.toHaveBeenCalled();
expect(lines.join("\n")).not.toContain("[openclaw] running: openclaw.setup");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rejects setup when the current route fails its live inference check", async () => {
const tempDir = opTempDirs.make("openclaw-failed-inference-setup-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
mockConfig.setConfig({ agents: { defaults: { model: { primary: "openai/gpt-5.5" } } } });
const { runtime, lines } = createSystemAgentTestRuntime();
const applySetup = vi.fn();
await expect(
executeSystemAgentOperation({ kind: "setup", workspace: "/tmp/work" }, runtime, {
approved: true,
deps: {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
verifyInferenceConfig: async () => ({
ok: false as const,
status: "auth" as const,
error: "not authenticated",
}),
},
}),
).rejects.toThrow("failed a live check");
expect(applySetup).not.toHaveBeenCalled();
expect(lines.join("\n")).not.toContain("[openclaw] running: openclaw.setup");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rejects route drift during setup verification but preserves the concurrent edit", async () => {
mockConfig.setConfig({
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
auth: { order: { openai: ["openai:old"] } },
});
const { runtime } = createSystemAgentTestRuntime();
const applySetup = vi.fn();
await expect(
executeSystemAgentOperation({ kind: "setup", workspace: "/tmp/work" }, runtime, {
approved: true,
deps: {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
verifyInferenceConfig: async () => {
mockConfig.setConfig({
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
auth: { order: { openai: ["openai:new"] } },
});
return { ok: true as const, modelRef: "openai/gpt-5.5", latencyMs: 8 };
},
},
}),
).rejects.toThrow("changed during setup verification");
expect(applySetup).not.toHaveBeenCalled();
expect(mockConfig.currentConfig()).toMatchObject({
auth: { order: { openai: ["openai:new"] } },
});
});
it("preserves unrelated concurrent edits after re-verifying the same setup route", async () => {
mockConfig.setConfig({
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
gateway: { port: 18789 },
});
const { runtime } = createSystemAgentTestRuntime();
const applySetup = vi.fn(async () => ({
configPath: "/tmp/openclaw.json",
configHashBefore: "mock-hash-0",
configHashAfter: "mock-hash-1",
lines: [],
}));
const result = await executeSystemAgentOperation(
{ kind: "setup", workspace: "/tmp/work" },
runtime,
{
approved: true,
deps: {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
verifyInferenceConfig: async () => {
mockConfig.setConfig({
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
gateway: { port: 19000 },
});
return { ok: true as const, modelRef: "openai/gpt-5.5", latencyMs: 7 };
},
},
},
);
expect(result.applied).toBe(true);
expect(mockConfig.currentConfig()).toMatchObject({ gateway: { port: 19000 } });
expect(applySetup).toHaveBeenCalledWith(
expect.objectContaining({ expectedInferenceRoute: expect.any(Object) }),
{ commit: expect.any(Function) },
);
});
it("rejects a setup model switch before writing", async () => {
const tempDir = opTempDirs.make("openclaw-model-switch-setup-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const { runtime } = createSystemAgentTestRuntime();
const applySetup = vi.fn();
await expect(
executeSystemAgentOperation(
{ kind: "setup", workspace: "/tmp/work", model: "acme/different" },
runtime,
{
approved: true,
deps: {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
},
},
),
).rejects.toThrow("Exit OpenClaw and run `openclaw onboard`");
expect(applySetup).not.toHaveBeenCalled();
});
it("allows the same requested model while preserving it without a model write", async () => {
const tempDir = opTempDirs.make("openclaw-same-model-setup-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const { runtime } = createSystemAgentTestRuntime();
mockConfig.setConfig({ agents: { defaults: { model: { primary: "openai/gpt-5.5" } } } });
const applySetup = vi.fn(async () => ({
configPath: path.join(tempDir, "openclaw.json"),
configHashBefore: "mock-hash-0",
configHashAfter: "mock-hash-1",
lines: ["Workspace: /tmp/work"],
}));
const result = await executeSystemAgentOperation(
{ kind: "setup", workspace: "/tmp/work", model: "openai/gpt-5.5" },
runtime,
{
approved: true,
deps: {
applySetup,
loadOverview: async () => ({ defaultModel: "openai/gpt-5.5" }) as never,
verifyInferenceConfig: async () => ({
ok: true as const,
modelRef: "openai/gpt-5.5",
latencyMs: 5,
}),
},
},
);
expect(result).toEqual({ applied: true });
expect(applySetup).toHaveBeenCalledWith(
{
workspace: "/tmp/work",
expectedInferenceRoute: expect.any(Object),
surface: "cli",
runtime,
},
{ commit: expect.any(Function) },
);
});
it("live-verifies a staged default model before writing and preserves concurrent edits", async () => {
const tempDir = opTempDirs.make("openclaw-verified-model-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
mockConfig.setConfig({
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6", fallbacks: ["openai/gpt-5.2"] },
},
list: [{ id: "main", default: true, workspace: "/tmp/main" }],
},
gateway: { port: 18789 },
models: { providers: { openai: { baseUrl: "https://api.openai.com/v1" } } },
});
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
let verificationCalls = 0;
const verifyInferenceConfig = vi.fn(async ({ config }: { config: TestConfig }) => {
verificationCalls += 1;
const stagedDefaults = requireRecord(
requireRecord(config.agents, "agents").defaults,
"defaults",
);
expect(stagedDefaults.model).toEqual({
primary: "openai/gpt-5.5",
fallbacks: ["openai/gpt-5.2"],
});
expect(
requireRecord(
requireRecord(
requireRecord(mockConfig.currentConfig().agents, "agents").defaults,
"defaults",
).model,
"persisted model",
).primary,
).toBe("anthropic/claude-sonnet-4-6");
if (verificationCalls === 1) {
const current = mockConfig.currentConfig();
const currentModels = requireRecord(current.models, "models");
const currentProviders = requireRecord(currentModels.providers, "providers");
mockConfig.setConfig({
...current,
auth: {
profiles: { "google:other": { provider: "google", mode: "api_key" } },
},
models: {
...currentModels,
providers: {
...currentProviders,
google: {
baseUrl: "https://example.invalid",
models: [{ id: "unrelated", name: "Unrelated", contextWindow: 1, maxTokens: 1 }],
},
},
},
agents: {
...requireRecord(current.agents, "agents"),
defaults: {
...requireRecord(requireRecord(current.agents, "agents").defaults, "defaults"),
models: { "google/unrelated": { agentRuntime: { id: "openclaw" } } },
},
list: [
{ id: "main", default: true, workspace: "/tmp/main" },
{ id: "work", workspace: "/tmp/work" },
],
},
channels: { telegram: { enabled: true } },
});
}
return { ok: true as const, modelRef: "openai/gpt-5.5", latencyMs: 17 };
});
const result = await executeSystemAgentOperation(
{ kind: "set-default-model", model: "openai/gpt-5.5" },
runtime,
{ approved: true, deps: { verifyInferenceConfig } },
);
expect(result).toEqual({ applied: true });
expect(verifyInferenceConfig).toHaveBeenCalledTimes(2);
expect(verifyInferenceConfig).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ requireExecutionOwner: true }),
);
expect(verifyInferenceConfig).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ requireExecutionOwner: true }),
);
expect(mockConfig.mutateConfigFile).toHaveBeenCalledOnce();
expect(mockConfig.mutateConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
writeOptions: { preCommitRuntimePreflight: expect.any(Function) },
}),
);
const persisted = mockConfig.currentConfig();
expect(
requireRecord(requireRecord(persisted.agents, "agents").defaults, "defaults").model,
).toEqual({ primary: "openai/gpt-5.5", fallbacks: ["openai/gpt-5.2"] });
expect(requireRecord(persisted.agents, "agents").list).toEqual([
{ id: "main", default: true, workspace: "/tmp/main" },
{ id: "work", workspace: "/tmp/work" },
]);
expect(requireRecord(persisted.auth, "auth").profiles).toEqual({
"google:other": { provider: "google", mode: "api_key" },
});
expect(
requireRecord(requireRecord(persisted.models, "models").providers, "providers"),
).toMatchObject({
openai: { baseUrl: "https://api.openai.com/v1" },
google: expect.any(Object),
});
expect(
requireRecord(
requireRecord(requireRecord(persisted.agents, "agents").defaults, "defaults").models,
"default models",
),
).toHaveProperty("google/unrelated");
expect(persisted.channels).toEqual({ telegram: { enabled: true } });
expect(lines.join("\n")).toContain("Default model: openai/gpt-5.5");
const audit = parseLastJsonLine(
await fs.readFile(path.join(tempDir, "audit", "system-agent.jsonl"), "utf8"),
);
expectAuditRecord(
audit,
{
operation: "config.setDefaultModel",
summary: "Set default model to openai/gpt-5.5",
},
{
requestedModel: "openai/gpt-5.5",
effectiveModel: "openai/gpt-5.5",
inferenceVerified: true,
inferenceLatencyMs: 17,
},
);
});
it.each([
{
field: "default agent",
initial: {
agents: {
defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } },
list: [{ id: "main", default: true }, { id: "work" }],
},
},
change: (config: TestConfig) => {
const next = structuredClone(config);
const list = requireRecord(next.agents, "agents").list as Array<{
id: string;
default?: boolean;
}>;
delete list[0]?.default;
list[1]!.default = true;
return next;
},
},
{
field: "default marker",
initial: {
agents: {
defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } },
list: [{ id: "main", default: true }, { id: "work" }],
},
},
change: (config: TestConfig) => {
const next = structuredClone(config);
const list = requireRecord(next.agents, "agents").list as Array<{
id: string;
default?: boolean;
}>;
delete list[0]?.default;
return next;
},
},
{
field: "auth profile order",
initial: {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
auth: { order: { anthropic: ["anthropic:one"] } },
},
change: (config: TestConfig) => ({
...structuredClone(config),
auth: { order: { anthropic: ["anthropic:two"] } },
}),
},
{
field: "runtime metadata",
initial: {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "claude-cli" } },
},
},
},
},
change: (config: TestConfig) => {
const next = structuredClone(config);
const defaults = requireRecord(requireRecord(next.agents, "agents").defaults, "defaults");
defaults.models = {
"anthropic/claude-sonnet-4-6": { agentRuntime: { id: "openclaw" } },
};
return next;
},
},
{
field: "model",
initial: {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
},
change: (config: TestConfig) => {
const next = structuredClone(config);
const defaults = requireRecord(requireRecord(next.agents, "agents").defaults, "defaults");
defaults.model = { primary: "anthropic/claude-opus-4-6" };
return next;
},
},
{
field: "config-backed environment",
initial: {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
env: { vars: { ANTHROPIC_API_KEY: "first" } },
},
change: (config: TestConfig) => ({
...structuredClone(config),
env: { vars: { ANTHROPIC_API_KEY: "second" } },
}),
},
{
field: "secret provider policy",
initial: {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
secrets: { defaults: { env: "first" } },
},
change: (config: TestConfig) => ({
...structuredClone(config),
secrets: { defaults: { env: "second" } },
}),
},
{
field: "plugin load policy",
initial: {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
plugins: { enabled: true },
},
change: (config: TestConfig) => ({
...structuredClone(config),
plugins: { enabled: false },
}),
},
])(
"aborts when concurrent $field changes invalidate the verified route",
async ({ initial, change }) => {
const tempDir = opTempDirs.make("openclaw-route-conflict-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
mockConfig.setConfig(initial);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi.fn(async () => {
mockConfig.setConfig(change(mockConfig.currentConfig()));
return { ok: true as const, modelRef: "openai/gpt-5.5", latencyMs: 7 };
});
await expect(
executeSystemAgentOperation(
{ kind: "set-default-model", model: "openai/gpt-5.5" },
runtime,
{
approved: true,
deps: { verifyInferenceConfig },
},
),
).rejects.toThrow("inference route changed during verification");
expect(mockConfig.mutateConfigFile).toHaveBeenCalledOnce();
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
},
);
it("keeps the working model and writes no audit when live inference fails", async () => {
const tempDir = opTempDirs.make("openclaw-rejected-model-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
gateway: { port: 18789 },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi.fn(async () => ({
ok: false as const,
status: "auth" as const,
error: "Provider authentication failed.",
}));
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
}),
).rejects.toThrow(
"The requested model failed a live inference test, so the current default model was not changed. Provider authentication failed. Fix provider authentication or model access, then retry.",
);
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(mockConfig.mutateConfigFile).not.toHaveBeenCalled();
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("writes nothing when the exact latest route fails its locked recheck", async () => {
const tempDir = opTempDirs.make("openclaw-latest-route-rejected-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi
.fn()
.mockResolvedValueOnce({ ok: true, modelRef: "openai/gpt-5.5", latencyMs: 5 })
.mockResolvedValueOnce({ ok: false, status: "auth", error: "credential changed" });
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
}),
).rejects.toThrow("no longer passes live inference at the config commit boundary");
expect(verifyInferenceConfig).toHaveBeenCalledTimes(2);
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rejects a live result from a different model before opening the write boundary", async () => {
const tempDir = opTempDirs.make("openclaw-mismatched-model-result-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi.fn(async () => ({
ok: true as const,
modelRef: "openai/gpt-5.4",
latencyMs: 5,
}));
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
}),
).rejects.toThrow("did not verify the exact model route");
expect(verifyInferenceConfig).toHaveBeenCalledOnce();
expect(mockConfig.mutateConfigFile).not.toHaveBeenCalled();
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rejects a different model result from the final commit-boundary probe", async () => {
const tempDir = opTempDirs.make("openclaw-final-mismatched-model-result-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi
.fn()
.mockResolvedValueOnce({ ok: true, modelRef: "openai/gpt-5.5", latencyMs: 5 })
.mockResolvedValueOnce({ ok: true, modelRef: "openai/gpt-5.4", latencyMs: 5 });
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
}),
).rejects.toThrow("did not verify the exact model route at the config commit boundary");
expect(verifyInferenceConfig).toHaveBeenCalledTimes(2);
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rechecks the existing inference binding inside the locked model transform", async () => {
const tempDir = opTempDirs.make("openclaw-model-binding-rotated-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
let bindingOwner = "verified";
const verifyInferenceConfig = vi.fn(async () => {
bindingOwner = "rotated";
return {
ok: true as const,
modelRef: "openai/gpt-5.5",
latencyMs: 5,
};
});
const beforePersistentApply = vi.fn(async () => {
if (bindingOwner !== "verified") {
throw new SystemAgentInferenceUnavailableError("conversation");
}
});
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
beforePersistentApply,
}),
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
expect(verifyInferenceConfig).toHaveBeenCalledOnce();
expect(beforePersistentApply).toHaveBeenCalledOnce();
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("rechecks the existing inference binding after the candidate's final live probe", async () => {
const tempDir = opTempDirs.make("openclaw-model-binding-final-probe-rotated-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const originalConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
};
mockConfig.setConfig(originalConfig);
mockConfig.mutateConfigFile.mockClear();
const { runtime, lines } = createSystemAgentTestRuntime();
let bindingOwner = "verified";
let verificationCalls = 0;
const verifyInferenceConfig = vi.fn(async () => {
verificationCalls += 1;
if (verificationCalls === 2) {
bindingOwner = "rotated";
}
return {
ok: true as const,
modelRef: "openai/gpt-5.5",
latencyMs: 5,
};
});
const beforePersistentApply = vi.fn(async () => {
if (bindingOwner !== "verified") {
throw new SystemAgentInferenceUnavailableError("conversation");
}
});
await expect(
executeSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" }, runtime, {
approved: true,
deps: { verifyInferenceConfig },
beforePersistentApply,
}),
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
expect(verifyInferenceConfig).toHaveBeenCalledTimes(2);
expect(beforePersistentApply).toHaveBeenCalledTimes(2);
expect(mockConfig.currentConfig()).toEqual(originalConfig);
expect(lines.join("\n")).not.toContain("[openclaw] done: config.setDefaultModel");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("stages and persists model changes at the effective default-agent owner", async () => {
const tempDir = opTempDirs.make("openclaw-default-agent-model-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
mockConfig.setConfig({
agents: {
defaults: { model: { primary: "anthropic/global-default" } },
list: [
{
id: "work",
default: true,
model: { primary: "anthropic/work-default" },
},
],
},
});
const { runtime } = createSystemAgentTestRuntime();
const verifyInferenceConfig = vi.fn(async ({ config }: { config: TestConfig }) => {
const agents = requireRecord(config.agents, "agents");
expect(requireRecord(agents.defaults, "defaults").model).toEqual({
primary: "anthropic/global-default",
});
const list = agents.list as Array<{ id: string; model: unknown }>;
expect(list.find((agent) => agent.id === "work")?.model).toEqual({
primary: "openai/gpt-5.5",
});
return { ok: true as const, modelRef: "openai/gpt-5.5", latencyMs: 9 };
});
await executeSystemAgentOperation(
{ kind: "set-default-model", model: "openai/gpt-5.5" },
runtime,
{ approved: true, deps: { verifyInferenceConfig } },
);
const agents = requireRecord(mockConfig.currentConfig().agents, "agents");
expect(requireRecord(agents.defaults, "defaults").model).toEqual({
primary: "anthropic/global-default",
});
const list = agents.list as Array<{ id: string; model: unknown }>;
expect(list.find((agent) => agent.id === "work")?.model).toEqual({
primary: "openai/gpt-5.5",
});
});
it("refuses doctor repairs before any write or audit", async () => {
const tempDir = opTempDirs.make("openclaw-doctor-fix-refused-");
setTestEnvValue("OPENCLAW_STATE_DIR", tempDir);
const { runtime, lines } = createSystemAgentTestRuntime();
const runDoctor = vi.fn(async () => {});
const result = await executeSystemAgentOperation({ kind: "doctor-fix" }, runtime, {
approved: true,
deps: { runDoctor },
auditDetails: { rescue: true },
});
expect(result).toEqual({ applied: false });
expect(isPersistentSystemAgentOperation({ kind: "doctor-fix" })).toBe(false);
expect(runDoctor).not.toHaveBeenCalled();
expect(lines.join("\n")).toContain("Exit OpenClaw");
expect(lines.join("\n")).toContain("openclaw doctor --fix");
expect(lines.join("\n")).not.toContain("[openclaw] running: doctor.fix");
await expect(fs.access(path.join(tempDir, "audit", "system-agent.jsonl"))).rejects.toThrow();
});
it("returns from the agent TUI back to OpenClaw", async () => {
const { runtime, lines } = createSystemAgentTestRuntime();
const runTui = vi.fn(async () => ({
exitReason: "return-to-system-agent" as const,
systemAgentMessage: "restart gateway",
}));
const result = await executeSystemAgentOperation(
{ kind: "open-tui", agentId: "work" },
runtime,
{
deps: { runTui },
},
);
expect(runTui).toHaveBeenCalledWith({
local: true,
session: "agent:work:main",
deliver: false,
historyLimit: 200,
});
expectRecordFields(result as unknown as Record<string, unknown>, {
applied: false,
returnToShell: true,
nextInput: "restart gateway",
});
expect(lines.join("\n")).toContain(
"[openclaw] returned from agent with request: restart gateway",
);
});
it("re-enters the OpenClaw shell when the agent TUI returns without a request", async () => {
const { runtime, lines } = createSystemAgentTestRuntime();
const runTui = vi.fn(async () => ({
exitReason: "return-to-system-agent" as const,
}));
const result = await executeSystemAgentOperation({ kind: "open-tui" }, runtime, {
deps: { runTui },
});
expectRecordFields(result as unknown as Record<string, unknown>, {
applied: false,
returnToShell: true,
});
expect((result as { nextInput?: string }).nextInput).toBeUndefined();
expect(lines.join("\n")).toContain("[openclaw] returned from agent");
});
});