Fix memory plugin CLI help dispatch (#83841)

* fix cli help for active memory plugin

* docs add changelog for memory cli help

* test fix root help mock type
This commit is contained in:
Josh Avant
2026-05-18 20:35:55 -05:00
committed by GitHub
parent 0b4fc26d4a
commit eb6dd2c65d
14 changed files with 434 additions and 24 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ describe("command-registration-policy", () => {
primary: "voicecall",
hasBuiltinPrimary: false,
}),
).toBe(true);
).toBe(false);
expect(
shouldSkipPluginCommandRegistration({
argv: ["node", "openclaw", "help", "--help"],
+3 -1
View File
@@ -22,7 +22,9 @@ export function shouldSkipPluginCommandRegistration(params: {
return invocation.hasHelpOrVersion && invocation.commandPath.length <= 1;
}
if (invocation.hasHelpOrVersion) {
return true;
return (
!params.primary || params.hasBuiltinPrimary || isReservedNonPluginCommandRoot(params.primary)
);
}
if (params.hasBuiltinPrimary) {
return true;
+51
View File
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadRootHelpRenderOptionsForConfigSensitivePlugins } from "./root-help-live-config.js";
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("../config/config.js", () => ({
readConfigFileSnapshot: readConfigFileSnapshotMock,
}));
describe("root help live config", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("uses precomputed help when plugin-sensitive config is invalid", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: false,
sourceConfig: {
plugins: {
slots: {
memory: "memory-lancedb",
},
},
},
runtimeConfig: {},
});
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins({})).resolves.toBeNull();
});
it("uses snapshot runtime config when plugin config affects help", async () => {
const runtimeConfig = {
plugins: {
slots: {
memory: "memory-lancedb",
},
},
};
const env = {};
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
sourceConfig: runtimeConfig,
runtimeConfig,
});
await expect(loadRootHelpRenderOptionsForConfigSensitivePlugins(env)).resolves.toEqual({
config: runtimeConfig,
env,
});
});
});
+53
View File
@@ -0,0 +1,53 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { RootHelpRenderOptions } from "./program/root-help.js";
function hasEntries(value: object | undefined): boolean {
return !!value && Object.keys(value).length > 0;
}
function hasListEntries(value: string[] | undefined): boolean {
return Array.isArray(value) && value.length > 0;
}
export function hasPluginHelpAffectingConfig(config: OpenClawConfig | null | undefined): boolean {
const plugins = config?.plugins;
if (!plugins) {
return false;
}
return (
plugins.enabled === false ||
hasListEntries(plugins.allow) ||
hasListEntries(plugins.deny) ||
plugins.bundledDiscovery !== undefined ||
hasListEntries(plugins.load?.paths) ||
hasEntries(plugins.slots) ||
hasEntries(plugins.entries) ||
hasEntries(plugins.installs)
);
}
export function hasPluginHelpAffectingEnv(env: NodeJS.ProcessEnv): boolean {
return Boolean(
env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() || env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim(),
);
}
export async function loadRootHelpRenderOptionsForConfigSensitivePlugins(
env: NodeJS.ProcessEnv = process.env,
): Promise<RootHelpRenderOptions | null> {
const configModule = await import("../config/config.js");
const snapshot = await configModule.readConfigFileSnapshot({
observe: false,
skipPluginValidation: true,
});
if (!snapshot.valid) {
return null;
}
if (!hasPluginHelpAffectingEnv(env) && !hasPluginHelpAffectingConfig(snapshot.sourceConfig)) {
return null;
}
return {
config: snapshot.runtimeConfig,
env,
};
}
+34
View File
@@ -2,6 +2,7 @@ import process from "node:process";
import { CommanderError } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loggingState } from "../logging/state.js";
import type { RootHelpRenderOptions } from "./program/root-help.js";
import { runCli, shouldStartProxyForCli } from "./run-main.js";
const tryRouteCliMock = vi.hoisted(() => vi.fn());
@@ -18,6 +19,9 @@ const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
const outputRootHelpMock = vi.hoisted(() => vi.fn());
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const loadRootHelpRenderOptionsForConfigSensitivePluginsMock = vi.hoisted(() =>
vi.fn<() => Promise<RootHelpRenderOptions | null>>(async () => null),
);
const buildProgramMock = vi.hoisted(() => vi.fn());
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
@@ -168,6 +172,11 @@ vi.mock("./root-help-metadata.js", () => ({
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
}));
vi.mock("./root-help-live-config.js", () => ({
loadRootHelpRenderOptionsForConfigSensitivePlugins:
loadRootHelpRenderOptionsForConfigSensitivePluginsMock,
}));
vi.mock("./program.js", () => ({
buildProgram: buildProgramMock,
}));
@@ -242,6 +251,7 @@ describe("runCli exit behavior", () => {
listAgentHarnessIdsMock.mockReturnValue([]);
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValue(null);
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
loadConfigMock.mockReturnValue({});
startProxyMock.mockResolvedValue(null);
@@ -401,6 +411,7 @@ describe("runCli exit behavior", () => {
await runCli(["node", "openclaw", "--help"]);
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled();
expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled();
@@ -416,6 +427,7 @@ describe("runCli exit behavior", () => {
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "--help"]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
expect(buildProgramMock).not.toHaveBeenCalled();
@@ -424,6 +436,28 @@ describe("runCli exit behavior", () => {
exitSpy.mockRestore();
});
it("renders config-sensitive root help live instead of precomputed metadata", async () => {
const liveOptions: RootHelpRenderOptions = {
config: {
plugins: {
slots: {
memory: "memory-lancedb",
},
},
},
env: process.env,
};
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValueOnce(liveOptions);
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
await runCli(["node", "openclaw", "--help"]);
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
expect(outputPrecomputedRootHelpTextMock).not.toHaveBeenCalled();
expect(outputRootHelpMock).toHaveBeenCalledWith(liveOptions);
expect(buildProgramMock).not.toHaveBeenCalled();
});
it("does not start the managed proxy for local gateway client commands", async () => {
tryRouteCliMock.mockResolvedValueOnce(true);
+12 -4
View File
@@ -534,11 +534,19 @@ export async function runCli(argv: string[] = process.argv) {
try {
if (shouldUseRootHelpFastPath(normalizedArgv)) {
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
if (!outputPrecomputedRootHelpText()) {
const { outputRootHelp } = await import("./program/root-help.js");
await outputRootHelp();
const { loadRootHelpRenderOptionsForConfigSensitivePlugins } =
await import("./root-help-live-config.js");
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(
process.env,
);
if (!liveRootHelpOptions) {
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
if (outputPrecomputedRootHelpText()) {
return;
}
}
const { outputRootHelp } = await import("./program/root-help.js");
await outputRootHelp(liveRootHelpOptions ?? undefined);
return;
}