fix(ci): align skills api and trim status startup

This commit is contained in:
Peter Steinberger
2026-03-27 22:24:31 +00:00
parent 4d7c6519fc
commit a5cb9ec674
23 changed files with 538 additions and 237 deletions

View File

@@ -66,15 +66,10 @@ describe("tryRouteCli", () => {
}
});
it("passes suppressDoctorStdout=true for routed --json commands", async () => {
it("skips config guard for routed status --json commands", async () => {
await expect(tryRouteCli(["node", "openclaw", "status", "--json"])).resolves.toBe(true);
expect(ensureConfigReadyMock).toHaveBeenCalledWith(
expect.objectContaining({
commandPath: ["status"],
suppressDoctorStdout: true,
}),
);
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
});

View File

@@ -1,9 +1,7 @@
import { isTruthyEnvValue } from "../infra/env.js";
import { loggingState } from "../logging/state.js";
import { defaultRuntime } from "../runtime.js";
import { VERSION } from "../version.js";
import { getCommandPathWithRootOptions, hasFlag, hasHelpOrVersion } from "./argv.js";
import { emitCliBanner } from "./banner.js";
import { findRoutedCommand } from "./program/routes.js";
async function prepareRoutedCommand(params: {
@@ -12,13 +10,22 @@ async function prepareRoutedCommand(params: {
loadPlugins?: boolean | ((argv: string[]) => boolean);
}) {
const suppressDoctorStdout = hasFlag(params.argv, "--json");
emitCliBanner(VERSION, { argv: params.argv });
const { ensureConfigReady } = await import("./program/config-guard.js");
await ensureConfigReady({
runtime: defaultRuntime,
commandPath: params.commandPath,
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
});
const skipConfigGuard = params.commandPath[0] === "status" && suppressDoctorStdout;
if (!suppressDoctorStdout && process.stdout.isTTY) {
const [{ emitCliBanner }, { VERSION }] = await Promise.all([
import("./banner.js"),
import("../version.js"),
]);
emitCliBanner(VERSION, { argv: params.argv });
}
if (!skipConfigGuard) {
const { ensureConfigReady } = await import("./program/config-guard.js");
await ensureConfigReady({
runtime: defaultRuntime,
commandPath: params.commandPath,
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
});
}
const shouldLoadPlugins =
typeof params.loadPlugins === "function" ? params.loadPlugins(params.argv) : params.loadPlugins;
if (shouldLoadPlugins) {

View File

@@ -7,6 +7,7 @@ const normalizeEnvMock = vi.hoisted(() => vi.fn());
const ensurePathMock = vi.hoisted(() => vi.fn());
const assertRuntimeMock = vi.hoisted(() => vi.fn());
const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => {}));
const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false));
const outputRootHelpMock = vi.hoisted(() => vi.fn());
const buildProgramMock = vi.hoisted(() => vi.fn());
const maybeRunCliInContainerMock = vi.hoisted(() =>
@@ -44,6 +45,10 @@ vi.mock("../plugins/memory-runtime.js", () => ({
closeActiveMemorySearchManagers: closeActiveMemorySearchManagersMock,
}));
vi.mock("../plugins/memory-state.js", () => ({
hasMemoryRuntime: hasMemoryRuntimeMock,
}));
vi.mock("./program/root-help.js", () => ({
outputRootHelp: outputRootHelpMock,
}));
@@ -57,6 +62,7 @@ const { runCli } = await import("./run-main.js");
describe("runCli exit behavior", () => {
beforeEach(() => {
vi.clearAllMocks();
hasMemoryRuntimeMock.mockReturnValue(false);
});
it("does not force process.exit after successful routed command", async () => {
@@ -69,7 +75,7 @@ describe("runCli exit behavior", () => {
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "status"]);
expect(tryRouteCliMock).toHaveBeenCalledWith(["node", "openclaw", "status"]);
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
exitSpy.mockRestore();
});
@@ -85,11 +91,20 @@ describe("runCli exit behavior", () => {
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
expect(buildProgramMock).not.toHaveBeenCalled();
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
exitSpy.mockRestore();
});
it("closes memory managers when a runtime was registered", async () => {
tryRouteCliMock.mockResolvedValueOnce(true);
hasMemoryRuntimeMock.mockReturnValue(true);
await runCli(["node", "openclaw", "status"]);
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
});
it("returns after a handled container-target invocation", async () => {
maybeRunCliInContainerMock.mockReturnValueOnce({ handled: true, exitCode: 0 });

View File

@@ -1,11 +1,15 @@
import { existsSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { resolveStateDir } from "../config/paths.js";
import { normalizeEnv } from "../infra/env.js";
import { formatUncaughtError } from "../infra/errors.js";
import { isMainModule } from "../infra/is-main.js";
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
import { enableConsoleCapture } from "../logging.js";
import { hasMemoryRuntime } from "../plugins/memory-state.js";
import {
getCommandPathWithRootOptions,
getPrimaryCommand,
@@ -13,12 +17,14 @@ import {
isRootHelpInvocation,
} from "./argv.js";
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
import { loadCliDotEnv } from "./dotenv.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
import { tryRouteCli } from "./route.js";
import { normalizeWindowsArgv } from "./windows-argv.js";
async function closeCliMemoryManagers(): Promise<void> {
if (!hasMemoryRuntime()) {
return;
}
try {
const { closeActiveMemorySearchManagers } = await import("../plugins/memory-runtime.js");
await closeActiveMemorySearchManagers();
@@ -80,6 +86,13 @@ export function shouldUseRootHelpFastPath(argv: string[]): boolean {
return isRootHelpInvocation(argv);
}
function shouldLoadCliDotEnv(env: NodeJS.ProcessEnv = process.env): boolean {
if (existsSync(path.join(process.cwd(), ".env"))) {
return true;
}
return existsSync(path.join(resolveStateDir(env), ".env"));
}
export async function runCli(argv: string[] = process.argv) {
const originalArgv = normalizeWindowsArgv(argv);
const parsedContainer = parseCliContainerArgs(originalArgv);
@@ -108,7 +121,10 @@ export async function runCli(argv: string[] = process.argv) {
}
let normalizedArgv = parsedProfile.argv;
loadCliDotEnv({ quiet: true });
if (shouldLoadCliDotEnv()) {
const { loadCliDotEnv } = await import("./dotenv.js");
loadCliDotEnv({ quiet: true });
}
normalizeEnv();
if (shouldEnsureCliPath(normalizedArgv)) {
ensureOpenClawCliOnPath();

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import type { SkillEntry } from "../agents/skills.js";
@@ -38,7 +39,11 @@ describe("skills-cli (e2e)", () => {
description: "Capture UI screenshots",
filePath: path.join(baseDir, "SKILL.md"),
baseDir,
source: "openclaw-bundled",
sourceInfo: createSyntheticSourceInfo(path.join(baseDir, "SKILL.md"), {
source: "openclaw-bundled",
scope: "project",
baseDir,
}),
disableModelInvocation: false,
},
frontmatter: {},