| import { describe, expect, it } from "vitest"; |
| import { formatCommand, parseCommand } from "#/utils/acp-command"; |
|
|
| describe("parseCommand", () => { |
| it("splits a simple npx invocation into argv tokens", () => { |
| expect( |
| parseCommand("npx -y @agentclientprotocol/claude-agent-acp"), |
| ).toEqual(["npx", "-y", "@agentclientprotocol/claude-agent-acp"]); |
| }); |
|
|
| it("respects double-quoted segments — the headline regression .split fix", () => { |
| |
| |
| |
| |
| expect(parseCommand('bash -c "echo hello world"')).toEqual([ |
| "bash", |
| "-c", |
| "echo hello world", |
| ]); |
| }); |
|
|
| it("respects single-quoted segments and embedded whitespace", () => { |
| expect(parseCommand("env FOO='bar baz' npx -y my-acp")).toEqual([ |
| "env", |
| "FOO=bar baz", |
| "npx", |
| "-y", |
| "my-acp", |
| ]); |
| }); |
|
|
| it("preserves URLs with query strings — the headline shell-quote-glob fix", () => { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| expect( |
| parseCommand("node acp.js --endpoint https://example.com/acp?tenant=abc"), |
| ).toEqual([ |
| "node", |
| "acp.js", |
| "--endpoint", |
| "https://example.com/acp?tenant=abc", |
| ]); |
| }); |
|
|
| it("preserves URLs with multiple query params", () => { |
| |
| expect(parseCommand("curl https://x.com?a=1&b=2")).toEqual([ |
| "curl", |
| "https://x.com?a=1&b=2", |
| ]); |
| }); |
|
|
| it("preserves shell metacharacters as literal argv tokens", () => { |
| |
| |
| |
| |
| |
| |
| |
| expect(parseCommand("foo | bar")).toEqual(["foo", "|", "bar"]); |
| expect(parseCommand("foo > log.txt")).toEqual(["foo", ">", "log.txt"]); |
| expect(parseCommand("foo *.txt")).toEqual(["foo", "*.txt"]); |
| expect(parseCommand("foo $X")).toEqual(["foo", "$X"]); |
| expect(parseCommand("foo `bar`")).toEqual(["foo", "`bar`"]); |
| expect(parseCommand("foo # comment")).toEqual(["foo", "#", "comment"]); |
| expect(parseCommand("foo && bar")).toEqual(["foo", "&&", "bar"]); |
| expect(parseCommand("foo; bar")).toEqual(["foo;", "bar"]); |
| }); |
|
|
| it("treats blank input as an empty argv", () => { |
| expect(parseCommand("")).toEqual([]); |
| expect(parseCommand(" \t\n ")).toEqual([]); |
| }); |
|
|
| it("honors backslash escapes outside quotes", () => { |
| |
| |
| |
| expect(parseCommand("foo\\ bar")).toEqual(["foo bar"]); |
| |
| expect(parseCommand('foo\\"bar')).toEqual(['foo"bar']); |
| }); |
|
|
| it('honors ``\\\\`` and ``\\"`` inside double-quoted segments', () => { |
| expect(parseCommand('bash -c "echo \\"hi\\""')).toEqual([ |
| "bash", |
| "-c", |
| 'echo "hi"', |
| ]); |
| expect(parseCommand('"foo\\\\bar"')).toEqual(["foo\\bar"]); |
| }); |
|
|
| it("does not env-expand $VAR refs — keeps them as literal", () => { |
| |
| |
| |
| |
| |
| |
| |
| const result = parseCommand("npx $ANTHROPIC_API_KEY"); |
| expect(result).toEqual(["npx", "$ANTHROPIC_API_KEY"]); |
| |
| |
| expect(result.some((t) => /sk-ant-/.test(t))).toBe(false); |
| }); |
|
|
| it("does not run subshells: $(…) and backticks become literal tokens", () => { |
| |
| |
| |
| expect(parseCommand("echo $(date)")).toEqual(["echo", "$(date)"]); |
| expect(parseCommand("echo `date`")).toEqual(["echo", "`date`"]); |
| }); |
|
|
| it("survives unterminated quotes without throwing", () => { |
| |
| |
| |
| |
| expect(parseCommand('bash -c "unterminated')).toEqual([ |
| "bash", |
| "-c", |
| "unterminated", |
| ]); |
| expect(parseCommand("foo 'unterminated single")).toEqual([ |
| "foo", |
| "unterminated single", |
| ]); |
| }); |
| }); |
|
|
| describe("formatCommand", () => { |
| it("renders package-style tokens verbatim, no escaping of @ or /", () => { |
| |
| |
| |
| |
| |
| |
| expect( |
| formatCommand(["npx", "-y", "@agentclientprotocol/claude-agent-acp"]), |
| ).toBe("npx -y @agentclientprotocol/claude-agent-acp"); |
| }); |
|
|
| it("shell-quotes tokens that contain whitespace", () => { |
| expect(formatCommand(["bash", "-c", "echo hello world"])).toBe( |
| "bash -c 'echo hello world'", |
| ); |
| }); |
|
|
| it("round-trips arbitrary argv arrays through parseCommand", () => { |
| const cases: string[][] = [ |
| ["npx", "-y", "@agentclientprotocol/claude-agent-acp"], |
| ["npx", "-y", "@zed-industries/codex-acp"], |
| ["npx", "-y", "@google/gemini-cli", "--acp"], |
| ["bash", "-c", "echo hello world"], |
| ["env", "FOO=bar baz", "npx", "-y", "my-acp"], |
| ["./bin/my-agent", "--flag=value"], |
| |
| ["node", "acp.js", "--endpoint", "https://example.com/acp?tenant=abc"], |
| |
| ["curl", "https://x.com?a=1&b=2"], |
| |
| |
| |
| |
| ["bash", "-c", ""], |
| ["program", "", "--flag"], |
| ]; |
| for (const argv of cases) { |
| expect(parseCommand(formatCommand(argv))).toEqual(argv); |
| } |
| }); |
|
|
| it("renders an empty argv as an empty string", () => { |
| expect(formatCommand([])).toBe(""); |
| }); |
|
|
| it("explicitly quotes empty-string tokens so they survive the round trip", () => { |
| |
| |
| |
| expect(formatCommand(["bash", "-c", ""])).toBe("bash -c ''"); |
| }); |
| }); |
|
|