mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
7422222788
* feat(claws): add local project lifecycle * fix(claws): harden local project lifecycle * fix(claws): harden project source selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3cdcdb00-ade8-4e61-85a7-8151b35f216a * fix(claws): preserve project archive paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * fix(claws): harden author lifecycle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * chore(claws): normalize authoring test mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * fix(claws): normalize archive path spellings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * fix(claws): exclude repository-local sources Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * fix(claws): satisfy CI contracts Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * chore: record CI fix attribution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 * fix(claws): reject excluded manifest links Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3cdcdb00-ade8-4e61-85a7-8151b35f216a Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085
415 lines
12 KiB
TypeScript
415 lines
12 KiB
TypeScript
// Command startup policy tests cover which CLI commands require startup side effects.
|
|
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cliCommandCatalog } from "./command-catalog.js";
|
|
import { resolveCliExecutionStartupContext } from "./command-execution-startup.js";
|
|
import { resolveCliStartupPolicy } from "./command-startup-policy.js";
|
|
|
|
function resolvePolicy(params: {
|
|
argv?: string[];
|
|
commandPath: string[];
|
|
jsonOutputMode?: boolean;
|
|
env?: NodeJS.ProcessEnv;
|
|
}) {
|
|
return resolveCliStartupPolicy({
|
|
jsonOutputMode: false,
|
|
...params,
|
|
});
|
|
}
|
|
|
|
describe("command-startup-policy", () => {
|
|
afterEach(() => {
|
|
vi.doUnmock("./command-path-policy.js");
|
|
vi.resetModules();
|
|
});
|
|
|
|
it("resolves config guard policy for Commander and invocation-aware commands", () => {
|
|
for (const commandPath of [
|
|
["backup", "create"],
|
|
["config"],
|
|
["config", "file"],
|
|
["config", "validate"],
|
|
["config", "schema"],
|
|
["docs"],
|
|
["agent", "exec"],
|
|
["status"],
|
|
["agents", "bindings"],
|
|
["approvals", "pending"],
|
|
["commitments"],
|
|
["skills"],
|
|
["skills", "list"],
|
|
["skills", "check"],
|
|
["skills", "info"],
|
|
["skills", "search"],
|
|
["hooks"],
|
|
["hooks", "list"],
|
|
["hooks", "info"],
|
|
["hooks", "check"],
|
|
["memory", "search"],
|
|
["memory", "status"],
|
|
["gateway", "stability"],
|
|
["gateway", "usage-cost"],
|
|
]) {
|
|
expect(resolvePolicy({ commandPath }).skipConfigGuard, commandPath.join(" ")).toBe(true);
|
|
}
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent"],
|
|
commandPath: ["agent"],
|
|
}).skipConfigGuard,
|
|
).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "--local"],
|
|
commandPath: ["agent"],
|
|
}).skipConfigGuard,
|
|
).toBe(false);
|
|
expect(resolvePolicy({ commandPath: ["config", "set"] }).skipConfigGuard).toBe(false);
|
|
for (const flag of ["--index", "--fix"]) {
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "memory", "status", flag],
|
|
commandPath: ["memory", "status"],
|
|
}).skipConfigGuard,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("skips operator-state startup for local Claw authoring commands only", () => {
|
|
for (const subcommand of ["create", "validate", "build", "dev"]) {
|
|
const commandPath = ["claws", subcommand];
|
|
expect(resolvePolicy({ commandPath }).skipConfigGuard, commandPath.join(" ")).toBe(true);
|
|
}
|
|
for (const subcommand of ["add", "update", "remove"]) {
|
|
const commandPath = ["claws", subcommand];
|
|
expect(resolvePolicy({ commandPath }).skipConfigGuard, commandPath.join(" ")).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("skips the config guard for exact root update dry-runs", () => {
|
|
for (const argv of [
|
|
["node", "openclaw", "update", "--dry-run"],
|
|
["node", "openclaw", "--profile", "work", "update", "--dry-run"],
|
|
["node", "openclaw", "--update", "--dry-run"],
|
|
]) {
|
|
expect(
|
|
resolvePolicy({
|
|
argv,
|
|
commandPath: ["update"],
|
|
}).skipConfigGuard,
|
|
argv.join(" "),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps the config guard for non-dry-run and descendant update invocations", () => {
|
|
for (const testCase of [
|
|
{
|
|
argv: ["node", "openclaw", "update"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "--tag", "--dry-run"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "--channel", "--dry-run"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "--timeout", "--dry-run"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "--tag=--dry-run"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "--", "--dry-run"],
|
|
commandPath: ["update"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "status", "--dry-run"],
|
|
commandPath: ["update", "status"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "repair", "--dry-run"],
|
|
commandPath: ["update", "repair"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "finalize", "--dry-run"],
|
|
commandPath: ["update", "finalize"],
|
|
},
|
|
{
|
|
argv: ["node", "openclaw", "update", "wizard", "--dry-run"],
|
|
commandPath: ["update", "wizard"],
|
|
},
|
|
]) {
|
|
expect(resolvePolicy(testCase).skipConfigGuard, testCase.argv.join(" ")).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("keeps every route-first command on the same config guard declaration as Commander", () => {
|
|
for (const entry of cliCommandCatalog.filter((candidate) => candidate.route)) {
|
|
expect(entry.policy?.configGuard, entry.commandPath.join(" ")).toBeDefined();
|
|
for (const jsonOutputMode of [false, true]) {
|
|
const argv = ["node", "openclaw", ...entry.commandPath];
|
|
const expectedSkip = entry.commandPath.join(" ") !== "config unset";
|
|
const routed = resolveCliExecutionStartupContext({ argv, jsonOutputMode });
|
|
const commander = resolveCliExecutionStartupContext({
|
|
argv,
|
|
commandPath: [...entry.commandPath],
|
|
jsonOutputMode,
|
|
});
|
|
expect(routed.startupPolicy.skipConfigGuard, entry.commandPath.join(" ")).toBe(
|
|
commander.startupPolicy.skipConfigGuard,
|
|
);
|
|
expect(routed.startupPolicy.skipConfigGuard, entry.commandPath.join(" ")).toBe(
|
|
expectedSkip,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("skips when-suppressed guards only for suppressed output", async () => {
|
|
vi.doMock("./command-path-policy.js", () => ({
|
|
resolveCliCommandPathPolicy: () => ({
|
|
configGuard: "when-suppressed",
|
|
loadPlugins: "never",
|
|
pluginRegistry: { scope: "all" },
|
|
ownsProtocolStdout: false,
|
|
hideBanner: false,
|
|
ensureCliPath: true,
|
|
networkProxy: "default",
|
|
}),
|
|
}));
|
|
const { resolveCliStartupPolicy: resolveWithSuppressedGuard } = await importFreshModule<
|
|
typeof import("./command-startup-policy.js")
|
|
>(import.meta.url, "./command-startup-policy.js?when-suppressed");
|
|
|
|
expect(
|
|
resolveWithSuppressedGuard({ commandPath: ["test"], jsonOutputMode: false }).skipConfigGuard,
|
|
).toBe(false);
|
|
expect(
|
|
resolveWithSuppressedGuard({ commandPath: ["test"], jsonOutputMode: true }).skipConfigGuard,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("matches plugin preload policy", () => {
|
|
for (const commandPath of [
|
|
["memory", "index"],
|
|
["memory", "search"],
|
|
["memory", "status"],
|
|
]) {
|
|
const policy = resolvePolicy({ commandPath });
|
|
expect(policy.loadPlugins, commandPath.join(" ")).toBe(true);
|
|
expect(policy.pluginRegistry, commandPath.join(" ")).toEqual({ scope: "memory" });
|
|
}
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["health"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "status"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "list"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "add"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["channels", "logs"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["message", "send"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["message", "send"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "--json"],
|
|
commandPath: ["agent"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "--json", "--local"],
|
|
commandPath: ["agent"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent", "exec", "fix it"],
|
|
commandPath: ["agent", "exec"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
argv: ["node", "openclaw", "agent"],
|
|
commandPath: ["agent"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "list"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "list"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "bind"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "bindings"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "unbind"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "set-identity"],
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["agents", "delete"],
|
|
jsonOutputMode: true,
|
|
}).loadPlugins,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches banner suppression policy", () => {
|
|
expect(resolvePolicy({ commandPath: ["update", "status"], env: {} }).hideBanner).toBe(true);
|
|
expect(resolvePolicy({ commandPath: ["completion"], env: {} }).hideBanner).toBe(true);
|
|
expect(
|
|
resolvePolicy({
|
|
commandPath: ["status"],
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_HIDE_BANNER: "1",
|
|
},
|
|
}).hideBanner,
|
|
).toBe(true);
|
|
expect(resolvePolicy({ commandPath: ["status"], env: {} }).hideBanner).toBe(false);
|
|
});
|
|
|
|
it("uses process env banner suppression when startup env is omitted", () => {
|
|
const originalHideBanner = process.env.OPENCLAW_HIDE_BANNER;
|
|
try {
|
|
process.env.OPENCLAW_HIDE_BANNER = "1";
|
|
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: false,
|
|
}).hideBanner,
|
|
).toBe(true);
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: false,
|
|
env: {},
|
|
}).hideBanner,
|
|
).toBe(false);
|
|
} finally {
|
|
if (originalHideBanner === undefined) {
|
|
delete process.env.OPENCLAW_HIDE_BANNER;
|
|
} else {
|
|
process.env.OPENCLAW_HIDE_BANNER = originalHideBanner;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("aggregates startup policy for both dispatch paths", () => {
|
|
expect(
|
|
resolveCliStartupPolicy({
|
|
commandPath: ["status"],
|
|
jsonOutputMode: true,
|
|
env: {},
|
|
}),
|
|
).toEqual({
|
|
suppressDoctorStdout: true,
|
|
hideBanner: false,
|
|
skipConfigGuard: true,
|
|
loadPlugins: false,
|
|
pluginRegistry: { scope: "channels" },
|
|
});
|
|
});
|
|
|
|
it("suppresses startup stdout for the mcp serve protocol", () => {
|
|
expect(resolvePolicy({ commandPath: ["mcp", "serve"] }).suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("reserves stdout for the node worker protocol", () => {
|
|
const policy = resolvePolicy({ commandPath: ["node", "worker"] });
|
|
|
|
expect(policy.hideBanner).toBe(true);
|
|
expect(policy.loadPlugins).toBe(false);
|
|
expect(policy.suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("isolates cloud worker startup", () => {
|
|
const policy = resolvePolicy({ commandPath: ["worker"] });
|
|
|
|
expect(policy.skipConfigGuard).toBe(true);
|
|
expect(policy.hideBanner).toBe(true);
|
|
expect(policy.loadPlugins).toBe(false);
|
|
expect(policy.suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("suppresses startup stdout for the bare acp protocol", () => {
|
|
expect(resolvePolicy({ commandPath: ["acp"] }).suppressDoctorStdout).toBe(true);
|
|
});
|
|
|
|
it("keeps startup stdout for non-protocol commands", () => {
|
|
expect(resolvePolicy({ commandPath: ["mcp", "list"] }).suppressDoctorStdout).toBe(false);
|
|
expect(resolvePolicy({ commandPath: ["acp", "client"] }).suppressDoctorStdout).toBe(false);
|
|
expect(resolvePolicy({ commandPath: ["status"] }).suppressDoctorStdout).toBe(false);
|
|
});
|
|
});
|