Skip to content

Commit 17c0ad8

Browse files
authored
[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
1 parent 7613f6e commit 17c0ad8

7 files changed

Lines changed: 89 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515
- **WhatsApp restart recovery:** stop automatic restart loops after logged-out or connection-replaced disconnects until the account reconnects. (#78511) Thanks @openperf.
1616
- **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.
1717
- **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.
18+
- **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)
1819
- **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)
1920
- **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)
2021

scripts/test-shell-completion.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ async function main() {
158158
// Profile uses slow dynamic pattern - upgrade to cached version
159159
if (status.usesSlowPattern) {
160160
console.log(theme.warn("Profile uses slow dynamic completion. Upgrading to cached version..."));
161-
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
161+
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
162+
shell: status.shell,
163+
generationMode: "full",
164+
});
162165
if (cacheGenerated) {
163166
await installCompletion(status.shell, false, CLI_NAME);
164167
console.log(theme.success("Upgraded to cached completion."));
@@ -171,7 +174,10 @@ async function main() {
171174
// Profile has completion but no cache - auto-fix
172175
if (status.profileInstalled && !status.cacheExists) {
173176
console.log(theme.warn("Profile has completion but cache is missing. Regenerating..."));
174-
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
177+
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
178+
shell: status.shell,
179+
generationMode: "full",
180+
});
175181
if (cacheGenerated) {
176182
console.log(theme.success("Cache regenerated successfully."));
177183
} else {
@@ -208,7 +214,10 @@ async function main() {
208214
// Generate cache first (required for fast shell startup)
209215
if (!status.cacheExists) {
210216
console.log(theme.muted("Generating completion cache..."));
211-
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, { shell: status.shell });
217+
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, {
218+
shell: status.shell,
219+
generationMode: "full",
220+
});
212221
if (!cacheGenerated) {
213222
console.log(theme.error("Failed to generate completion cache."));
214223
return;

src/cli/update-cli/update-command.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,10 +1467,11 @@ async function tryInstallShellCompletion(opts: {
14671467
}
14681468

14691469
const status = await checkShellCompletionStatus(CLI_NAME);
1470+
const generationOptions = { generationMode: "core-only" } as const;
14701471

14711472
if (status.usesSlowPattern) {
14721473
defaultRuntime.log(theme.muted("Upgrading shell completion to cached version..."));
1473-
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
1474+
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, generationOptions);
14741475
if (cacheGenerated) {
14751476
await installShellCompletionForUpdate(status.shell, true);
14761477
}
@@ -1479,7 +1480,7 @@ async function tryInstallShellCompletion(opts: {
14791480

14801481
if (status.profileInstalled && !status.cacheExists) {
14811482
defaultRuntime.log(theme.muted("Regenerating shell completion cache..."));
1482-
await ensureCompletionCacheExists(CLI_NAME);
1483+
await ensureCompletionCacheExists(CLI_NAME, generationOptions);
14831484
return;
14841485
}
14851486

@@ -1503,7 +1504,7 @@ async function tryInstallShellCompletion(opts: {
15031504
return;
15041505
}
15051506

1506-
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
1507+
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME, generationOptions);
15071508
if (!cacheGenerated) {
15081509
defaultRuntime.log(theme.warn("Failed to generate completion cache."));
15091510
return;

src/commands/doctor-completion.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@ import path from "node:path";
44
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55
import * as noteModule from "../../packages/terminal-core/src/note.js";
66
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
7+
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../cli/completion-runtime.js";
78
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
89
import {
910
checkShellCompletionStatus,
1011
doctorShellCompletion,
12+
ensureCompletionCacheExists,
1113
shellCompletionStatusToHealthFindings,
1214
shellCompletionStatusToRepairEffects,
1315
type ShellCompletionStatus,
1416
} from "./doctor-completion.js";
1517

16-
const originalEnv = captureEnv(["HOME", "OPENCLAW_STATE_DIR", "SHELL"]);
18+
const originalEnv = captureEnv([
19+
"HOME",
20+
"OPENCLAW_STATE_DIR",
21+
"SHELL",
22+
COMPLETION_SKIP_PLUGIN_COMMANDS_ENV,
23+
]);
1724
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
1825

1926
afterEach(async () => {
@@ -167,6 +174,36 @@ describe("doctorShellCompletion", () => {
167174
spawnSyncMock.mockClear();
168175
});
169176

177+
it.each([
178+
{ generationMode: "core-only" as const, expectedSkipValue: "1" },
179+
{ generationMode: "full" as const, expectedSkipValue: undefined },
180+
])(
181+
"uses explicit $generationMode cache generation even with an ambient skip guard",
182+
async ({ generationMode, expectedSkipValue }) => {
183+
const stateDir = tempDirs.make("openclaw-doctor-state-");
184+
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
185+
setTestEnvValue(COMPLETION_SKIP_PLUGIN_COMMANDS_ENV, "1");
186+
187+
await expect(
188+
ensureCompletionCacheExists("openclaw", {
189+
shell: "powershell",
190+
generationMode,
191+
}),
192+
).resolves.toBe(true);
193+
194+
expect(spawnSyncMock).toHaveBeenCalledWith(
195+
process.execPath,
196+
expect.arrayContaining(["completion", "--write-state", "--shell", "powershell"]),
197+
expect.any(Object),
198+
);
199+
const spawnCalls = spawnSyncMock.mock.calls as unknown as Array<
200+
[string, string[], { env?: NodeJS.ProcessEnv }]
201+
>;
202+
const spawnOptions = spawnCalls.at(-1)?.[2];
203+
expect(spawnOptions?.env?.[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV]).toBe(expectedSkipValue);
204+
},
205+
);
206+
170207
it.each([
171208
{ code: "EACCES", usesSlowPattern: true, action: "upgraded" },
172209
{ code: "EPERM", usesSlowPattern: true, action: "upgraded" },

src/commands/doctor-completion.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { note } from "../../packages/terminal-core/src/note.js";
55
import { resolveCliName } from "../cli/cli-name.js";
66
import {
77
completionCacheExists,
8+
COMPLETION_SKIP_PLUGIN_COMMANDS_ENV,
89
formatCompletionReloadCommand,
910
installCompletion,
1011
isCompletionInstalled,
@@ -26,6 +27,10 @@ export type ShellCompletionStatusOptions = {
2627
shell?: CompletionShell;
2728
};
2829

30+
export type CompletionCacheGenerationOptions = ShellCompletionStatusOptions & {
31+
generationMode: "core-only" | "full";
32+
};
33+
2934
const PROFILE_WRITE_ERROR_CODES = new Set(["EACCES", "EPERM", "EROFS"]);
3035

3136
function findProfileWriteError(err: unknown): NodeJS.ErrnoException | undefined {
@@ -74,7 +79,7 @@ async function installCompletionForDoctor(
7479

7580
/** Generate the completion cache by spawning the CLI. */
7681
async function generateCompletionCache(
77-
options: ShellCompletionStatusOptions = {},
82+
options: CompletionCacheGenerationOptions,
7883
): Promise<boolean> {
7984
const root = await resolveOpenClawPackageRoot({
8085
moduleUrl: import.meta.url,
@@ -90,9 +95,16 @@ async function generateCompletionCache(
9095
if (options.shell) {
9196
args.push("--shell", options.shell);
9297
}
98+
const env = { ...process.env };
99+
// The mode is explicit so ambient repair state cannot silently change a full user-facing cache.
100+
if (options.generationMode === "core-only") {
101+
env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV] = "1";
102+
} else {
103+
delete env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV];
104+
}
93105
const result = spawnSync(process.execPath, args, {
94106
cwd: root,
95-
env: process.env,
107+
env,
96108
encoding: "utf-8",
97109
timeout: COMPLETION_CACHE_WRITE_TIMEOUT_MS,
98110
});
@@ -217,7 +229,7 @@ export async function doctorShellCompletion(
217229
);
218230

219231
if (!status.cacheExists) {
220-
const generated = await generateCompletionCache();
232+
const generated = await generateCompletionCache({ generationMode: "core-only" });
221233
if (!generated) {
222234
note(
223235
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
@@ -236,7 +248,7 @@ export async function doctorShellCompletion(
236248
`Shell completion is configured in your ${status.shell} profile but the cache is missing.\nRegenerating cache...`,
237249
"Shell completion",
238250
);
239-
const generated = await generateCompletionCache();
251+
const generated = await generateCompletionCache({ generationMode: "core-only" });
240252
if (generated) {
241253
note(`Completion cache regenerated at ${status.cachePath}`, "Shell completion");
242254
} else {
@@ -259,7 +271,7 @@ export async function doctorShellCompletion(
259271
});
260272

261273
if (shouldInstall) {
262-
const generated = await generateCompletionCache();
274+
const generated = await generateCompletionCache({ generationMode: "core-only" });
263275
if (!generated) {
264276
note(
265277
`Failed to generate completion cache. Run \`${cliName} completion --write-state\` manually.`,
@@ -275,8 +287,8 @@ export async function doctorShellCompletion(
275287

276288
/** Ensures the shell completion cache exists without prompting during setup/update flows. */
277289
export async function ensureCompletionCacheExists(
278-
binName = "openclaw",
279-
options: ShellCompletionStatusOptions = {},
290+
binName: string,
291+
options: CompletionCacheGenerationOptions,
280292
): Promise<boolean> {
281293
const shell = options.shell ?? resolveShellFromEnv();
282294
const cacheExists = await completionCacheExists(shell, binName);

src/wizard/setup.completion.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ describe("setupWizardShellCompletion", () => {
4949
await setupWizardShellCompletion({ flow: "quickstart", prompter, deps });
5050

5151
expect(prompter.confirm).not.toHaveBeenCalled();
52-
expect(deps.ensureCompletionCacheExists).toHaveBeenCalledWith("openclaw");
52+
expect(deps.ensureCompletionCacheExists).toHaveBeenCalledWith("openclaw", {
53+
generationMode: "full",
54+
});
5355
expect(deps.installCompletion).toHaveBeenCalledWith("zsh", true, "openclaw");
5456
expect(prompter.note).toHaveBeenCalled();
5557
});

src/wizard/setup.completion.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
installCompletion,
88
resolveCompletionProfilePath,
99
} from "../cli/completion-runtime.js";
10-
import type { ShellCompletionStatus } from "../commands/doctor-completion.js";
10+
import type {
11+
CompletionCacheGenerationOptions,
12+
ShellCompletionStatus,
13+
} from "../commands/doctor-completion.js";
1114
import {
1215
checkShellCompletionStatus,
1316
ensureCompletionCacheExists,
@@ -20,7 +23,10 @@ import type { WizardFlow } from "./setup.types.js";
2023
type CompletionDeps = {
2124
resolveCliName: () => string;
2225
checkShellCompletionStatus: (binName: string) => Promise<ShellCompletionStatus>;
23-
ensureCompletionCacheExists: (binName: string) => Promise<boolean>;
26+
ensureCompletionCacheExists: (
27+
binName: string,
28+
options: CompletionCacheGenerationOptions,
29+
) => Promise<boolean>;
2430
installCompletion: (shell: string, yes: boolean, binName?: string) => Promise<void>;
2531
};
2632

@@ -63,10 +69,11 @@ export async function setupWizardShellCompletion(params: {
6369

6470
const cliName = deps.resolveCliName();
6571
const completionStatus = await deps.checkShellCompletionStatus(cliName);
72+
const generationOptions = { generationMode: "full" } as const;
6673

6774
if (completionStatus.usesSlowPattern) {
6875
// Case 1: Profile uses slow dynamic pattern - silently upgrade to cached version
69-
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
76+
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName, generationOptions);
7077
if (cacheGenerated) {
7178
await deps.installCompletion(completionStatus.shell, true, cliName);
7279
}
@@ -75,7 +82,7 @@ export async function setupWizardShellCompletion(params: {
7582

7683
if (completionStatus.profileInstalled && !completionStatus.cacheExists) {
7784
// Case 2: Profile has completion but no cache - auto-fix silently
78-
await deps.ensureCompletionCacheExists(cliName);
85+
await deps.ensureCompletionCacheExists(cliName, generationOptions);
7986
return;
8087
}
8188

@@ -97,7 +104,7 @@ export async function setupWizardShellCompletion(params: {
97104
}
98105

99106
// Generate cache first (required for fast shell startup)
100-
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName);
107+
const cacheGenerated = await deps.ensureCompletionCacheExists(cliName, generationOptions);
101108
if (!cacheGenerated) {
102109
await params.prompter.note(
103110
t("wizard.completion.cacheFailed", { command: `${cliName} completion --install` }),

0 commit comments

Comments
 (0)