mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
perf(crestodian): reduce test import overhead
This commit is contained in:
15
src/crestodian/crestodian.test-helpers.ts
Normal file
15
src/crestodian/crestodian.test-helpers.ts
Normal 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}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user