mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: restore Fish completion after global CLI options (#115222)
* fix(cli): restore Fish completions after root options * test(ui): restore shared history after agent tool clicks * test(ui): wait for terminal availability before toggling * test(ui): wait for Settings takeover before asserting sidebar
This commit is contained in:
committed by
GitHub
parent
d44d0d96ad
commit
4d9588dfed
@@ -304,7 +304,7 @@ describe("completion-cli", () => {
|
||||
const script = getCompletionScript("fish", createCompletionProgram());
|
||||
|
||||
expect(script).toContain(
|
||||
'complete -c openclaw -n "__fish_use_subcommand" -a "gateway" -d \'Gateway commands\'',
|
||||
'complete -c openclaw -n "__openclaw_command_path_matches --" -a "gateway" -d \'Gateway commands\'',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'complete -c openclaw -n "__openclaw_command_path_matches gateway -- -t --token" -a "status" -d \'Show gateway status\'',
|
||||
@@ -326,6 +326,43 @@ describe("completion-cli", () => {
|
||||
expect(script).toContain("'gateway status'");
|
||||
});
|
||||
|
||||
itWithFish.each([
|
||||
["a separate long root option", "openclaw --profile work g"],
|
||||
["an inline long root option", "openclaw --profile=work g"],
|
||||
["a separate short root option", "openclaw -p work g"],
|
||||
["an inline short root option", "openclaw -p=work g"],
|
||||
["an attached short root option", "openclaw -pwork g"],
|
||||
["a separate log-level root option", "openclaw --log-level debug g"],
|
||||
["an inline log-level root option", "openclaw --log-level=debug g"],
|
||||
["a separate container root option", "openclaw --container local g"],
|
||||
["an inline container root option", "openclaw --container=local g"],
|
||||
["repeated root options", "openclaw --profile first --profile second g"],
|
||||
[
|
||||
"mixed value-taking root options",
|
||||
"openclaw --profile work --log-level debug --container local g",
|
||||
],
|
||||
["a preceding boolean root option", "openclaw -v --profile work g"],
|
||||
["a root option value named like a command", "openclaw --profile gateway g"],
|
||||
])("completes root commands in real Fish after %s", (_name, commandLine) => {
|
||||
const program = createCompletionProgram()
|
||||
.option("-p, --profile <name>", "Profile")
|
||||
.option("--log-level <level>", "Log level")
|
||||
.option("--container <name>", "Container");
|
||||
|
||||
expect(runGeneratedFishCompletion(program, commandLine)).toContain("gateway");
|
||||
});
|
||||
|
||||
itWithFish.each([
|
||||
["a separate long root option", "openclaw --profile work --p"],
|
||||
["an inline long root option", "openclaw --profile=work --p"],
|
||||
["a separate short root option", "openclaw -p work --p"],
|
||||
["repeated root options", "openclaw --profile first --profile second --p"],
|
||||
])("completes root options in real Fish after %s", (_name, commandLine) => {
|
||||
const program = createCompletionProgram().option("-p, --profile <name>", "Profile");
|
||||
|
||||
expect(runGeneratedFishCompletion(program, commandLine)).toContain("--profile");
|
||||
});
|
||||
|
||||
itWithFish.each([
|
||||
["the exact nested command", "openclaw gateway status -"],
|
||||
["a separate long option value", "openclaw gateway --token secret status -"],
|
||||
@@ -383,7 +420,7 @@ describe("completion-cli", () => {
|
||||
const fishScript = getCompletionScript("fish", program);
|
||||
|
||||
expect(fishScript).toContain(
|
||||
"complete -c openclaw -n \"__fish_use_subcommand\" -l trigger-script -d 'Condition script file, or - for stdin'",
|
||||
"complete -c openclaw -n \"__openclaw_command_path_matches -- --trigger-script --ws --workspace\" -l trigger-script -d 'Condition script file, or - for stdin'",
|
||||
);
|
||||
expect(fishScript).not.toContain(" -s > ");
|
||||
expect(fishScript).toContain(" -l ws -l workspace -d 'Workspace'");
|
||||
@@ -508,6 +545,18 @@ function createAliasedCompletionProgram(): Command {
|
||||
}
|
||||
|
||||
describe("completion-cli command aliases", () => {
|
||||
itWithFish.each([
|
||||
["a canonical root command", "openclaw --profile work inf", "infer"],
|
||||
["an aliased root command", "openclaw --profile work cap", "capability"],
|
||||
["an inline profile and alias", "openclaw --profile=work cap", "capability"],
|
||||
["an alias-shaped profile value", "openclaw --profile capability cap", "capability"],
|
||||
["a repeated profile and alias", "openclaw --profile first --profile second cap", "capability"],
|
||||
])("completes real Fish root aliases after %s", (_name, commandLine, expected) => {
|
||||
expect(runGeneratedFishCompletion(createAliasedCompletionProgram(), commandLine)).toContain(
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
it("completes root and nested aliases in zsh lists and dispatch", () => {
|
||||
const script = getCompletionScript("zsh", createAliasedCompletionProgram());
|
||||
|
||||
@@ -568,7 +617,7 @@ printf '%s\\n' "\${COMPREPLY[@]}"
|
||||
const script = getCompletionScript("fish", createAliasedCompletionProgram());
|
||||
|
||||
expect(script).toContain(
|
||||
'complete -c openclaw -n "__fish_use_subcommand" -a "capability" -d \'Run inference\'',
|
||||
'complete -c openclaw -n "__openclaw_command_path_matches -- --profile" -a "capability" -d \'Run inference\'',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'complete -c openclaw -n "__openclaw_command_path_matches capability -- --profile" -a "embed" -d \'Embed text\'',
|
||||
|
||||
@@ -147,9 +147,11 @@ function __${rootCmd}_command_path_matches
|
||||
end
|
||||
set -a command_tokens $token
|
||||
end
|
||||
for i in (seq (count $expected))
|
||||
if test "$command_tokens[$i]" != "$expected[$i]"
|
||||
return 1
|
||||
if test (count $expected) -gt 0
|
||||
for i in (seq (count $expected))
|
||||
if test "$command_tokens[$i]" != "$expected[$i]"
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
${rejectDescendantCommands}
|
||||
@@ -164,7 +166,8 @@ function fishCommandPathCondition(
|
||||
parents: readonly string[],
|
||||
): string {
|
||||
const valueOptions = collectFishPathOptionFlags(program, parents, true);
|
||||
return `__${rootCmd}_command_path_matches ${parents.join(" ")} -- ${fishWords(valueOptions)}`.trimEnd();
|
||||
const commandPath = parents.length > 0 ? ` ${parents.join(" ")}` : "";
|
||||
return `__${rootCmd}_command_path_matches${commandPath} -- ${fishWords(valueOptions)}`.trimEnd();
|
||||
}
|
||||
|
||||
async function writeCompletionCache(params: {
|
||||
@@ -626,9 +629,7 @@ function generateFishCompletion(program: Command): string {
|
||||
// One condition per alias-expanded parent path so completion keeps working
|
||||
// after the user typed an alias segment.
|
||||
const conditions = parentVariants.map((parents) =>
|
||||
parents.length === 0
|
||||
? "__fish_use_subcommand"
|
||||
: fishCommandPathCondition(program, rootCmd, parents),
|
||||
fishCommandPathCondition(program, rootCmd, parents),
|
||||
);
|
||||
for (const condition of conditions) {
|
||||
// Subcommands (canonical names and aliases)
|
||||
|
||||
@@ -341,6 +341,8 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await expect.poll(() => identityCard.isVisible()).toBe(true);
|
||||
await openSettingsFromIdentity();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/general");
|
||||
// Route changes paint Settings before the previous app sidebar finishes yielding.
|
||||
await sidebar.waitFor({ state: "hidden" });
|
||||
const { search: settingsSearch, sidebar: settingsSidebar } =
|
||||
await waitForSettingsSidebar(page);
|
||||
await expect.poll(() => sidebar.isVisible()).toBe(false);
|
||||
|
||||
@@ -75,6 +75,16 @@ describeControlUiE2e("Control UI terminal repaint", () => {
|
||||
try {
|
||||
await page.goto(server.baseUrl);
|
||||
await gateway.waitForRequest("connect");
|
||||
// The connect request is observable before its response upgrades the lazy terminal panel.
|
||||
// Wait for the advertised surface before exercising the real keyboard shortcut.
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
(
|
||||
document.querySelector("openclaw-terminal-panel") as
|
||||
| (HTMLElement & { available?: boolean })
|
||||
| null
|
||||
)?.available === true,
|
||||
);
|
||||
await page.keyboard.press("Control+Backquote");
|
||||
await gateway.waitForRequest("terminal.open");
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
// Control UI tests cover agents panels tools skills behavior.
|
||||
import { render } from "lit";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { SkillStatusEntry } from "../../api/types.ts";
|
||||
import { renderAgentSkills, renderAgentTools } from "./panels-tools-skills.ts";
|
||||
|
||||
let previousBrowserLocation: { state: unknown; url: string };
|
||||
|
||||
beforeEach(() => {
|
||||
previousBrowserLocation = {
|
||||
state: window.history.state,
|
||||
url: `${window.location.pathname}${window.location.search}${window.location.hash}`,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`);
|
||||
// Non-isolated UI tests share browser history; restore both its state and URL.
|
||||
window.history.replaceState(previousBrowserLocation.state, "", previousBrowserLocation.url);
|
||||
});
|
||||
|
||||
function createBaseParams(overrides: Partial<Parameters<typeof renderAgentTools>[0]> = {}) {
|
||||
|
||||
Reference in New Issue
Block a user