mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
[codex] Fix doctor completion cache plugin loading (#76235)
* fix(completion): make cache generation mode explicit * test(completion): type spawned cache options * fix(completion): require explicit cache binary name
This commit is contained in:
committed by
GitHub
parent
7613f6ebb3
commit
17c0ad86cb
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- **WhatsApp restart recovery:** stop automatic restart loops after logged-out or connection-replaced disconnects until the account reconnects. (#78511) Thanks @openperf.
|
||||
- **Local Gateway CLI auth:** keep loopback CLI token/password calls off durable device scopes so read probes cannot block later write/admin commands behind a stale pairing baseline. (#95997) Thanks @vincentkoc.
|
||||
- **Plugin module identity:** keep OpenClaw package chunks on Node's native module graph when jiti transforms plugin entries, preventing duplicate evaluation and class identity drift. (#88384) Thanks @vincentkoc.
|
||||
- **Shell completion repair:** generate core-only caches during doctor and update repair while preserving full plugin command completion for onboarding and explicit user rebuilds. (#76235)
|
||||
- **iMessage group warnings:** suppress the false drop-all startup warning when an effective group sender allowlist can admit groups, and point true empty-allowlist configurations at the correct remedy. (#100046)
|
||||
- **Control UI mobile login:** keep Gateway recovery guidance visible after connection failures, make the disconnected gate scroll safely on constrained screens, and improve mobile keyboard and tap-target behavior. (#100208)
|
||||
|
||||
|
||||
@@ -158,7 +158,10 @@ async function main() {
|
||||
// Profile uses slow dynamic pattern - upgrade to cached version
|
||||
if (status.usesSlowPattern) {
|
||||
console.log(theme.warn("Profile uses slow dynamic completion. Upgrading to cached version..."));
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
|
||||
shell: status.shell,
|
||||
generationMode: "full",
|
||||
});
|
||||
if (cacheGenerated) {
|
||||
await installCompletion(status.shell, false, CLI_NAME);
|
||||
console.log(theme.success("Upgraded to cached completion."));
|
||||
@@ -171,7 +174,10 @@ async function main() {
|
||||
// Profile has completion but no cache - auto-fix
|
||||
if (status.profileInstalled && !status.cacheExists) {
|
||||
console.log(theme.warn("Profile has completion but cache is missing. Regenerating..."));
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
|
||||
shell: status.shell,
|
||||
generationMode: "full",
|
||||
});
|
||||
if (cacheGenerated) {
|
||||
console.log(theme.success("Cache regenerated successfully."));
|
||||
} else {
|
||||
@@ -208,7 +214,10 @@ async function main() {
|
||||
// Generate cache first (required for fast shell startup)
|
||||
if (!status.cacheExists) {
|
||||
console.log(theme.muted("Generating completion cache..."));
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
|
||||
shell: status.shell,
|
||||
generationMode: "full",
|
||||
});
|
||||
if (!cacheGenerated) {
|
||||
console.log(theme.error("Failed to generate completion cache."));
|
||||
return;
|
||||
|
||||
@@ -1467,10 +1467,11 @@ async function tryInstallShellCompletion(opts: {
|
||||
}
|
||||
|
||||
const status = await checkShellCompletionStatus(CLI_NAME);
|
||||
const generationOptions = { generationMode: "core-only" } as const;
|
||||
|
||||
if (status.usesSlowPattern) {
|
||||
defaultRuntime.log(theme.muted("Upgrading shell completion to cached version..."));
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, generationOptions);
|
||||
if (cacheGenerated) {
|
||||
await installShellCompletionForUpdate(status.shell, true);
|
||||
}
|
||||
@@ -1479,7 +1480,7 @@ async function tryInstallShellCompletion(opts: {
|
||||
|
||||
if (status.profileInstalled && !status.cacheExists) {
|
||||
defaultRuntime.log(theme.muted("Regenerating shell completion cache..."));
|
||||
await ensureCompletionCacheExists(CLI_NAME);
|
||||
await ensureCompletionCacheExists(CLI_NAME, generationOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1503,7 +1504,7 @@ async function tryInstallShellCompletion(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
|
||||
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, generationOptions);
|
||||
if (!cacheGenerated) {
|
||||
defaultRuntime.log(theme.warn("Failed to generate completion cache."));
|
||||
return;
|
||||
|
||||
@@ -4,16 +4,23 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as noteModule from "../../packages/terminal-core/src/note.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../cli/completion-runtime.js";
|
||||
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
|
||||
import {
|
||||
checkShellCompletionStatus,
|
||||
doctorShellCompletion,
|
||||
ensureCompletionCacheExists,
|
||||
shellCompletionStatusToHealthFindings,
|
||||
shellCompletionStatusToRepairEffects,
|
||||
type ShellCompletionStatus,
|
||||
} from "./doctor-completion.js";
|
||||
|
||||
const originalEnv = captureEnv(["HOME", "OPENCLAW_STATE_DIR", "SHELL"]);
|
||||
const originalEnv = captureEnv([
|
||||
"HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"SHELL",
|
||||
COMPLETION_SKIP_PLUGIN_COMMANDS_ENV,
|
||||
]);
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -167,6 +174,36 @@ describe("doctorShellCompletion", () => {
|
||||
spawnSyncMock.mockClear();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ generationMode: "core-only" as const, expectedSkipValue: "1" },
|
||||
{ generationMode: "full" as const, expectedSkipValue: undefined },
|
||||
])(
|
||||
"uses explicit $generationMode cache generation even with an ambient skip guard",
|
||||
async ({ generationMode, expectedSkipValue }) => {
|
||||
const stateDir = tempDirs.make("openclaw-doctor-state-");
|
||||
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
|
||||
setTestEnvValue(COMPLETION_SKIP_PLUGIN_COMMANDS_ENV, "1");
|
||||
|
||||
await expect(
|
||||
ensureCompletionCacheExists("openclaw", {
|
||||
shell: "powershell",
|
||||
generationMode,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(spawnSyncMock).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
expect.arrayContaining(["completion", "--write-state", "--shell", "powershell"]),
|
||||
expect.any(Object),
|
||||
);
|
||||
const spawnCalls = spawnSyncMock.mock.calls as unknown as Array<
|
||||
[string, string[], { env?: NodeJS.ProcessEnv }]
|
||||
>;
|
||||
const spawnOptions = spawnCalls.at(-1)?.[2];
|
||||
expect(spawnOptions?.env?.[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV]).toBe(expectedSkipValue);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ code: "EACCES", usesSlowPattern: true, action: "upgraded" },
|
||||
{ code: "EPERM", usesSlowPattern: true, action: "upgraded" },
|
||||
|
||||
@@ -5,6 +5,7 @@ import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { resolveCliName } from "../cli/cli-name.js";
|
||||
import {
|
||||
completionCacheExists,
|
||||
COMPLETION_SKIP_PLUGIN_COMMANDS_ENV,
|
||||
formatCompletionReloadCommand,
|
||||
installCompletion,
|
||||
isCompletionInstalled,
|
||||
@@ -26,6 +27,10 @@ export type ShellCompletionStatusOptions = {
|
||||
shell?: CompletionShell;
|
||||
};
|
||||
|
||||
export type CompletionCacheGenerationOptions = ShellCompletionStatusOptions & {
|
||||
generationMode: "core-only" | "full";
|
||||
};
|
||||
|
||||
const PROFILE_WRITE_ERROR_CODES = new Set(["EACCES", "EPERM", "EROFS"]);
|
||||
|
||||
function findProfileWriteError(err: unknown): NodeJS.ErrnoException | undefined {
|
||||
@@ -74,7 +79,7 @@ async function installCompletionForDoctor(
|
||||
|
||||
/** Generate the completion cache by spawning the CLI. */
|
||||
async function generateCompletionCache(
|
||||
options: ShellCompletionStatusOptions = {},
|
||||
options: CompletionCacheGenerationOptions,
|
||||
): Promise<boolean> {
|
||||
const root = await resolveOpenClawPackageRoot({
|
||||
moduleUrl: import.meta.url,
|
||||
@@ -90,9 +95,16 @@ async function generateCompletionCache(
|
||||
if (options.shell) {
|
||||
args.push("--shell", options.shell);
|
||||
}
|
||||
const env = { ...process.env };
|
||||
// The mode is explicit so ambient repair state cannot silently change a full user-facing cache.
|
||||
if (options.generationMode === "core-only") {
|
||||
env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV] = "1";
|
||||
} else {
|
||||
delete env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV];
|
||||
}
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
env,
|
||||
encoding: "utf-8",
|
||||
timeout: COMPLETION_CACHE_WRITE_TIMEOUT_MS,
|
||||
});
|
||||
@@ -217,7 +229,7 @@ export async function doctorShellCompletion(
|
||||
);
|
||||
|
||||
if (!status.cacheExists) {
|
||||
const generated = await generateCompletionCache();
|
||||
const generated = await generateCompletionCache({ generationMode: "core-only" });
|
||||
if (!generated) {
|
||||
note(
|
||||
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
|
||||
@@ -236,7 +248,7 @@ export async function doctorShellCompletion(
|
||||
`Shell completion is configured in your ${status.shell} profile but the cache is missing.\nRegenerating cache...`,
|
||||
"Shell completion",
|
||||
);
|
||||
const generated = await generateCompletionCache();
|
||||
const generated = await generateCompletionCache({ generationMode: "core-only" });
|
||||
if (generated) {
|
||||
note(`Completion cache regenerated at ${status.cachePath}`, "Shell completion");
|
||||
} else {
|
||||
@@ -259,7 +271,7 @@ export async function doctorShellCompletion(
|
||||
});
|
||||
|
||||
if (shouldInstall) {
|
||||
const generated = await generateCompletionCache();
|
||||
const generated = await generateCompletionCache({ generationMode: "core-only" });
|
||||
if (!generated) {
|
||||
note(
|
||||
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
|
||||
@@ -275,8 +287,8 @@ export async function doctorShellCompletion(
|
||||
|
||||
/** Ensures the shell completion cache exists without prompting during setup/update flows. */
|
||||
export async function ensureCompletionCacheExists(
|
||||
binName = "openclaw",
|
||||
options: ShellCompletionStatusOptions = {},
|
||||
binName: string,
|
||||
options: CompletionCacheGenerationOptions,
|
||||
): Promise<boolean> {
|
||||
const shell = options.shell ?? resolveShellFromEnv();
|
||||
const cacheExists = await completionCacheExists(shell, binName);
|
||||
|
||||
@@ -49,7 +49,9 @@ describe("setupWizardShellCompletion", () => {
|
||||
await setupWizardShellCompletion({ flow: "quickstart", prompter, deps });
|
||||
|
||||
expect(prompter.confirm).not.toHaveBeenCalled();
|
||||
expect(deps.ensureCompletionCacheExists).toHaveBeenCalledWith("openclaw");
|
||||
expect(deps.ensureCompletionCacheExists).toHaveBeenCalledWith("openclaw", {
|
||||
generationMode: "full",
|
||||
});
|
||||
expect(deps.installCompletion).toHaveBeenCalledWith("zsh", true, "openclaw");
|
||||
expect(prompter.note).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
installCompletion,
|
||||
resolveCompletionProfilePath,
|
||||
} from "../cli/completion-runtime.js";
|
||||
import type { ShellCompletionStatus } from "../commands/doctor-completion.js";
|
||||
import type {
|
||||
CompletionCacheGenerationOptions,
|
||||
ShellCompletionStatus,
|
||||
} from "../commands/doctor-completion.js";
|
||||
import {
|
||||
checkShellCompletionStatus,
|
||||
ensureCompletionCacheExists,
|
||||
@@ -20,7 +23,10 @@ import type { WizardFlow } from "./setup.types.js";
|
||||
type CompletionDeps = {
|
||||
resolveCliName: () => string;
|
||||
checkShellCompletionStatus: (binName: string) => Promise<ShellCompletionStatus>;
|
||||
ensureCompletionCacheExists: (binName: string) => Promise<boolean>;
|
||||
ensureCompletionCacheExists: (
|
||||
binName: string,
|
||||
options: CompletionCacheGenerationOptions,
|
||||
) => Promise<boolean>;
|
||||
installCompletion: (shell: string, yes: boolean, binName?: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -63,10 +69,11 @@ export async function setupWizardShellCompletion(params: {
|
||||
|
||||
const cliName = deps.resolveCliName();
|
||||
const completionStatus = await deps.checkShellCompletionStatus(cliName);
|
||||
const generationOptions = { generationMode: "full" } as const;
|
||||
|
||||
if (completionStatus.usesSlowPattern) {
|
||||
// Case 1: Profile uses slow dynamic pattern - silently upgrade to cached version
|
||||
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
|
||||
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName, generationOptions);
|
||||
if (cacheGenerated) {
|
||||
await deps.installCompletion(completionStatus.shell, true, cliName);
|
||||
}
|
||||
@@ -75,7 +82,7 @@ export async function setupWizardShellCompletion(params: {
|
||||
|
||||
if (completionStatus.profileInstalled && !completionStatus.cacheExists) {
|
||||
// Case 2: Profile has completion but no cache - auto-fix silently
|
||||
await deps.ensureCompletionCacheExists(cliName);
|
||||
await deps.ensureCompletionCacheExists(cliName, generationOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ export async function setupWizardShellCompletion(params: {
|
||||
}
|
||||
|
||||
// Generate cache first (required for fast shell startup)
|
||||
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
|
||||
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName, generationOptions);
|
||||
if (!cacheGenerated) {
|
||||
await params.prompter.note(
|
||||
t("wizard.completion.cacheFailed", { command: `${cliName} completion --install` }),
|
||||
|
||||
Reference in New Issue
Block a user