mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:30:43 +00:00
perf(test): route more unit tests through fast lane
This commit is contained in:
@@ -1,127 +1,115 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runCrestodian } from "./crestodian.js";
|
||||
import { createCrestodianTestRuntime } from "./crestodian.test-helpers.js";
|
||||
import type { CrestodianOverview } from "./overview.js";
|
||||
|
||||
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" })),
|
||||
}));
|
||||
const overview: CrestodianOverview = {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
})),
|
||||
}));
|
||||
const crestodianOverviewDeps = {
|
||||
formatOverview: () => "Default model: openai/gpt-5.5",
|
||||
loadOverview: async () => overview,
|
||||
};
|
||||
|
||||
describe("runCrestodian", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("uses the assistant planner only to choose typed operations", async () => {
|
||||
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 } = createCrestodianTestRuntime();
|
||||
const runGatewayRestart = vi.fn(async () => {});
|
||||
const onReady = vi.fn();
|
||||
let runGatewayRestartCalls = 0;
|
||||
let onReadyCalls = 0;
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
message: "the local bridge looks sleepy, poke it",
|
||||
deps: { runGatewayRestart },
|
||||
onReady,
|
||||
deps: {
|
||||
runGatewayRestart: async () => {
|
||||
runGatewayRestartCalls += 1;
|
||||
},
|
||||
},
|
||||
onReady: () => {
|
||||
onReadyCalls += 1;
|
||||
},
|
||||
planWithAssistant: async () => ({
|
||||
reply: "I can queue a Gateway restart.",
|
||||
command: "restart gateway",
|
||||
modelLabel: "openai/gpt-5.5",
|
||||
}),
|
||||
...crestodianOverviewDeps,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(runGatewayRestart).not.toHaveBeenCalled();
|
||||
expect(onReady).not.toHaveBeenCalled();
|
||||
expect(runGatewayRestartCalls).toBe(0);
|
||||
expect(onReadyCalls).toBe(0);
|
||||
expect(lines.join("\n")).toContain("[crestodian] planner: openai/gpt-5.5");
|
||||
expect(lines.join("\n")).toContain("[crestodian] interpreted: restart gateway");
|
||||
expect(lines.join("\n")).toContain("Plan: restart the Gateway. Say yes to apply.");
|
||||
});
|
||||
|
||||
it("keeps deterministic parsing ahead of the assistant planner", async () => {
|
||||
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 } = createCrestodianTestRuntime();
|
||||
const planner = vi.fn(async () => ({ command: "restart gateway" }));
|
||||
const onReady = vi.fn();
|
||||
let plannerCalls = 0;
|
||||
let onReadyCalls = 0;
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
message: "models",
|
||||
planWithAssistant: planner,
|
||||
onReady,
|
||||
planWithAssistant: async () => {
|
||||
plannerCalls += 1;
|
||||
return { command: "restart gateway" };
|
||||
},
|
||||
onReady: () => {
|
||||
onReadyCalls += 1;
|
||||
},
|
||||
...crestodianOverviewDeps,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(planner).not.toHaveBeenCalled();
|
||||
expect(onReady).not.toHaveBeenCalled();
|
||||
expect(plannerCalls).toBe(0);
|
||||
expect(onReadyCalls).toBe(0);
|
||||
expect(lines.join("\n")).toContain("Default model:");
|
||||
});
|
||||
|
||||
it("starts interactive Crestodian in the TUI shell", async () => {
|
||||
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 } = createCrestodianTestRuntime();
|
||||
const runInteractiveTui = vi.fn(async () => {});
|
||||
const onReady = vi.fn();
|
||||
let runInteractiveTuiCalls = 0;
|
||||
let onReadyCalls = 0;
|
||||
|
||||
await runCrestodian(
|
||||
{
|
||||
input: { isTTY: true } as unknown as NodeJS.ReadableStream,
|
||||
output: { isTTY: true } as unknown as NodeJS.WritableStream,
|
||||
runInteractiveTui,
|
||||
onReady,
|
||||
runInteractiveTui: async () => {
|
||||
runInteractiveTuiCalls += 1;
|
||||
},
|
||||
onReady: () => {
|
||||
onReadyCalls += 1;
|
||||
},
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(runInteractiveTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ runInteractiveTui }),
|
||||
runtime,
|
||||
);
|
||||
expect(onReady).toHaveBeenCalledTimes(1);
|
||||
expect(runInteractiveTuiCalls).toBe(1);
|
||||
expect(onReadyCalls).toBe(1);
|
||||
expect(lines.join("\n")).not.toContain("Say: status");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
isPersistentCrestodianOperation,
|
||||
type CrestodianCommandDeps,
|
||||
} from "./operations.js";
|
||||
import { formatCrestodianOverview, loadCrestodianOverview } from "./overview.js";
|
||||
import {
|
||||
formatCrestodianOverview,
|
||||
loadCrestodianOverview,
|
||||
type CrestodianOverview,
|
||||
} from "./overview.js";
|
||||
|
||||
type CrestodianInteractiveRunner = (
|
||||
opts: RunCrestodianOptions,
|
||||
@@ -22,12 +26,27 @@ export type RunCrestodianOptions = {
|
||||
interactive?: boolean;
|
||||
onReady?: () => void;
|
||||
deps?: CrestodianCommandDeps;
|
||||
formatOverview?: (overview: CrestodianOverview) => string;
|
||||
loadOverview?: typeof loadCrestodianOverview;
|
||||
planWithAssistant?: CrestodianAssistantPlanner;
|
||||
input?: NodeJS.ReadableStream;
|
||||
output?: NodeJS.WritableStream;
|
||||
runInteractiveTui?: CrestodianInteractiveRunner;
|
||||
};
|
||||
|
||||
function crestodianCommandDepsFromOptions(
|
||||
opts: RunCrestodianOptions,
|
||||
): CrestodianCommandDeps | undefined {
|
||||
if (!opts.deps && !opts.formatOverview && !opts.loadOverview) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...opts.deps,
|
||||
...(opts.formatOverview ? { formatOverview: opts.formatOverview } : {}),
|
||||
...(opts.loadOverview ? { loadOverview: opts.loadOverview } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function runOneShot(
|
||||
input: string,
|
||||
runtime: RuntimeEnv,
|
||||
@@ -36,7 +55,7 @@ async function runOneShot(
|
||||
const operation = await resolveCrestodianOperation(input, runtime, opts);
|
||||
await executeCrestodianOperation(operation, runtime, {
|
||||
approved: opts.yes === true || !isPersistentCrestodianOperation(operation),
|
||||
deps: opts.deps,
|
||||
deps: crestodianCommandDepsFromOptions(opts),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +64,7 @@ export async function runCrestodian(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<void> {
|
||||
if (opts.json) {
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await (opts.loadOverview ?? loadCrestodianOverview)();
|
||||
writeRuntimeJson(runtime, overview);
|
||||
return;
|
||||
}
|
||||
@@ -58,9 +77,9 @@ export async function runCrestodian(
|
||||
delayMs: 0,
|
||||
fallback: "none",
|
||||
},
|
||||
async () => await loadCrestodianOverview(),
|
||||
async () => await (opts.loadOverview ?? loadCrestodianOverview)(),
|
||||
);
|
||||
runtime.log(formatCrestodianOverview(overview));
|
||||
runtime.log((opts.formatOverview ?? formatCrestodianOverview)(overview));
|
||||
runtime.log("");
|
||||
await runOneShot(opts.message, runtime, opts);
|
||||
return;
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
parseCrestodianOperation,
|
||||
type CrestodianOperation,
|
||||
} from "./operations.js";
|
||||
import type { CrestodianOverview } from "./overview.js";
|
||||
import { loadCrestodianOverview, type CrestodianOverview } from "./overview.js";
|
||||
|
||||
export type CrestodianDialogueOptions = {
|
||||
loadOverview?: typeof loadCrestodianOverview;
|
||||
planWithAssistant?: CrestodianAssistantPlanner;
|
||||
};
|
||||
|
||||
@@ -28,8 +29,7 @@ export async function resolveCrestodianOperation(
|
||||
if (!shouldAskAssistant(input, operation)) {
|
||||
return operation;
|
||||
}
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await (opts.loadOverview ?? loadCrestodianOverview)();
|
||||
const planner = opts.planWithAssistant ?? (await import("./assistant.js")).planCrestodianCommand;
|
||||
const plan = await planner({ input, overview });
|
||||
if (!plan) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { CrestodianOverview } from "./overview.js";
|
||||
|
||||
type ConfigModule = typeof import("../config/config.js");
|
||||
type ConfigFileSnapshot = Awaited<ReturnType<ConfigModule["readConfigFileSnapshot"]>>;
|
||||
type CrestodianOverviewLoader = () => Promise<CrestodianOverview>;
|
||||
type CrestodianOverviewFormatter = (overview: CrestodianOverview) => string;
|
||||
|
||||
export type CrestodianOperation =
|
||||
| { kind: "none"; message: string }
|
||||
@@ -47,6 +49,8 @@ export type CrestodianOperationResult = {
|
||||
};
|
||||
|
||||
export type CrestodianCommandDeps = {
|
||||
formatOverview?: CrestodianOverviewFormatter;
|
||||
loadOverview?: CrestodianOverviewLoader;
|
||||
runAgentsAdd?: (
|
||||
opts: {
|
||||
name?: string;
|
||||
@@ -352,6 +356,27 @@ async function readConfigFileSnapshotLazy(): Promise<ConfigFileSnapshot> {
|
||||
return await readConfigFileSnapshot();
|
||||
}
|
||||
|
||||
async function loadOverviewForOperation(
|
||||
deps: CrestodianCommandDeps | undefined,
|
||||
): Promise<CrestodianOverview> {
|
||||
if (deps?.loadOverview) {
|
||||
return await deps.loadOverview();
|
||||
}
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
return await loadCrestodianOverview();
|
||||
}
|
||||
|
||||
async function formatOverviewForOperation(
|
||||
overview: CrestodianOverview,
|
||||
deps: CrestodianCommandDeps | undefined,
|
||||
): Promise<string> {
|
||||
if (deps?.formatOverview) {
|
||||
return deps.formatOverview(overview);
|
||||
}
|
||||
const { formatCrestodianOverview } = await import("./overview.js");
|
||||
return formatCrestodianOverview(overview);
|
||||
}
|
||||
|
||||
async function loadConfigFileMutationHelpers(): Promise<{
|
||||
mutateConfigFile: ConfigModule["mutateConfigFile"];
|
||||
readConfigFileSnapshot: ConfigModule["readConfigFileSnapshot"];
|
||||
@@ -388,9 +413,9 @@ function createNoExitRuntime(runtime: RuntimeEnv): RuntimeEnv {
|
||||
async function resolveTuiAgentId(params: {
|
||||
requestedAgentId: string | undefined;
|
||||
requestedWorkspace?: string;
|
||||
deps?: CrestodianCommandDeps;
|
||||
}): Promise<string | undefined> {
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForOperation(params.deps);
|
||||
const workspace = params.requestedWorkspace
|
||||
? resolveUserPath(params.requestedWorkspace)
|
||||
: undefined;
|
||||
@@ -429,14 +454,12 @@ 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));
|
||||
const overview = await loadOverviewForOperation(opts.deps);
|
||||
runtime.log(await formatOverviewForOperation(overview, opts.deps));
|
||||
return { applied: false };
|
||||
}
|
||||
if (operation.kind === "agents") {
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForOperation(opts.deps);
|
||||
runtime.log(
|
||||
[
|
||||
"Agents:",
|
||||
@@ -456,8 +479,7 @@ export async function executeCrestodianOperation(
|
||||
return { applied: false };
|
||||
}
|
||||
if (operation.kind === "models") {
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForOperation(opts.deps);
|
||||
runtime.log(
|
||||
[
|
||||
`Default model: ${overview.defaultModel ?? "not configured"}`,
|
||||
@@ -480,8 +502,7 @@ export async function executeCrestodianOperation(
|
||||
return { applied: false };
|
||||
}
|
||||
if (operation.kind === "setup") {
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForOperation(opts.deps);
|
||||
const setupModel = chooseSetupModel(overview, operation.model);
|
||||
if (!opts.approved) {
|
||||
const message = [
|
||||
@@ -720,8 +741,7 @@ export async function executeCrestodianOperation(
|
||||
return { applied: false };
|
||||
}
|
||||
if (operation.kind === "gateway-status") {
|
||||
const { loadCrestodianOverview } = await import("./overview.js");
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForOperation(opts.deps);
|
||||
runtime.log(formatGatewayStatusLine(overview));
|
||||
return { applied: false };
|
||||
}
|
||||
@@ -782,6 +802,7 @@ export async function executeCrestodianOperation(
|
||||
const agentId = await resolveTuiAgentId({
|
||||
requestedAgentId: operation.agentId,
|
||||
requestedWorkspace: operation.workspace,
|
||||
deps: opts.deps,
|
||||
});
|
||||
const session = agentId ? buildAgentMainSessionKey({ agentId }) : undefined;
|
||||
const runTui = opts.deps?.runTui ?? (await import("../tui/tui.js")).runTui;
|
||||
|
||||
@@ -1,58 +1,34 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
runTui: vi.fn(async (_opts: unknown) => ({ exitReason: "exit" as const })),
|
||||
}));
|
||||
|
||||
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",
|
||||
formatCrestodianStartupMessage: () => "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 type { CrestodianOverview } from "./overview.js";
|
||||
import { runCrestodianTui } from "./tui-backend.js";
|
||||
|
||||
const overview: CrestodianOverview = {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
function createRuntime(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
log: () => undefined,
|
||||
error: () => undefined,
|
||||
exit: (code) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
@@ -60,32 +36,32 @@ function createRuntime(): RuntimeEnv {
|
||||
}
|
||||
|
||||
describe("runCrestodianTui", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
mocks.runTui.mockClear();
|
||||
});
|
||||
|
||||
it("runs Crestodian inside the shared TUI shell", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "crestodian-tui-"));
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(tempDir, "openclaw.json"));
|
||||
let runTuiCalls = 0;
|
||||
let runTuiOptions: unknown;
|
||||
|
||||
await runCrestodianTui({}, createRuntime());
|
||||
|
||||
expect(mocks.runTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
local: true,
|
||||
session: "agent:crestodian:main",
|
||||
historyLimit: 200,
|
||||
config: {},
|
||||
title: "openclaw crestodian",
|
||||
}),
|
||||
await runCrestodianTui(
|
||||
{
|
||||
deps: {
|
||||
loadOverview: async () => overview,
|
||||
},
|
||||
runTui: async (opts) => {
|
||||
runTuiCalls += 1;
|
||||
runTuiOptions = opts;
|
||||
return { exitReason: "exit" };
|
||||
},
|
||||
},
|
||||
createRuntime(),
|
||||
);
|
||||
const callOptions = mocks.runTui.mock.calls[0]?.[0] as { backend?: unknown } | undefined;
|
||||
expect(callOptions?.backend).toBeTruthy();
|
||||
|
||||
expect(runTuiCalls).toBe(1);
|
||||
expect(runTuiOptions).toMatchObject({
|
||||
local: true,
|
||||
session: "agent:crestodian:main",
|
||||
historyLimit: 200,
|
||||
config: {},
|
||||
title: "openclaw crestodian",
|
||||
});
|
||||
expect((runTuiOptions as { backend?: unknown }).backend).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
TuiModelChoice,
|
||||
TuiSessionList,
|
||||
} from "../tui/tui-backend.js";
|
||||
import { runTui } from "../tui/tui.js";
|
||||
import { runTui as defaultRunTui } from "../tui/tui.js";
|
||||
import type { CrestodianAssistantPlanner } from "./assistant.js";
|
||||
import { approvalQuestion, isYes, resolveCrestodianOperation } from "./dialogue.js";
|
||||
import {
|
||||
@@ -21,10 +21,13 @@ import {
|
||||
} from "./operations.js";
|
||||
import { formatCrestodianStartupMessage, loadCrestodianOverview } from "./overview.js";
|
||||
|
||||
type RunTui = typeof defaultRunTui;
|
||||
|
||||
export type CrestodianTuiOptions = {
|
||||
yes?: boolean;
|
||||
deps?: CrestodianCommandDeps;
|
||||
planWithAssistant?: CrestodianAssistantPlanner;
|
||||
runTui?: RunTui;
|
||||
};
|
||||
|
||||
type CrestodianHistoryMessage = {
|
||||
@@ -52,6 +55,13 @@ function createCaptureRuntime(): CaptureRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOverviewForTui(opts: CrestodianTuiOptions) {
|
||||
if (opts.deps?.loadOverview) {
|
||||
return await opts.deps.loadOverview();
|
||||
}
|
||||
return await loadCrestodianOverview();
|
||||
}
|
||||
|
||||
function message(role: "assistant" | "user", text: string): CrestodianHistoryMessage {
|
||||
return {
|
||||
role,
|
||||
@@ -143,7 +153,7 @@ class CrestodianTuiBackend implements TuiBackend {
|
||||
}
|
||||
|
||||
async listSessions(): Promise<TuiSessionList> {
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForTui(this.opts);
|
||||
const model = splitModelRef(overview.defaultModel);
|
||||
return {
|
||||
ts: Date.now(),
|
||||
@@ -200,7 +210,7 @@ class CrestodianTuiBackend implements TuiBackend {
|
||||
|
||||
async resetSession(): Promise<{ ok: boolean }> {
|
||||
this.pending = null;
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForTui(this.opts);
|
||||
this.messages.splice(
|
||||
0,
|
||||
this.messages.length,
|
||||
@@ -210,7 +220,7 @@ class CrestodianTuiBackend implements TuiBackend {
|
||||
}
|
||||
|
||||
async getGatewayStatus(): Promise<string> {
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForTui(this.opts);
|
||||
return overview.gateway.reachable ? "Gateway reachable" : "Gateway unreachable";
|
||||
}
|
||||
|
||||
@@ -316,8 +326,9 @@ export async function runCrestodianTui(
|
||||
): Promise<void> {
|
||||
let nextInput: string | undefined;
|
||||
for (;;) {
|
||||
const overview = await loadCrestodianOverview();
|
||||
const overview = await loadOverviewForTui(opts);
|
||||
const backend = new CrestodianTuiBackend(opts, formatCrestodianStartupMessage(overview));
|
||||
const runTui = opts.runTui ?? defaultRunTui;
|
||||
await runTui({
|
||||
local: true,
|
||||
session: CRESTODIAN_SESSION_KEY,
|
||||
|
||||
@@ -1,60 +1,64 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tryHandleRootHelpFastPath } from "./entry.js";
|
||||
|
||||
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
|
||||
vi.mock("./cli/root-help-metadata.js", () => ({
|
||||
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
|
||||
}));
|
||||
|
||||
describe("entry root help fast path", () => {
|
||||
it("prefers precomputed root help text when available", async () => {
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
|
||||
let outputPrecomputedRootHelpTextCalls = 0;
|
||||
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
env: {},
|
||||
outputPrecomputedRootHelpText: () => {
|
||||
outputPrecomputedRootHelpTextCalls += 1;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputPrecomputedRootHelpTextCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("renders root help without importing the full program", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
let outputRootHelpCalls = 0;
|
||||
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputRootHelpCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("ignores non-root help invocations", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
let outputRootHelpCalls = 0;
|
||||
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], {
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(handled).toBe(false);
|
||||
expect(outputRootHelpMock).not.toHaveBeenCalled();
|
||||
expect(outputRootHelpCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("skips the host help fast path when a container target is active", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
let outputRootHelpCalls = 0;
|
||||
|
||||
const handled = await tryHandleRootHelpFastPath(
|
||||
["node", "openclaw", "--container", "demo", "--help"],
|
||||
{
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
outputRootHelp: () => {
|
||||
outputRootHelpCalls += 1;
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(handled).toBe(false);
|
||||
expect(outputRootHelpMock).not.toHaveBeenCalled();
|
||||
expect(outputRootHelpCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,7 @@ if (
|
||||
export async function tryHandleRootHelpFastPath(
|
||||
argv: string[],
|
||||
deps: {
|
||||
outputPrecomputedRootHelpText?: () => boolean;
|
||||
outputRootHelp?: () => void | Promise<void>;
|
||||
onError?: (error: unknown) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -159,7 +160,9 @@ export async function tryHandleRootHelpFastPath(
|
||||
await deps.outputRootHelp();
|
||||
return true;
|
||||
}
|
||||
const { outputPrecomputedRootHelpText } = await import("./cli/root-help-metadata.js");
|
||||
const outputPrecomputedRootHelpText =
|
||||
deps.outputPrecomputedRootHelpText ??
|
||||
(await import("./cli/root-help-metadata.js")).outputPrecomputedRootHelpText;
|
||||
if (!outputPrecomputedRootHelpText()) {
|
||||
const { outputRootHelp } = await import("./cli/program/root-help.js");
|
||||
await outputRootHelp();
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const libraryPath = resolve(dirname(fileURLToPath(import.meta.url)), "library.ts");
|
||||
const libraryPath = new URL("./library.ts", import.meta.url);
|
||||
const lazyRuntimeSpecifiers = [
|
||||
"./auto-reply/reply.runtime.js",
|
||||
"./cli/prompt.js",
|
||||
@@ -15,32 +12,17 @@ const lazyRuntimeSpecifiers = [
|
||||
|
||||
function readLibraryModuleImports() {
|
||||
const sourceText = readFileSync(libraryPath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(libraryPath, sourceText, ts.ScriptTarget.Latest, true);
|
||||
const staticImports = new Set<string>();
|
||||
const dynamicImports = new Set<string>();
|
||||
const staticImportPattern = /(?:^|\n)\s*import\s+(?!type\b)[\s\S]*?\s+from\s+["']([^"']+)["']/g;
|
||||
const dynamicImportPattern = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (
|
||||
ts.isImportDeclaration(node) &&
|
||||
ts.isStringLiteral(node.moduleSpecifier) &&
|
||||
!node.importClause?.isTypeOnly
|
||||
) {
|
||||
staticImports.add(node.moduleSpecifier.text);
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
node.arguments.length === 1 &&
|
||||
ts.isStringLiteral(node.arguments[0])
|
||||
) {
|
||||
dynamicImports.add(node.arguments[0].text);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
for (const match of sourceText.matchAll(staticImportPattern)) {
|
||||
staticImports.add(match[1]);
|
||||
}
|
||||
for (const match of sourceText.matchAll(dynamicImportPattern)) {
|
||||
dynamicImports.add(match[1]);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return { dynamicImports, staticImports };
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,13 @@ describe("ensureDir", () => {
|
||||
describe("sleep", () => {
|
||||
it("resolves after delay using fake timers", async () => {
|
||||
vi.useFakeTimers();
|
||||
const promise = sleep(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
vi.useRealTimers();
|
||||
try {
|
||||
const promise = sleep(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,10 +68,11 @@ describe("resolveHomeDir", () => {
|
||||
it("prefers OPENCLAW_HOME over HOME", () => {
|
||||
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
||||
vi.stubEnv("HOME", "/home/other");
|
||||
|
||||
expect(resolveHomeDir()).toBe(path.resolve("/srv/openclaw-home"));
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
try {
|
||||
expect(resolveHomeDir()).toBe(path.resolve("/srv/openclaw-home"));
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,12 +80,13 @@ describe("shortenHomePath", () => {
|
||||
it("uses $OPENCLAW_HOME prefix when OPENCLAW_HOME is set", () => {
|
||||
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
||||
vi.stubEnv("HOME", "/home/other");
|
||||
|
||||
expect(shortenHomePath(`${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`)).toBe(
|
||||
"$OPENCLAW_HOME/.openclaw/openclaw.json",
|
||||
);
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
try {
|
||||
expect(shortenHomePath(`${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`)).toBe(
|
||||
"$OPENCLAW_HOME/.openclaw/openclaw.json",
|
||||
);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,12 +94,15 @@ describe("shortenHomeInString", () => {
|
||||
it("uses $OPENCLAW_HOME replacement when OPENCLAW_HOME is set", () => {
|
||||
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
||||
vi.stubEnv("HOME", "/home/other");
|
||||
|
||||
expect(
|
||||
shortenHomeInString(`config: ${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`),
|
||||
).toBe("config: $OPENCLAW_HOME/.openclaw/openclaw.json");
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
try {
|
||||
expect(
|
||||
shortenHomeInString(
|
||||
`config: ${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`,
|
||||
),
|
||||
).toBe("config: $OPENCLAW_HOME/.openclaw/openclaw.json");
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,10 +124,11 @@ describe("resolveUserPath", () => {
|
||||
it("prefers OPENCLAW_HOME for tilde expansion", () => {
|
||||
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
||||
vi.stubEnv("HOME", "/home/other");
|
||||
|
||||
expect(resolveUserPath("~/openclaw")).toBe(path.resolve("/srv/openclaw-home", "openclaw"));
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
try {
|
||||
expect(resolveUserPath("~/openclaw")).toBe(path.resolve("/srv/openclaw-home", "openclaw"));
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the provided env for tilde expansion", () => {
|
||||
|
||||
@@ -62,19 +62,27 @@ export const forcedUnitFastTestFiles = [
|
||||
"packages/memory-host-sdk/src/host/session-files.test.ts",
|
||||
"src/acp/client.test.ts",
|
||||
"src/acp/control-plane/manager.test.ts",
|
||||
"src/acp/translator.cancel-scoping.test.ts",
|
||||
"src/acp/translator.stop-reason.test.ts",
|
||||
"src/acp/persistent-bindings.test.ts",
|
||||
"src/acp/server.startup.test.ts",
|
||||
"src/acp/translator.session-rate-limit.test.ts",
|
||||
"src/browser-lifecycle-cleanup.test.ts",
|
||||
"src/crestodian/overview.test.ts",
|
||||
"src/crestodian/crestodian.test.ts",
|
||||
"src/crestodian/operations.test.ts",
|
||||
"src/crestodian/rescue-message.test.ts",
|
||||
"src/crestodian/tui-backend.test.ts",
|
||||
"src/flows/channel-setup.test.ts",
|
||||
"src/context-engine/context-engine.test.ts",
|
||||
"src/canvas-host/server.state-dir.test.ts",
|
||||
"src/docker-image-digests.test.ts",
|
||||
"src/dockerfile.test.ts",
|
||||
"src/entry.test.ts",
|
||||
"src/i18n/registry.test.ts",
|
||||
"src/install-sh-version.test.ts",
|
||||
"src/logger.test.ts",
|
||||
"src/library.test.ts",
|
||||
"src/memory-host-sdk/host/internal.test.ts",
|
||||
"src/memory-host-sdk/host/batch-http.test.ts",
|
||||
"src/memory-host-sdk/host/backend-config.test.ts",
|
||||
@@ -84,9 +92,13 @@ export const forcedUnitFastTestFiles = [
|
||||
"src/mcp/channel-server.shutdown-unhandled-rejection.test.ts",
|
||||
"src/node-host/invoke-system-run-plan.test.ts",
|
||||
"src/node-host/invoke-system-run.test.ts",
|
||||
"src/pairing/allow-from-store-read.test.ts",
|
||||
"src/pairing/pairing-store.test.ts",
|
||||
"src/plugin-sdk/memory-host-events.test.ts",
|
||||
"src/proxy-capture/store.sqlite.test.ts",
|
||||
"src/security/audit-exec-surface.test.ts",
|
||||
"src/security/audit-extra.async.test.ts",
|
||||
"src/security/dm-policy-shared.test.ts",
|
||||
"src/security/audit-plugins-trust.test.ts",
|
||||
"src/security/audit-workspace-skill-escape.test.ts",
|
||||
"src/security/external-content.test.ts",
|
||||
@@ -94,10 +106,13 @@ export const forcedUnitFastTestFiles = [
|
||||
"src/security/skill-scanner.test.ts",
|
||||
"src/security/windows-acl.test.ts",
|
||||
"src/realtime-transcription/websocket-session.test.ts",
|
||||
"src/routing/resolve-route.test.ts",
|
||||
"src/trajectory/export.test.ts",
|
||||
"src/tts/provider-registry.test.ts",
|
||||
"src/tts/status-config.test.ts",
|
||||
"src/terminal/table.test.ts",
|
||||
"src/test-helpers/state-dir-env.test.ts",
|
||||
"src/utils.test.ts",
|
||||
"src/version.test.ts",
|
||||
];
|
||||
const forcedUnitFastTestFileSet = new Set(forcedUnitFastTestFiles);
|
||||
|
||||
Reference in New Issue
Block a user