command_program/command_tool whitespace-split and only skipped whole wrapper words, so quoted paths broke, sudo -u/env -i/nohup/time/npx --yes/pnpm|yarn --silent dlx/bun x were rejected, and pnpm claude / yarn claude (a package.json script, not `dlx claude`) resolved as a Claude launch. Replace it with a tokenizer that honours quoting and escaping without evaluating any expansion, and per-wrapper option tables (env, sudo, command, nohup, time, npx, pnpm, yarn, bun) that require dlx/x before looking through a package-manager wrapper. Ambiguous shell grammar (pipelines, list operators, redirections, substitution, unterminated quotes) is rejected outright rather than guessed through; command_program still degrades to the naive first word for display, but command_tool returns None. Mirror the same resolver in web/src/toolIcon.ts and add fixtures/command-resolution.json as a parity fixture both test suites assert against.
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { splitCommand, toolIconSvg } from "./toolIcon.js";
|
|
import commandResolutionFixture from "../../fixtures/command-resolution.json";
|
|
|
|
interface CommandResolutionCase {
|
|
command: string;
|
|
program: string;
|
|
tool: string | null;
|
|
}
|
|
|
|
describe("tool icons on the command fragment (docs/SIDEBAR.md)", () => {
|
|
it("recognises the agents and shells sessions are spawned with", () => {
|
|
const cases: Array<[string, string]> = [
|
|
["claude", "claude"],
|
|
["codex", "codex"],
|
|
["opencode", "opencode"],
|
|
["gemini", "gemini"],
|
|
["aider", "aider"],
|
|
["cursor-agent", "cursor"],
|
|
["pi", "pi"],
|
|
["zsh", "shell"],
|
|
["bash", "shell"],
|
|
["fish", "shell"],
|
|
];
|
|
for (const [command, tool] of cases) {
|
|
expect(splitCommand(command).tool, command).toBe(tool);
|
|
}
|
|
});
|
|
|
|
it("drops everything after the program", () => {
|
|
expect(splitCommand("claude --dangerously-skip-permissions")).toEqual({
|
|
tool: "claude",
|
|
program: "claude",
|
|
});
|
|
expect(splitCommand(" zsh -l ")).toEqual({ tool: "shell", program: "zsh" });
|
|
});
|
|
|
|
it("looks through paths, env assignments and wrappers", () => {
|
|
expect(splitCommand("/usr/local/bin/claude").tool).toBe("claude");
|
|
expect(splitCommand("FOO=1 BAR=2 claude").tool).toBe("claude");
|
|
expect(splitCommand("npx opencode").tool).toBe("opencode");
|
|
expect(splitCommand("pnpm dlx opencode run").tool).toBe("opencode");
|
|
expect(splitCommand("pnpm dlx opencode run").program).toBe("opencode");
|
|
});
|
|
|
|
it("leaves an unrecognised program as its bare name, mark-less", () => {
|
|
expect(splitCommand("hx src/main.rs")).toEqual({
|
|
tool: null,
|
|
program: "hx",
|
|
});
|
|
// A wrapper with nothing after it is not a tool either.
|
|
expect(splitCommand("sudo").tool).toBeNull();
|
|
expect(splitCommand("").tool).toBeNull();
|
|
});
|
|
|
|
it("resolves every case in the shared command-resolution fixture (#199)", () => {
|
|
const cases = commandResolutionFixture.cases as CommandResolutionCase[];
|
|
expect(cases.length).toBeGreaterThan(0);
|
|
for (const { command, program, tool } of cases) {
|
|
expect(splitCommand(command), command).toEqual({ tool, program });
|
|
}
|
|
});
|
|
|
|
it("gives every recognised tool a labelled mark", () => {
|
|
for (const tool of ["claude", "codex", "opencode", "gemini", "aider", "cursor", "pi", "shell"]) {
|
|
const svg = toolIconSvg(tool);
|
|
expect(svg, tool).toContain("<svg");
|
|
expect(svg, tool).toContain(`data-tool="${tool}"`);
|
|
expect(svg, tool).toContain("aria-label=");
|
|
}
|
|
expect(toolIconSvg("nonesuch")).toBeNull();
|
|
});
|
|
});
|