perf(crestodian): reduce test import overhead

This commit is contained in:
Peter Steinberger
2026-04-25 12:03:58 +01:00
parent 84a22a64be
commit 4d00c47072
10 changed files with 468 additions and 230 deletions

View File

@@ -0,0 +1,15 @@
import type { RuntimeEnv } from "../runtime.js";
export function createCrestodianTestRuntime(): { runtime: RuntimeEnv; lines: string[] } {
const lines: string[] = [];
return {
lines,
runtime: {
log: (...args) => lines.push(args.join(" ")),
error: (...args) => lines.push(args.join(" ")),
exit: (code) => {
throw new Error(`exit ${code}`);
},
},
};
}

View File

@@ -1,25 +1,49 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runCrestodian } from "./crestodian.js";
import { createCrestodianTestRuntime } from "./crestodian.test-helpers.js";
function createRuntime(): { runtime: RuntimeEnv; lines: string[] } {
const lines: string[] = [];
return {
lines,
runtime: {
log: (...args) => lines.push(args.join(" ")),
error: (...args) => lines.push(args.join(" ")),
exit: (code) => {
throw new Error(`exit ${code}`);
},
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", () => ({
formatCrestodianOverview: () => "Default model: openai/gpt-5.5",
loadCrestodianOverview: vi.fn(async () => ({
defaultAgentId: "main",
defaultModel: "openai/gpt-5.5",
agents: [{ id: "main", isDefault: true, model: "openai/gpt-5.5" }],
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" },
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",
},
})),
}));
describe("runCrestodian", () => {
beforeEach(() => {
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
});
afterEach(() => {
vi.unstubAllEnvs();
});
@@ -28,7 +52,7 @@ describe("runCrestodian", () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-run-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runGatewayRestart = vi.fn(async () => {});
await runCrestodian(
@@ -54,7 +78,7 @@ describe("runCrestodian", () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-run-deterministic-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const planner = vi.fn(async () => ({ command: "restart gateway" }));
await runCrestodian(
@@ -73,7 +97,7 @@ describe("runCrestodian", () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-run-tui-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runInteractiveTui = vi.fn(async () => {});
await runCrestodian(

View File

@@ -8,7 +8,11 @@ import {
type CrestodianCommandDeps,
} from "./operations.js";
import { formatCrestodianOverview, loadCrestodianOverview } from "./overview.js";
import { runCrestodianTui } from "./tui-backend.js";
type CrestodianInteractiveRunner = (
opts: RunCrestodianOptions,
runtime: RuntimeEnv,
) => Promise<void>;
export type RunCrestodianOptions = {
message?: string;
@@ -19,7 +23,7 @@ export type RunCrestodianOptions = {
planWithAssistant?: CrestodianAssistantPlanner;
input?: NodeJS.ReadableStream;
output?: NodeJS.WritableStream;
runInteractiveTui?: typeof runCrestodianTui;
runInteractiveTui?: CrestodianInteractiveRunner;
};
async function runOneShot(
@@ -62,6 +66,7 @@ export async function runCrestodian(
return;
}
const runInteractiveTui = opts.runInteractiveTui ?? runCrestodianTui;
const runInteractiveTui =
opts.runInteractiveTui ?? (await import("./tui-backend.js")).runCrestodianTui;
await runInteractiveTui(opts, runtime);
}

View File

@@ -1,15 +1,11 @@
import type { RuntimeEnv } from "../runtime.js";
import {
planCrestodianCommand,
type CrestodianAssistantPlan,
type CrestodianAssistantPlanner,
} from "./assistant.js";
import type { CrestodianAssistantPlan, CrestodianAssistantPlanner } from "./assistant.js";
import {
describeCrestodianPersistentOperation,
parseCrestodianOperation,
type CrestodianOperation,
} from "./operations.js";
import { loadCrestodianOverview, type CrestodianOverview } from "./overview.js";
import type { CrestodianOverview } from "./overview.js";
export type CrestodianDialogueOptions = {
planWithAssistant?: CrestodianAssistantPlanner;
@@ -32,8 +28,9 @@ export async function resolveCrestodianOperation(
if (!shouldAskAssistant(input, operation)) {
return operation;
}
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
const planner = opts.planWithAssistant ?? planCrestodianCommand;
const planner = opts.planWithAssistant ?? (await import("./assistant.js")).planCrestodianCommand;
const plan = await planner({ input, overview });
if (!plan) {
return operation;

View File

@@ -1,29 +1,159 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createCrestodianTestRuntime } from "./crestodian.test-helpers.js";
import {
executeCrestodianOperation,
parseCrestodianOperation,
type CrestodianOperationResult,
} from "./operations.js";
function createRuntime(): { runtime: RuntimeEnv; lines: string[] } {
const lines: string[] = [];
type TestConfig = Record<string, unknown>;
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 {
lines,
runtime: {
log: (...args) => lines.push(args.join(" ")),
error: (...args) => lines.push(args.join(" ")),
exit: (code) => {
throw new Error(`exit ${code}`);
reset() {
state.path = "/tmp/openclaw.json";
state.exists = true;
state.config = {};
state.hash = "mock-hash-0";
},
missing(path: string) {
state.path = path;
state.exists = false;
state.config = {};
state.hash = undefined;
},
currentConfig() {
return cloneConfig();
},
readConfigFileSnapshot: vi.fn(async () => snapshot()),
mutateConfigFile: vi.fn(
async (params: {
mutate: (
draft: TestConfig,
context: { snapshot: ReturnType<typeof snapshot> },
) => Promise<void> | void;
}) => {
const before = snapshot();
const draft = cloneConfig();
await params.mutate(draft, { snapshot: before });
state.exists = true;
state.config = draft;
state.hash = "mock-hash-1";
return {
path: state.path,
previousHash: 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", () => ({
formatCrestodianOverview: () => "Default model: openai/gpt-5.5",
loadCrestodianOverview: 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" },
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,
}));
vi.mock("../commands/models/shared.js", () => ({
applyDefaultModelPrimaryUpdate: ({
cfg,
modelRaw,
field,
}: {
cfg: TestConfig;
modelRaw: string;
field: "model" | "imageModel";
}) => ({
...cfg,
agents: {
...(cfg.agents as TestConfig | undefined),
defaults: {
...(cfg.agents as { defaults?: TestConfig } | undefined)?.defaults,
[field]: { primary: modelRaw },
},
},
};
}
}),
}));
vi.mock("../config/model-input.js", () => ({
resolveAgentModelPrimaryValue: (model?: string | { primary?: string }) =>
typeof model === "string" ? model : model?.primary,
}));
describe("parseCrestodianOperation", () => {
beforeEach(() => {
mockConfig.reset();
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
});
afterEach(() => {
vi.unstubAllEnvs();
});
@@ -100,7 +230,7 @@ describe("parseCrestodianOperation", () => {
});
it("requires approval before restarting gateway", async () => {
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runGatewayRestart = vi.fn(async () => {});
const result = await executeCrestodianOperation({ kind: "gateway-restart" }, runtime, {
@@ -115,84 +245,9 @@ describe("parseCrestodianOperation", () => {
expect(runGatewayRestart).not.toHaveBeenCalled();
});
it("restarts gateway through typed deps and writes an audit entry", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-gateway-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
const { runtime, lines } = createRuntime();
const runGatewayRestart = vi.fn(async () => {});
await expect(
executeCrestodianOperation({ kind: "gateway-restart" }, runtime, {
approved: true,
deps: { runGatewayRestart },
auditDetails: { rescue: true, channel: "whatsapp" },
}),
).resolves.toMatchObject({ applied: true });
expect(runGatewayRestart).toHaveBeenCalledTimes(1);
expect(lines.join("\n")).toContain("[crestodian] done: gateway.restart");
const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim());
expect(audit).toMatchObject({
operation: "gateway.restart",
summary: "Restarted Gateway",
details: { rescue: true, channel: "whatsapp" },
});
});
it("creates agents through typed deps and writes an audit entry", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-agent-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
const { runtime, lines } = createRuntime();
const runAgentsAdd = vi.fn(async () => {});
await expect(
executeCrestodianOperation(
{
kind: "create-agent",
agentId: "work",
workspace: "/tmp/work",
model: "openai/gpt-5.2",
},
runtime,
{
approved: true,
deps: { runAgentsAdd },
auditDetails: { rescue: true, channel: "whatsapp" },
},
),
).resolves.toMatchObject({ applied: true });
expect(runAgentsAdd).toHaveBeenCalledWith(
{
name: "work",
workspace: "/tmp/work",
model: "openai/gpt-5.2",
nonInteractive: true,
},
runtime,
{ hasFlags: true },
);
expect(lines.join("\n")).toContain("[crestodian] done: agents.create");
const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim());
expect(audit).toMatchObject({
operation: "agents.create",
summary: "Created agent work",
details: {
rescue: true,
channel: "whatsapp",
agentId: "work",
workspace: "/tmp/work",
model: "openai/gpt-5.2",
},
});
});
it("validates missing config without exiting the process", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-validate-"));
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
mockConfig.missing("/tmp/openclaw.json");
const { runtime, lines } = createCrestodianTestRuntime();
await expect(
executeCrestodianOperation({ kind: "config-validate" }, runtime),
@@ -204,8 +259,7 @@ describe("parseCrestodianOperation", () => {
it("applies config set through typed deps and writes an audit entry", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-config-set-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runConfigSet = vi.fn(async () => {});
await expect(
@@ -242,8 +296,7 @@ describe("parseCrestodianOperation", () => {
it("applies SecretRef config set through typed deps and writes an audit entry", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-config-ref-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runConfigSet = vi.fn(async () => {});
await expect(
@@ -290,9 +343,8 @@ describe("parseCrestodianOperation", () => {
it("runs setup bootstrap only after approval and audits it", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-setup-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
vi.stubEnv("OPENAI_API_KEY", "test-key");
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const plan = await executeCrestodianOperation(
{ kind: "setup", workspace: "/tmp/work" },
@@ -311,10 +363,7 @@ describe("parseCrestodianOperation", () => {
).resolves.toMatchObject({ applied: true });
expect(lines.join("\n")).toContain("[crestodian] done: crestodian.setup");
const config = JSON.parse(
await fs.readFile(path.join(tempDir, "openclaw.json"), "utf8"),
) as Record<string, unknown>;
expect(config).toMatchObject({
expect(mockConfig.currentConfig()).toMatchObject({
agents: {
defaults: {
workspace: "/tmp/work",
@@ -339,8 +388,7 @@ describe("parseCrestodianOperation", () => {
it("runs doctor repairs only after approval and audits them", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-doctor-fix-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runDoctor = vi.fn(async () => {});
const plan = await executeCrestodianOperation({ kind: "doctor-fix" }, runtime, {
@@ -376,23 +424,7 @@ describe("parseCrestodianOperation", () => {
});
it("returns from the agent TUI back to Crestodian", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-tui-return-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
await fs.writeFile(
path.join(tempDir, "openclaw.json"),
JSON.stringify(
{
agents: {
defaults: { model: { primary: "openai/gpt-5.2" } },
list: [{ id: "main", default: true }, { id: "work" }],
},
},
null,
2,
),
);
const { runtime, lines } = createRuntime();
const { runtime, lines } = createCrestodianTestRuntime();
const runTui = vi.fn(async () => ({
exitReason: "return-to-crestodian" as const,
crestodianMessage: "restart gateway",

View File

@@ -1,18 +1,15 @@
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import type { ConfigSetOptions } from "../cli/config-set-input.js";
import { doctorCommand } from "../commands/doctor.js";
import type { DoctorOptions } from "../commands/doctor.types.js";
import { healthCommand } from "../commands/health.js";
import { applyDefaultModelPrimaryUpdate } from "../commands/models/shared.js";
import { statusCommand } from "../commands/status.command.js";
import { mutateConfigFile, readConfigFileSnapshot } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import type { TuiResult } from "../tui/tui-types.js";
import { resolveUserPath, shortenHomePath } from "../utils.js";
import { appendCrestodianAuditEntry, resolveCrestodianAuditPath } from "./audit.js";
import { formatCrestodianOverview, loadCrestodianOverview } from "./overview.js";
import type { CrestodianOverview } from "./overview.js";
type ConfigModule = typeof import("../config/config.js");
type ConfigFileSnapshot = Awaited<ReturnType<ConfigModule["readConfigFileSnapshot"]>>;
export type CrestodianOperation =
| { kind: "none"; message: string }
@@ -294,9 +291,12 @@ function formatSetupPlanDescription(
}
function chooseSetupModel(
overview: Awaited<ReturnType<typeof loadCrestodianOverview>>,
overview: CrestodianOverview,
requestedModel: string | undefined,
): { model?: string; source: string } {
): {
model?: string;
source: string;
} {
if (requestedModel?.trim()) {
return { model: requestedModel.trim(), source: "requested" };
}
@@ -323,9 +323,7 @@ function logQueued(runtime: RuntimeEnv, operation: string): void {
runtime.log(`[crestodian] running: ${operation}`);
}
function formatGatewayStatusLine(
overview: Awaited<ReturnType<typeof loadCrestodianOverview>>,
): string {
function formatGatewayStatusLine(overview: CrestodianOverview): string {
return [
`Gateway: ${overview.gateway.reachable ? "reachable" : "not reachable"}`,
`URL: ${overview.gateway.url}`,
@@ -349,9 +347,20 @@ async function runGatewayLifecycle(operation: "start" | "stop" | "restart"): Pro
await lifecycle.runDaemonRestart();
}
function formatConfigValidationLine(
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
): string {
async function readConfigFileSnapshotLazy(): Promise<ConfigFileSnapshot> {
const { readConfigFileSnapshot } = await import("../config/config.js");
return await readConfigFileSnapshot();
}
async function loadConfigFileMutationHelpers(): Promise<{
mutateConfigFile: ConfigModule["mutateConfigFile"];
readConfigFileSnapshot: ConfigModule["readConfigFileSnapshot"];
}> {
const { mutateConfigFile, readConfigFileSnapshot } = await import("../config/config.js");
return { mutateConfigFile, readConfigFileSnapshot };
}
function formatConfigValidationLine(snapshot: ConfigFileSnapshot): string {
if (!snapshot.exists) {
return `Config missing: ${shortenHomePath(snapshot.path)}`;
}
@@ -380,6 +389,7 @@ async function resolveTuiAgentId(params: {
requestedAgentId: string | undefined;
requestedWorkspace?: string;
}): Promise<string | undefined> {
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
const workspace = params.requestedWorkspace
? resolveUserPath(params.requestedWorkspace)
@@ -419,11 +429,13 @@ export async function executeCrestodianOperation(
return { applied: false, exitsInteractive: operation.message.includes("Bye.") };
}
if (operation.kind === "overview") {
const { formatCrestodianOverview, loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
runtime.log(formatCrestodianOverview(overview));
return { applied: false };
}
if (operation.kind === "agents") {
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
runtime.log(
[
@@ -444,6 +456,7 @@ export async function executeCrestodianOperation(
return { applied: false };
}
if (operation.kind === "models") {
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
runtime.log(
[
@@ -462,11 +475,12 @@ export async function executeCrestodianOperation(
return { applied: false };
}
if (operation.kind === "config-validate") {
const snapshot = await readConfigFileSnapshot();
const snapshot = await readConfigFileSnapshotLazy();
runtime.log(formatConfigValidationLine(snapshot));
return { applied: false };
}
if (operation.kind === "setup") {
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
const setupModel = chooseSetupModel(overview, operation.model);
if (!opts.approved) {
@@ -482,13 +496,17 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "crestodian.setup");
const { mutateConfigFile, readConfigFileSnapshot } = await loadConfigFileMutationHelpers();
const before = await readConfigFileSnapshot();
const workspace = resolveUserPath(operation.workspace ?? process.cwd());
const applyDefaultModelPrimaryUpdate = setupModel.model
? (await import("../commands/models/shared.js")).applyDefaultModelPrimaryUpdate
: undefined;
const result = await mutateConfigFile({
base: "source",
mutate: (cfg) => {
let next = cfg;
if (setupModel.model) {
if (setupModel.model && applyDefaultModelPrimaryUpdate) {
next = applyDefaultModelPrimaryUpdate({
cfg: next,
modelRaw: setupModel.model,
@@ -543,6 +561,7 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "config.set");
const { readConfigFileSnapshot } = await import("../config/config.js");
const before = await readConfigFileSnapshot();
const runConfigSet =
opts.deps?.runConfigSet ??
@@ -580,6 +599,7 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "config.setRef");
const { readConfigFileSnapshot } = await import("../config/config.js");
const before = await readConfigFileSnapshot();
const runConfigSet =
opts.deps?.runConfigSet ??
@@ -622,6 +642,7 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "agents.create");
const { readConfigFileSnapshot } = await import("../config/config.js");
const before = await readConfigFileSnapshot();
const workspace = resolveUserPath(operation.workspace ?? process.cwd());
const runAgentsAdd =
@@ -656,7 +677,7 @@ export async function executeCrestodianOperation(
}
if (operation.kind === "doctor") {
logQueued(runtime, "doctor");
const runDoctor = opts.deps?.runDoctor ?? doctorCommand;
const runDoctor = opts.deps?.runDoctor ?? (await import("../commands/doctor.js")).doctorCommand;
await runDoctor(runtime, { nonInteractive: true });
runtime.log("[crestodian] done: doctor");
return { applied: false };
@@ -668,8 +689,9 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "doctor.fix");
const { readConfigFileSnapshot } = await import("../config/config.js");
const before = await readConfigFileSnapshot();
const runDoctor = opts.deps?.runDoctor ?? doctorCommand;
const runDoctor = opts.deps?.runDoctor ?? (await import("../commands/doctor.js")).doctorCommand;
await runDoctor(runtime, { nonInteractive: true, repair: true, yes: true });
const after = await readConfigFileSnapshot();
await appendCrestodianAuditEntry({
@@ -685,17 +707,20 @@ export async function executeCrestodianOperation(
}
if (operation.kind === "status") {
logQueued(runtime, "status.check");
const { statusCommand } = await import("../commands/status.command.js");
await statusCommand({ timeoutMs: 10_000 }, runtime);
runtime.log("[crestodian] done: status.check");
return { applied: false };
}
if (operation.kind === "health") {
logQueued(runtime, "health.check");
const { healthCommand } = await import("../commands/health.js");
await healthCommand({ timeoutMs: 10_000 }, runtime);
runtime.log("[crestodian] done: health.check");
return { applied: false };
}
if (operation.kind === "gateway-status") {
const { loadCrestodianOverview } = await import("./overview.js");
const overview = await loadCrestodianOverview();
runtime.log(formatGatewayStatusLine(overview));
return { applied: false };
@@ -781,7 +806,9 @@ export async function executeCrestodianOperation(
return { applied: false, message };
}
logQueued(runtime, "config.setDefaultModel");
const { mutateConfigFile, readConfigFileSnapshot } = await loadConfigFileMutationHelpers();
const before = await readConfigFileSnapshot();
const { applyDefaultModelPrimaryUpdate } = await import("../commands/models/shared.js");
const result = await mutateConfigFile({
base: "source",
mutate: (cfg) => {
@@ -794,6 +821,8 @@ export async function executeCrestodianOperation(
},
});
const after = await readConfigFileSnapshot();
const { resolveAgentModelPrimaryValue } = await import("../config/model-input.js");
const effectiveModel = resolveAgentModelPrimaryValue(result.nextConfig.agents?.defaults?.model);
await appendCrestodianAuditEntry({
operation: "config.setDefaultModel",
summary: `Set default model to ${operation.model}`,
@@ -803,13 +832,11 @@ export async function executeCrestodianOperation(
details: {
...opts.auditDetails,
requestedModel: operation.model,
effectiveModel: resolveAgentModelPrimaryValue(result.nextConfig.agents?.defaults?.model),
effectiveModel,
},
});
runtime.log(`Updated ${result.path}`);
runtime.log(
`Default model: ${resolveAgentModelPrimaryValue(result.nextConfig.agents?.defaults?.model) ?? operation.model}`,
);
runtime.log(`Default model: ${effectiveModel ?? operation.model}`);
runtime.log("[crestodian] done: config.setDefaultModel");
return { applied: true };
}

View File

@@ -1,8 +1,4 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resetConfigRuntimeState } from "../config/config.js";
vi.mock("./probes.js", () => ({
probeLocalCommand: vi.fn(async (command: string) => ({
@@ -13,17 +9,40 @@ vi.mock("./probes.js", () => ({
probeGatewayUrl: vi.fn(async (url: string) => ({ reachable: false, url, error: "offline" })),
}));
vi.mock("../config/config.js", () => ({
readConfigFileSnapshot: vi.fn(async () => ({
path: "/tmp/openclaw.json",
exists: true,
valid: true,
issues: [],
hash: "test-hash",
runtimeConfig: {
agents: {
defaults: { model: { primary: "openai/gpt-5.2" } },
list: [
{ id: "main", default: true },
{ id: "work", name: "Work" },
],
},
gateway: { port: 19001 },
},
sourceConfig: undefined,
})),
resolveConfigPath: vi.fn(() => "/tmp/openclaw.json"),
resolveGatewayPort: vi.fn((cfg: { gateway?: { port?: number } }) => cfg.gateway?.port ?? 8765),
}));
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails: vi.fn((input: { config: { gateway?: { port?: number } } }) => ({
url: `ws://127.0.0.1:${input.config.gateway?.port ?? 8765}`,
urlSource: "local loopback",
})),
}));
describe("loadCrestodianOverview", () => {
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
const previousTestFast = process.env.OPENCLAW_TEST_FAST;
afterEach(() => {
resetConfigRuntimeState();
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
if (previousTestFast === undefined) {
delete process.env.OPENCLAW_TEST_FAST;
} else {
@@ -32,26 +51,7 @@ describe("loadCrestodianOverview", () => {
});
it("summarizes config, agents, model, tools, and gateway", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-overview-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
await fs.writeFile(
path.join(tempDir, "openclaw.json"),
`${JSON.stringify(
{
agents: {
defaults: { model: { primary: "openai/gpt-5.2" } },
list: [
{ id: "main", default: true },
{ id: "work", name: "Work" },
],
},
gateway: { port: 19001 },
},
null,
2,
)}\n`,
);
const { formatCrestodianOverview, formatCrestodianStartupMessage, loadCrestodianOverview } =
await import("./overview.js");

View File

@@ -16,7 +16,6 @@ import {
type OpenClawConfig,
} from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { probeGatewayUrl, probeLocalCommand, type LocalCommandProbe } from "./probes.js";
@@ -61,6 +60,8 @@ export type CrestodianOverview = {
};
};
type OpenClawReferencePaths = Awaited<ReturnType<typeof resolveOpenClawReferencePaths>>;
function issueMessages(snapshot: ConfigFileSnapshot): string[] {
return snapshot.issues.map((issue) => {
const path = issue.path ? `${issue.path}: ` : "";
@@ -107,6 +108,17 @@ function buildAgentSummaries(cfg: OpenClawConfig): CrestodianAgentSummary[] {
return summaries;
}
function resolveFastTestReferences(env: NodeJS.ProcessEnv): OpenClawReferencePaths | undefined {
if (env.OPENCLAW_TEST_FAST !== "1") {
return undefined;
}
const sourcePath = process.cwd();
return {
sourcePath,
docsPath: `${sourcePath}/docs`,
};
}
export async function loadCrestodianOverview(
opts: { env?: NodeJS.ProcessEnv } = {},
): Promise<CrestodianOverview> {
@@ -122,6 +134,7 @@ export async function loadCrestodianOverview(
let gatewaySource = "local loopback";
let gatewayError: string | undefined;
try {
const { buildGatewayConnectionDetails } = await import("../gateway/call.js");
const details = buildGatewayConnectionDetails({ config: cfg, configPath });
gatewayUrl = details.url;
gatewaySource = details.urlSource;
@@ -133,11 +146,12 @@ export async function loadCrestodianOverview(
probeLocalCommand("codex"),
probeLocalCommand("claude"),
probeGatewayUrl(gatewayUrl),
resolveOpenClawReferencePaths({
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
}),
resolveFastTestReferences(env) ??
resolveOpenClawReferencePaths({
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url,
}),
]);
return {
config: {

View File

@@ -1,14 +1,105 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CommandContext } from "../auto-reply/reply/commands-types.js";
import { clearConfigCache } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { extractCrestodianRescueMessage, runCrestodianRescueMessage } from "./rescue-message.js";
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
type TestConfig = Record<string, unknown>;
const mockConfig = vi.hoisted(() => {
const state = {
path: "/tmp/openclaw.json",
config: {} as TestConfig,
hash: "mock-hash-0" as string | undefined,
};
const cloneConfig = () => structuredClone(state.config);
const snapshot = () => {
const config = cloneConfig();
return {
path: state.path,
exists: true,
raw: `${JSON.stringify(config)}\n`,
parsed: config,
sourceConfig: config,
resolved: config,
valid: true,
runtimeConfig: config,
config,
hash: state.hash,
issues: [],
warnings: [],
legacyIssues: [],
};
};
return {
reset() {
state.path = "/tmp/openclaw.json";
state.config = {};
state.hash = "mock-hash-0";
},
currentConfig() {
return cloneConfig();
},
readConfigFileSnapshot: vi.fn(async () => snapshot()),
mutateConfigFile: vi.fn(
async (params: {
mutate: (
draft: TestConfig,
context: { snapshot: ReturnType<typeof snapshot> },
) => Promise<void> | void;
}) => {
const before = snapshot();
const draft = cloneConfig();
await params.mutate(draft, { snapshot: before });
state.config = draft;
state.hash = "mock-hash-1";
return {
path: state.path,
previousHash: before.hash ?? null,
snapshot: before,
nextConfig: cloneConfig(),
result: undefined,
};
},
),
};
});
vi.mock("../config/config.js", () => ({
clearConfigCache: vi.fn(),
mutateConfigFile: mockConfig.mutateConfigFile,
readConfigFileSnapshot: mockConfig.readConfigFileSnapshot,
}));
vi.mock("../commands/models/shared.js", () => ({
applyDefaultModelPrimaryUpdate: ({
cfg,
modelRaw,
field,
}: {
cfg: TestConfig;
modelRaw: string;
field: "model" | "imageModel";
}) => ({
...cfg,
agents: {
...(cfg.agents as TestConfig | undefined),
defaults: {
...(cfg.agents as { defaults?: TestConfig } | undefined)?.defaults,
[field]: { primary: modelRaw },
},
},
}),
}));
vi.mock("../config/model-input.js", () => ({
resolveAgentModelPrimaryValue: (model?: string | { primary?: string }) =>
typeof model === "string" ? model : model?.primary,
}));
function commandContext(overrides: Partial<CommandContext> = {}): CommandContext {
return {
@@ -43,18 +134,16 @@ async function runRescue(
}
describe("Crestodian rescue message", () => {
beforeEach(() => {
mockConfig.reset();
});
afterEach(() => {
clearConfigCache();
if (originalStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = originalStateDir;
}
if (originalConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = originalConfigPath;
}
});
it("recognizes the Crestodian rescue command", () => {
@@ -91,20 +180,7 @@ describe("Crestodian rescue message", () => {
it("queues and applies persistent writes through conversational approval", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-rescue-"));
const configPath = path.join(tempDir, "openclaw.json");
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
vi.stubEnv("OPENCLAW_CONFIG_PATH", configPath);
await fs.writeFile(
configPath,
JSON.stringify(
{
meta: { lastTouchedVersion: "test", lastTouchedAt: new Date(0).toISOString() },
agents: { defaults: {} },
},
null,
2,
),
);
const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
await expect(runRescue("/crestodian set default model openai/gpt-5.2", cfg)).resolves.toContain(
@@ -114,8 +190,9 @@ describe("Crestodian rescue message", () => {
"Default model: openai/gpt-5.2",
);
const config = JSON.parse(await fs.readFile(configPath, "utf8")) as OpenClawConfig;
expect(config.agents?.defaults?.model).toMatchObject({ primary: "openai/gpt-5.2" });
expect(mockConfig.currentConfig()).toMatchObject({
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
});
const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim());
expect(audit.details).toMatchObject({
@@ -167,6 +244,15 @@ describe("Crestodian rescue message", () => {
);
expect(deps.runAgentsAdd).toHaveBeenCalledTimes(1);
expect(deps.runAgentsAdd).toHaveBeenCalledWith(
{
name: "work",
workspace: "/tmp/work",
nonInteractive: true,
},
expect.any(Object),
{ hasFlags: true },
);
const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim());
expect(audit).toMatchObject({

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
const mocks = vi.hoisted(() => ({
@@ -12,6 +12,40 @@ vi.mock("../tui/tui.js", () => ({
runTui: mocks.runTui,
}));
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", () => ({
formatCrestodianOverview: () => "Default model: openai/gpt-5.5",
loadCrestodianOverview: vi.fn(async () => ({
defaultAgentId: "main",
defaultModel: "openai/gpt-5.5",
agents: [{ id: "main", isDefault: true, model: "openai/gpt-5.5" }],
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" },
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",
},
})),
}));
import { runCrestodianTui } from "./tui-backend.js";
function createRuntime(): RuntimeEnv {
@@ -25,6 +59,10 @@ function createRuntime(): RuntimeEnv {
}
describe("runCrestodianTui", () => {
beforeEach(() => {
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
});
afterEach(() => {
vi.unstubAllEnvs();
mocks.runTui.mockClear();