fix: recognize Bash completion installed in login profiles (#113790)

* fix(cli): recognize Bash completion in login profiles

* test: track Bash completion profile fixtures

---------

Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
This commit is contained in:
Peter Steinberger
2026-07-25 11:08:59 -07:00
committed by GitHub
parent fca18dddc7
commit a1cc41acbc
4 changed files with 159 additions and 9 deletions
+90 -1
View File
@@ -1,18 +1,107 @@
// Completion runtime tests cover shell completion generation and runtime file writes.
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { withEnvAsync } from "../test-utils/env.js";
import {
formatCompletionReloadCommand,
installCompletion,
isCompletionInstalled,
resolveCompletionCachePath,
resolveCompletionProfilePath,
resolveShellFromEnv,
usesSlowDynamicCompletion,
} from "./completion-runtime.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
async function withBashCompletionHome(
run: (paths: { homeDir: string; stateDir: string }) => Promise<void>,
): Promise<void> {
const homeDir = tempDirs.make("openclaw-bash-completion-home-");
const stateDir = tempDirs.make("openclaw-bash-completion-state-");
await withEnvAsync({ HOME: homeDir, OPENCLAW_STATE_DIR: stateDir }, async () => {
await run({ homeDir, stateDir });
});
}
describe("completion-runtime", () => {
it("resolves the documented Bash login profile when .bashrc is absent", async () => {
await withBashCompletionHome(async ({ homeDir }) => {
expect(resolveCompletionProfilePath("bash")).toBe(path.join(homeDir, ".bash_profile"));
});
});
it("recognizes cached Bash completion installed into the login profile", async () => {
await withBashCompletionHome(async ({ homeDir }) => {
const cachePath = resolveCompletionCachePath("bash", "openclaw");
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await fs.writeFile(cachePath, "complete -W 'status' openclaw\n", "utf-8");
await installCompletion("bash", true, "openclaw");
const profilePath = path.join(homeDir, ".bash_profile");
await expect(fs.readFile(profilePath, "utf-8")).resolves.toContain(cachePath);
await expect(isCompletionInstalled("bash", "openclaw")).resolves.toBe(true);
await expect(usesSlowDynamicCompletion("bash", "openclaw")).resolves.toBe(false);
const shell = spawnSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
'source "$1"; complete -p openclaw',
"openclaw",
profilePath,
],
{ encoding: "utf8" },
);
expect(shell.stderr).toBe("");
expect(shell.status).toBe(0);
expect(shell.stdout).toContain("complete -W 'status' openclaw");
});
});
it("detects slow dynamic Bash completion in the login profile", async () => {
await withBashCompletionHome(async ({ homeDir }) => {
await fs.writeFile(
path.join(homeDir, ".bash_profile"),
"source <(openclaw completion --shell bash)\n",
"utf-8",
);
await expect(isCompletionInstalled("bash", "openclaw")).resolves.toBe(true);
await expect(usesSlowDynamicCompletion("bash", "openclaw")).resolves.toBe(true);
});
});
it("prefers an existing .bashrc over the Bash login profile", async () => {
await withBashCompletionHome(async ({ homeDir }) => {
const bashrc = path.join(homeDir, ".bashrc");
const bashProfile = path.join(homeDir, ".bash_profile");
const cachePath = resolveCompletionCachePath("bash", "openclaw");
await fs.writeFile(bashrc, "# existing interactive Bash profile\n", "utf-8");
await fs.writeFile(bashProfile, "# existing Bash login profile\n", "utf-8");
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await fs.writeFile(cachePath, "complete -W 'status' openclaw\n", "utf-8");
expect(resolveCompletionProfilePath("bash")).toBe(bashrc);
await installCompletion("bash", true, "openclaw");
await expect(fs.readFile(bashrc, "utf-8")).resolves.toContain(cachePath);
await expect(fs.readFile(bashProfile, "utf-8")).resolves.toBe(
"# existing Bash login profile\n",
);
await expect(isCompletionInstalled("bash", "openclaw")).resolves.toBe(true);
await expect(usesSlowDynamicCompletion("bash", "openclaw")).resolves.toBe(false);
});
});
it("formats PowerShell reload commands with single-quoted paths", () => {
expect(formatCompletionReloadCommand("powershell", "C:\\Users\\Ada\\profile.ps1")).toBe(
". 'C:\\Users\\Ada\\profile.ps1'",