mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
docs(cli): explain reserved catalog roots and drop stale entries (#118040)
* docs(cli): explain reserved catalog roots and drop stale entries * test(cli): use plugin registration for catalog guard * test(cli): load memory CLI through public facade * test(cli): keep plugin catalog paths owner-scoped
This commit is contained in:
committed by
GitHub
parent
6e87d3b5f3
commit
b5a7bc280e
@@ -150,11 +150,6 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
exact: true,
|
||||
policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" },
|
||||
},
|
||||
{
|
||||
commandPath: ["config", "models"],
|
||||
exact: true,
|
||||
policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" },
|
||||
},
|
||||
{
|
||||
commandPath: ["migrate"],
|
||||
policy: { configGuard: "skip", loadPlugins: "never", networkProxy: "bypass" },
|
||||
@@ -201,7 +196,6 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
{ commandPath: ["gateway", "call"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["gateway", "diagnostics"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["gateway", "discover"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["gateway", "export"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{
|
||||
commandPath: ["gateway", "health"],
|
||||
exact: true,
|
||||
@@ -331,10 +325,14 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
route: { id: "tasks-list" },
|
||||
},
|
||||
{
|
||||
// This unregistered root is reserved so plugin registration cannot claim it;
|
||||
// the catalog entry preserves its startup policy.
|
||||
commandPath: ["tool"],
|
||||
policy: { loadPlugins: "never", ensureCliPath: false, networkProxy: "bypass" },
|
||||
},
|
||||
{
|
||||
// This unregistered root is reserved so plugin registration cannot claim it;
|
||||
// the catalog entry preserves its startup policy.
|
||||
commandPath: ["tools"],
|
||||
policy: { loadPlugins: "never", ensureCliPath: false, networkProxy: "bypass" },
|
||||
},
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
// registered Commander commands. Keep those user-facing descriptions aligned.
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cliCommandCatalog } from "../command-catalog.js";
|
||||
import { isReservedNonPluginCommandRoot } from "../command-registration-policy.js";
|
||||
import { collectShellCompletionCommandTree } from "../completion-command-tree.js";
|
||||
import { getCoreCliCommandNames, registerCoreCliByName } from "./command-registry-core.js";
|
||||
import { createProgramContext } from "./context.js";
|
||||
import { getCoreCliCommandDescriptors } from "./core-command-descriptors.js";
|
||||
import { registerSubCliByName } from "./register.subclis.js";
|
||||
import { registerSubCliByName, registerSubCliCommands } from "./register.subclis.js";
|
||||
import { getSubCliEntries } from "./subcli-descriptors.js";
|
||||
|
||||
const RESERVED_CATALOG_ROOTS = {
|
||||
tool: "reserved so plugin registration cannot claim this unregistered root",
|
||||
tools: "reserved so plugin registration cannot claim this unregistered root",
|
||||
} as const;
|
||||
|
||||
const PLUGIN_CATALOG_PATHS = {
|
||||
"memory search": "registered and covered by the memory-core plugin",
|
||||
"memory status": "registered and covered by the memory-core plugin",
|
||||
} as const;
|
||||
|
||||
const JSON_NOT_APPLICABLE = {
|
||||
namespaces: {
|
||||
reason: "command group only; reporting subcommands declare JSON output individually",
|
||||
@@ -242,6 +254,16 @@ function supportsJsonOutput(path: string, command: Command): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function collectRegisteredCommandPaths(...programs: Command[]): Set<string> {
|
||||
return new Set(
|
||||
programs.flatMap((program) =>
|
||||
collectShellCompletionCommandTree(program).descendants.flatMap((context) =>
|
||||
context.pathVariants.map((path) => path.join(" ")),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe("root command descriptions", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENCLAW_ENABLE_PRIVATE_QA_CLI", "");
|
||||
@@ -285,6 +307,56 @@ describe("root command descriptions", () => {
|
||||
expect(mismatches, "root help vs registered command description drift").toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps startup policy catalog paths registered or explicitly reserved", async () => {
|
||||
const program = await registerAllBuiltInCommands();
|
||||
|
||||
// Private QA is a lazy source-checkout command. Its root placeholder proves
|
||||
// registration without importing the private build omitted from normal dist.
|
||||
vi.stubEnv("OPENCLAW_ENABLE_PRIVATE_QA_CLI", "1");
|
||||
const lazyProgram = new Command().name("openclaw");
|
||||
registerSubCliCommands(lazyProgram, ["node", "openclaw", "--help"]);
|
||||
|
||||
const registeredPaths = collectRegisteredCommandPaths(program, lazyProgram);
|
||||
const catalogPaths = new Set(cliCommandCatalog.map((entry) => entry.commandPath.join(" ")));
|
||||
const reservedPaths = new Set(Object.keys(RESERVED_CATALOG_ROOTS));
|
||||
const pluginPaths = new Set(Object.keys(PLUGIN_CATALOG_PATHS));
|
||||
|
||||
expect(
|
||||
Object.entries({ ...RESERVED_CATALOG_ROOTS, ...PLUGIN_CATALOG_PATHS }).filter(
|
||||
([, reason]) => reason.trim().length === 0,
|
||||
),
|
||||
"every catalog exception must document why core has no command",
|
||||
).toEqual([]);
|
||||
|
||||
const missing = [...catalogPaths].filter(
|
||||
(path) => !registeredPaths.has(path) && !reservedPaths.has(path) && !pluginPaths.has(path),
|
||||
);
|
||||
expect(missing, "catalog entries with no registered command or reserved-root decision").toEqual(
|
||||
[],
|
||||
);
|
||||
|
||||
const staleReservedRoots = [...reservedPaths].filter(
|
||||
(path) => !catalogPaths.has(path) || !isReservedNonPluginCommandRoot(path),
|
||||
);
|
||||
expect(
|
||||
staleReservedRoots,
|
||||
"reserved catalog roots must remain cataloged and blocked from plugin registration",
|
||||
).toEqual([]);
|
||||
|
||||
const stalePluginPaths = [...pluginPaths].filter(
|
||||
(path) => !catalogPaths.has(path) || registeredPaths.has(path),
|
||||
);
|
||||
expect(stalePluginPaths, "plugin catalog paths must remain plugin-owned").toEqual([]);
|
||||
|
||||
const reservedRootsThatNowRegister = [...reservedPaths].filter((path) =>
|
||||
registeredPaths.has(path),
|
||||
);
|
||||
expect(
|
||||
reservedRootsThatNowRegister,
|
||||
"registered commands must leave the reserved-root exception list",
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("classifies every built-in command as JSON output or explicitly not applicable", async () => {
|
||||
const program = await registerAllBuiltInCommands();
|
||||
const contexts = collectShellCompletionCommandTree(program).descendants;
|
||||
|
||||
Reference in New Issue
Block a user