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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 434 additions and 24 deletions

View file

@ -137,6 +137,7 @@ Docs: https://docs.openclaw.ai
- Mac app: render channel quick config as aligned Settings rows and hide schema-only variants that cannot be edited safely from the quick pane.
- Gateway/webchat: hide internal runtime-context and other `display: false` transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.
- CLI/help: keep `gateway`, `doctor`, `status`, and `health` help registration out of action/runtime imports so subcommand `--help` stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.
- CLI/help: show plugin-owned command help based on the active memory slot so LanceDB memory users see `ltm` instead of unavailable `memory` commands. Fixes #83745. (#83841) Thanks @joshavant.
- Cron/Discord: keep explicit announce runs in message-tool-only source-reply mode so scheduled agent turns post once instead of also echoing through automatic visible replies. Fixes #83261. Thanks @Theralley.
- Telegram: preserve forum-topic origin targets in inbound, audio-preflight, and skipped-message hook contexts so follow-up delivery stays bound to the originating topic. Fixes #83302. Thanks @M00zyx.
- Telegram: retry HTTP 421 Misdirected Request send failures on a fresh fallback transport so transient edge-node routing errors no longer drop outbound replies. Fixes #48892. (#48908) Thanks @MarsDoge.

View file

@ -10,7 +10,9 @@ title: "Memory"
# `openclaw memory`
Manage semantic memory indexing and search.
Provided by the active memory plugin (default: `memory-core`; set `plugins.slots.memory = "none"` to disable).
Provided by the bundled `memory-core` plugin. The command is available when
`plugins.slots.memory` selects `memory-core` (the default); other memory plugins
expose their own CLI namespaces.
Related:

View file

@ -238,12 +238,12 @@ openclaw ltm search "project preferences"
openclaw ltm stats
```
The plugin also extends `openclaw memory` with a non-vector `query` subcommand
that runs against the LanceDB table directly:
The `query` subcommand runs a non-vector query against the LanceDB table
directly:
```bash
openclaw memory query --cols id,text,createdAt --limit 20
openclaw memory query --filter "category = 'preference'" --order-by createdAt:desc
openclaw ltm query --cols id,text,createdAt --limit 20
openclaw ltm query --filter "category = 'preference'" --order-by createdAt:desc
```
- `--cols <columns>`: comma-separated column allowlist (defaults to `id`, `text`, `importance`, `category`, `createdAt`).

View file

@ -5,6 +5,14 @@ export default definePluginEntry({
name: "Memory LanceDB",
description: "LanceDB-backed memory provider",
register(api) {
api.registerCli(() => {}, { commands: ["ltm"] });
api.registerCli(() => {}, {
descriptors: [
{
name: "ltm",
description: "Inspect and query LanceDB-backed memory",
hasSubcommands: true,
},
],
});
},
});

View file

@ -334,6 +334,72 @@ const isBrowserHelpInvocation = (argv) =>
const isHelpFastPathDisabled = () =>
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
const normalizeLauncherHomeValue = (value) => {
const trimmed = value?.trim();
return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
};
const resolveLauncherOsHomeDir = () =>
normalizeLauncherHomeValue(process.env.HOME) ??
normalizeLauncherHomeValue(process.env.USERPROFILE) ??
os.homedir();
const resolveLauncherHomeDir = () => {
const explicit = normalizeLauncherHomeValue(process.env.OPENCLAW_HOME);
const rawHome =
explicit && (explicit === "~" || explicit.startsWith("~/") || explicit.startsWith("~\\"))
? explicit.replace(/^~(?=$|[\\/])/, resolveLauncherOsHomeDir())
: (explicit ?? resolveLauncherOsHomeDir());
return path.resolve(rawHome);
};
const resolveLauncherUserPath = (input) => {
if (input === "~") {
return resolveLauncherHomeDir();
}
if (input.startsWith("~/") || input.startsWith("~\\")) {
return path.join(resolveLauncherHomeDir(), input.slice(2));
}
return path.resolve(input);
};
const resolveLauncherConfigPaths = () => {
const explicit = process.env.OPENCLAW_CONFIG_PATH?.trim();
if (explicit) {
return [resolveLauncherUserPath(explicit)];
}
const stateOverride = process.env.OPENCLAW_STATE_DIR?.trim();
if (stateOverride) {
const stateDir = resolveLauncherUserPath(stateOverride);
return [path.join(stateDir, "openclaw.json"), path.join(stateDir, "clawdbot.json")];
}
const homeDir = resolveLauncherHomeDir();
return [
path.join(homeDir, ".openclaw", "openclaw.json"),
path.join(homeDir, ".openclaw", "clawdbot.json"),
path.join(homeDir, ".clawdbot", "openclaw.json"),
path.join(homeDir, ".clawdbot", "clawdbot.json"),
];
};
const shouldDeferRootHelpToRuntimeEntry = () => {
if (
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim() ||
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS?.trim()
) {
return true;
}
for (const configPath of resolveLauncherConfigPaths()) {
try {
const raw = readFileSync(configPath, "utf8");
return /\bplugins\b|\$include\b/.test(raw);
} catch {
continue;
}
}
return false;
};
const loadPrecomputedHelpText = (key) => {
try {
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
@ -349,6 +415,9 @@ const tryOutputBareRootHelp = async () => {
if (!isBareRootHelpInvocation(process.argv)) {
return false;
}
if (shouldDeferRootHelpToRuntimeEntry()) {
return false;
}
const precomputed = loadPrecomputedHelpText("rootHelpText");
if (precomputed) {
process.stdout.write(precomputed);
@ -358,7 +427,7 @@ const tryOutputBareRootHelp = async () => {
try {
const mod = await import(specifier);
if (typeof mod.outputRootHelp === "function") {
mod.outputRootHelp();
await mod.outputRootHelp();
return true;
}
} catch (err) {

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"],

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;

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,
});
});
});

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,
};
}

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);

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;
}

View file

@ -11,6 +11,7 @@ describe("entry root help fast path", () => {
outputPrecomputedRootHelpTextCalls += 1;
return true;
},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
});
expect(handled).toBe(true);
@ -24,6 +25,7 @@ describe("entry root help fast path", () => {
outputRootHelp: () => {
outputRootHelpCalls += 1;
},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
env: {},
});
@ -31,6 +33,37 @@ describe("entry root help fast path", () => {
expect(outputRootHelpCalls).toBe(1);
});
it("renders live root help when plugin config changes command descriptors", async () => {
let outputPrecomputedRootHelpTextCalls = 0;
const outputRootHelpOptions: unknown[] = [];
const liveOptions = {
config: {
plugins: {
slots: {
memory: "memory-lancedb",
},
},
},
env: {},
};
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
env: {},
outputPrecomputedRootHelpText: () => {
outputPrecomputedRootHelpTextCalls += 1;
return true;
},
outputRootHelp: (options) => {
outputRootHelpOptions.push(options);
},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => liveOptions,
});
expect(handled).toBe(true);
expect(outputPrecomputedRootHelpTextCalls).toBe(0);
expect(outputRootHelpOptions).toEqual([liveOptions]);
});
it("ignores non-root help invocations", async () => {
let outputRootHelpCalls = 0;
@ -38,6 +71,7 @@ describe("entry root help fast path", () => {
outputRootHelp: () => {
outputRootHelpCalls += 1;
},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
env: {},
});
@ -54,6 +88,7 @@ describe("entry root help fast path", () => {
outputRootHelp: () => {
outputRootHelpCalls += 1;
},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
env: {},
},
);

View file

@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import { isRootHelpInvocation } from "./cli/argv.js";
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import type { RootHelpRenderOptions } from "./cli/program/root-help.js";
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
import {
enableOpenClawCompileCache,
@ -157,7 +158,10 @@ export async function tryHandleRootHelpFastPath(
argv: string[],
deps: {
outputPrecomputedRootHelpText?: () => boolean;
outputRootHelp?: () => void | Promise<void>;
outputRootHelp?: (options?: RootHelpRenderOptions) => void | Promise<void>;
loadRootHelpRenderOptionsForConfigSensitivePlugins?: (
env?: NodeJS.ProcessEnv,
) => Promise<RootHelpRenderOptions | null>;
onError?: (error: unknown) => void;
env?: NodeJS.ProcessEnv;
} = {},
@ -178,17 +182,22 @@ export async function tryHandleRootHelpFastPath(
process.exitCode = 1;
});
try {
if (deps.outputRootHelp) {
await deps.outputRootHelp();
return true;
}
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();
const loadRootHelpRenderOptionsForConfigSensitivePlugins =
deps.loadRootHelpRenderOptionsForConfigSensitivePlugins ??
(await import("./cli/root-help-live-config.js"))
.loadRootHelpRenderOptionsForConfigSensitivePlugins;
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(deps.env);
if (!liveRootHelpOptions) {
const outputPrecomputedRootHelpText =
deps.outputPrecomputedRootHelpText ??
(await import("./cli/root-help-metadata.js")).outputPrecomputedRootHelpText;
if (outputPrecomputedRootHelpText()) {
return true;
}
}
const outputRootHelp =
deps.outputRootHelp ?? (await import("./cli/program/root-help.js")).outputRootHelp;
await outputRootHelp(liveRootHelpOptions ?? undefined);
return true;
} catch (error) {
handleError(error);

View file

@ -203,6 +203,144 @@ describe("openclaw launcher", () => {
expect(result.stderr).toContain("missing dist/entry.(m)js");
});
it("uses precomputed root help when plugin config does not invalidate it", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ rootHelpText: "PRECOMPUTED help\n" }),
"utf8",
);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("PRECOMPUTED help\n");
});
it("defers root help to the runtime entry when plugin config can change help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const configPath = path.join(fixtureRoot, "openclaw.json");
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
"utf8",
);
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
"process.stdout.write('RUNTIME ENTRY\\n');\n",
"utf8",
);
await fs.writeFile(
configPath,
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
"utf8",
);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv({ OPENCLAW_CONFIG_PATH: configPath }),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("RUNTIME ENTRY\n");
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("checks the OPENCLAW_HOME default config path before using precomputed root help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const openclawHome = path.join(fixtureRoot, "home");
const configDir = path.join(openclawHome, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
"utf8",
);
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
"process.stdout.write('RUNTIME ENTRY\\n');\n",
"utf8",
);
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
"utf8",
);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv({ OPENCLAW_HOME: openclawHome }),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("RUNTIME ENTRY\n");
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("checks legacy config candidates before using precomputed root help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const home = path.join(fixtureRoot, "home");
const legacyConfigDir = path.join(home, ".clawdbot");
await fs.mkdir(legacyConfigDir, { recursive: true });
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
"utf8",
);
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
"process.stdout.write('RUNTIME ENTRY\\n');\n",
"utf8",
);
await fs.writeFile(
path.join(legacyConfigDir, "clawdbot.json"),
JSON.stringify({ plugins: { slots: { memory: "memory-lancedb" } } }),
"utf8",
);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv({ HOME: home, OPENCLAW_HOME: undefined }),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("RUNTIME ENTRY\n");
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("defers root help when the active config has includes", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const configPath = path.join(fixtureRoot, "openclaw.json");
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ rootHelpText: "PRECOMPUTED memory help\n" }),
"utf8",
);
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
"process.stdout.write('RUNTIME ENTRY\\n');\n",
"utf8",
);
await fs.writeFile(configPath, JSON.stringify({ $include: "memory.json" }), "utf8");
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv({ OPENCLAW_CONFIG_PATH: configPath }),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("RUNTIME ENTRY\n");
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("explains how to recover from an unbuilt source install", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addSourceTreeMarker(fixtureRoot);