1import { describe, expect, it } from "vite-plus/test";
2
3import { commandDisplayText, commandProgramName } from "./commandLabel.ts";
4
5describe("commandProgramName", () => {
6 it.each([
7 ["/bin/bash --rcfile /tmp/config -c 'git status'", "git"],
8 ["zsh -lc 'CI=1 env -u DEBUG sudo -u root vp test run'", "vp"],
9 ['"C:\\Program Files\\Git\\bin\\bash.exe" -lc "git status"', "git"],
10 // regression: #9371 a multiline script whose later line re-quotes must still label its first program
11 ["/bin/zsh -lc 'git status\nsed -n '\"'1,20p' apps/web/src/components/DiffPanel.tsx\"", "git"],
12 ])("unwraps shell scripts without executing them: %s", (command, program) => {
13 expect(commandProgramName(command)).toBe(program);
14 });
15
16 it.each([
17 ["sudo -u root pnpm test", "pnpm"],
18 ["zsh script.sh -c 'git status'", "zsh"],
19 ["my-shell -c 'git status'", "my-shell"],
20 ["$HOME/.bun/bin/bun test", "bun"],
21 ])("preserves ordinary programs and actual shell launches: %s", (command, program) => {
22 expect(commandProgramName(command)).toBe(program);
23 });
24
25 it.each(["zsh -lc", "zsh -lc 'git status", "zsh -lc 'env'"])(
26 "falls back for missing or malformed scripts: %s",
27 (command) => {
28 expect(commandProgramName(command)).toBeNull();
29 },
30 );
31
32 it.each([
33 "if test -f package.json; then vp test; fi",
34 "[[ -f package.json ]]",
35 "test -f package.json",
36 'for file in *; do echo "$file"; done',
37 "check() { vp test; }",
38 "{ vp test; }",
39 "(( count += 1 ))",
40 ". ./script.sh",
41 "export NODE_ENV=test",
42 "repeat 3 echo ok",
43 "return 1",
44 "true",
45 ])("falls back for shell syntax and internal control commands: %s", (command) => {
46 expect(commandProgramName(command)).toBeNull();
47 });
48
49 it.each([
50 ['rg -n "if|for|while" src', "rg"],
51 ["vp test && git status", "vp"],
52 ["echo `git status`", "echo"],
53 ["cat <(rg needle src)", "cat"],
54 ])("uses the first executable-looking program: %s", (command, program) => {
55 expect(commandProgramName(command)).toBe(program);
56 });
57
58 it.each([
59 ["cd packages/client-runtime && vp test run", "vp"],
60 ["PATH+=:/tools npm test", "npm"],
61 ["cd $(find . -type d | head -1) && git status", "git"],
62 ["cd /tmp && # use the selected workspace\nnpm test", "npm"],
63 ["cd ${ROOT:-path;with;semicolons} && bun test", "bun"],
64 ])("skips leading cd commands and uses the next useful program: %s", (command, program) => {
65 expect(commandProgramName(command)).toBe(program);
66 });
67
68 it.each([
69 ["source ~/.nvm/nvm.sh && nvm use", "nvm"],
70 ["unset DEBUG; node app.js", "node"],
71 ["/bin/zsh -lc 'source ~/.nvm/nvm.sh && nvm use'", "nvm"],
72 ])("skips shell setup commands and uses the next useful program: %s", (command, program) => {
73 expect(commandProgramName(command)).toBe(program);
74 });
75
76 it.each([
77 ["timeout 10s python3 script.py", "python3"],
78 ["arch -arch arm64 /opt/tools/check", "check"],
79 ["timeout 60 script -q /dev/null env CI=1 node app.js", "node"],
80 ])("unwraps process-launch wrappers: %s", (command, program) => {
81 expect(commandProgramName(command)).toBe(program);
82 });
83
84 it.each([
85 "command -v git",
86 "exec > output.log",
87 "env CI=1 cd /tmp && npm test",
88 "$PY scripts/check.py",
89 "& { Get-Process }",
90 "coproc npm test",
91 ])("does not treat shell lookup and commandless wrapper forms as executions: %s", (command) => {
92 expect(commandProgramName(command)).toBeNull();
93 });
94
95 it.each([
96 ["hash --help", "hash"],
97 ["Exec node app.js", "Exec"],
98 ["CD /tmp && npm test", "CD"],
99 ])("does not hide legitimate or case-distinct program names: %s", (command, program) => {
100 expect(commandProgramName(command)).toBe(program);
101 });
102
103 it("uses the command after leading shell comments", () => {
104 expect(commandProgramName("# first comment\n # second comment\ngit status")).toBe("git");
105 });
106
107 it.each([
108 [">/tmp/log && npm test", "npm"],
109 ["cd /tmp && 2>& 1 node app.js", "node"],
110 ["cd /tmp && {output}>/tmp/log deno test", "deno"],
111 ])("skips redirections before the next command: %s", (command, program) => {
112 expect(commandProgramName(command)).toBe(program);
113 });
114
115 it.each([
116 ["cd /tmp <<-EOF\n\tnot-a-command\n\tEOF\nbun test", "bun"],
117 ["cd /tmp <<A <<B\none\nA\ntwo\nB\ngit status", "git"],
118 ["cd /tmp <<EOF && pnpm test\nnot-a-command\nEOF\nbun test", "pnpm"],
119 ])("skips heredoc bodies before finding the next command: %s", (command, program) => {
120 expect(commandProgramName(command)).toBe(program);
121 });
122
123 it("bounds nested shell unwrapping", () => {
124 let command = "git status";
125 for (let depth = 0; depth < 9; depth += 1) {
126 command = `sh -c '${command.replaceAll("'", "'\\''")}'`;
127 }
128 expect(commandProgramName(command)).toBeNull();
129 });
130
131 it("bounds the number of top-level setup segments", () => {
132 const command = [
133 ...Array.from({ length: 2_000 }, (_, index) => `export VALUE_${index}=configured`),
134 "npm test",
135 ].join(";");
136
137 expect(commandProgramName(command)).toBeNull();
138 });
139});
140
141describe("commandDisplayText with redacted Codex commands", () => {
142 // Codex replaces secrets in command text before sending it
143 // (codex-rs/secrets/src/sanitizer.rs). Usually the quoting survives.
144 it("unwraps a redacted command whose quoting is intact", () => {
145 expect(
146 commandDisplayText(
147 `/bin/zsh -lc 'curl -H "Authorization: Bearer [REDACTED_SECRET]" https://example.com'`,
148 ),
149 ).toBe('curl -H "Authorization: Bearer [REDACTED_SECRET]" https://example.com');
150 });
151
152 // Its secret-assignment pattern can also swallow the backslash of an escaped
153 // quote: `token=config[\"apiToken\"]` becomes `token=[REDACTED_SECRET]"apiToken\"]`.
154 // The bare quote ends the -lc argument early, so where the script ends is unknown.
155 it("keeps the wrapper when redaction broke the quoting", () => {
156 const input = `/bin/zsh -lc "python3 -c 'token=[REDACTED_SECRET]"apiToken\\"]; print(token)'"`;
157 expect(commandDisplayText(input)).toBe(input);
158 });
159});
160
161describe("commandDisplayText control characters", () => {
162 it.each([
163 ["printf '\u001b[31mred\u001b[0m'", "printf '␛[31mred␛[0m'"],
164 ["zsh -lc 'printf \"\u001b[1m\"'", 'printf "␛[1m"'],
165 ["printf 'a\\tb'\r\necho done", "printf 'a\\tb'\r\necho done"],
166 ])("shows %j as %j", (input, expected) => {
167 expect(commandDisplayText(input)).toBe(expected);
168 });
169});