1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 commandDisplayText,
5 commandHighlightLanguage,
6 commandProgramName,
7} from "./commandLabel.ts";
8
9describe("commandProgramName", () => {
10 it.each([
11 ["/bin/zsh -lc 'vp test run apps/web/src/session-logic.test.ts'", "vp"],
12 ["/bin/zsh -lc 'git diff --check'", "git"],
13 ['/bin/zsh -lc "npx -y react-doctor@latest apps/web"', "npx"],
14 ["/bin/zsh -lc 'rg -n \"registerHooks|worker\" apps/web/src'", "rg"],
15 ["/bin/bash --noprofile --norc -l -c 'sed -n 1,270p file.ts'", "sed"],
16 ["/bin/bash -o pipefail -lc 'vp test run'", "vp"],
17 ["/bin/bash --rcfile /tmp/config -c 'git status'", "git"],
18 ["sh -ec 'node scripts/check.js'", "node"],
19 ["fish --command 'rg --files'", "rg"],
20 ["zsh -lc 'CI=1 env -u DEBUG sudo -u root vp test run'", "vp"],
21 ["env CI=1 /bin/zsh -lc '\"/Applications/My Tools/bin/check\" --verbose'", "check"],
22 ["bash -lc \"zsh -c 'git status'\"", "git"],
23 ['"C:\\Program Files\\Git\\bin\\bash.exe" -lc "git status"', "git"],
24 ["/bin/zsh -lc 'git status\nsed -n '\"'1,20p' apps/web/src/components/DiffPanel.tsx\"", "git"],
25 ])("unwraps shell scripts without executing them: %s", (command, program) => {
26 expect(commandProgramName(command)).toBe(program);
27 });
28
29 it.each([
30 ["vp test run", "vp"],
31 ["sudo -u root pnpm test", "pnpm"],
32 ["env --split-string='CI=1 node scripts/check.js'", "node"],
33 ['"C:\\Program Files\\nodejs\\node.exe" script.js', "node.exe"],
34 ["/bin/zsh", "zsh"],
35 ["/bin/bash -l", "bash"],
36 ["zsh script.sh -c 'git status'", "zsh"],
37 ["bash -- -c 'git status'", "bash"],
38 ["bash --rcfile config.sh", "bash"],
39 ["my-shell -c 'git status'", "my-shell"],
40 ["$HOME/.bun/bin/bun test", "bun"],
41 ['"$ANDROID_HOME/emulator/emulator" -list-avds', "emulator"],
42 ["${ROOT}/bin/tool --version", "tool"],
43 ])("preserves ordinary programs and actual shell launches: %s", (command, program) => {
44 expect(commandProgramName(command)).toBe(program);
45 });
46
47 it.each([
48 "",
49 "zsh -lc",
50 "zsh -lc ''",
51 "zsh -lc 'git status",
52 "zsh -lc 'env'",
53 'zsh -lc "git \\\"',
54 ])("falls back for missing or malformed scripts: %s", (command) => {
55 expect(commandProgramName(command)).toBeNull();
56 });
57
58 it.each([
59 "if test -f package.json; then vp test; fi",
60 "[ -f package.json ]",
61 "[[ -f package.json ]]",
62 "test -f package.json",
63 'for file in *; do echo "$file"; done',
64 "while true; do sleep 1; done",
65 "until false; do sleep 1; done",
66 "case $name in test) vp test;; esac",
67 'select item in one two; do echo "$item"; done',
68 "function check() { vp test; }",
69 "check() { vp test; }",
70 "k(){ echo ok; }; k",
71 "{ vp test; }",
72 "(vp test)",
73 "(( count += 1 ))",
74 "! vp test",
75 ":",
76 ". ./script.sh",
77 "source ./script.sh",
78 "eval 'vp test'",
79 "cd packages/client-runtime",
80 "export NODE_ENV=test",
81 "local name=value",
82 "set -e",
83 "alias ll='ls -la'",
84 "repeat 3 echo ok",
85 "and vp test",
86 "return 1",
87 "break",
88 "continue",
89 "true",
90 "false",
91 ])("falls back for shell syntax and internal control commands: %s", (command) => {
92 expect(commandProgramName(command)).toBeNull();
93 });
94
95 it.each([
96 ['rg -n "if|for|while" src', "rg"],
97 ["printf '%s\\n' 'a;b|c'", "printf"],
98 ["node -e \"if (true) console.log('ok')\"", "node"],
99 ["echo '$(git status)'", "echo"],
100 ["vp test && git status", "vp"],
101 ["vp test || git status", "vp"],
102 ["rg needle src | head", "rg"],
103 ["vp test; git status", "vp"],
104 ["vp test &", "vp"],
105 ["vp test\ngit status", "vp"],
106 ['echo "$(git status)"', "echo"],
107 ["echo `git status`", "echo"],
108 ["cat <(rg needle src)", "cat"],
109 ])("uses the first executable-looking program: %s", (command, program) => {
110 expect(commandProgramName(command)).toBe(program);
111 });
112
113 it.each([
114 ["cd packages/client-runtime && vp test run", "vp"],
115 ['cd "a path with spaces"; git status', "git"],
116 ["cd apps/web\npnpm test", "pnpm"],
117 ["cd first && cd second && bun test", "bun"],
118 ["CI=1 cd apps/web && npm test", "npm"],
119 ["PATH+=:/tools npm test", "npm"],
120 ["PATH+=:/tools && npm test", "npm"],
121 ['TMP=$(mktemp -d); cd "$TMP"; npm pack ./package', "npm"],
122 ["cd $(find . -type d | head -1) && git status", "git"],
123 ["cd `find . -type d | head -1` && node script.js", "node"],
124 ["cd /tmp 2>&1 && npm test", "npm"],
125 ["cd /tmp 2<&0 && pnpm test", "pnpm"],
126 ["cd /tmp &>/dev/null && bun test", "bun"],
127 ["cd work |& npm test", "npm"],
128 ["cd /tmp && # use the selected workspace\nnpm test", "npm"],
129 ["export CI=1; # first note\n# second note\npnpm test", "pnpm"],
130 ["cd&&npm test", "npm"],
131 ["export CI=1;pnpm test", "pnpm"],
132 ["cd ${ROOT:-path;with;semicolons} && bun test", "bun"],
133 ["cd ${ROOT:-path&&fallback} && node app.js", "node"],
134 ["cd @(first|second) && npm test", "npm"],
135 ["cd /tmp \\\n&& npm test", "npm"],
136 ["/bin/zsh -lc 'cd apps/web && vp test run'", "vp"],
137 ])("skips leading cd commands and uses the next useful program: %s", (command, program) => {
138 expect(commandProgramName(command)).toBe(program);
139 });
140
141 it.each([
142 ["source ~/.nvm/nvm.sh && nvm use", "nvm"],
143 [". ./.env && pnpm test", "pnpm"],
144 ["export CI=1 && vp test run", "vp"],
145 ["unset DEBUG; node app.js", "node"],
146 ["export CI=1 && cd apps/web && pnpm test", "pnpm"],
147 ["/bin/zsh -lc 'source ~/.nvm/nvm.sh && nvm use'", "nvm"],
148 ])("skips shell setup commands and uses the next useful program: %s", (command, program) => {
149 expect(commandProgramName(command)).toBe(program);
150 });
151
152 it.each([
153 ["set -eu; npm test", "npm"],
154 [": && npm test", "npm"],
155 ["true && npm test", "npm"],
156 ["false || npm test", "npm"],
157 ["false; npm test", "npm"],
158 ["sudo -n true && npm test", "npm"],
159 ["sudo -n true; echo checked", "echo"],
160 ["test -d node_modules || vp i", "vp"],
161 ["[ -d node_modules ] || vp i", "vp"],
162 ])("skips non-descriptive shell commands before a useful program: %s", (command, program) => {
163 expect(commandProgramName(command)).toBe(program);
164 });
165
166 it.each(
167 ["cd /tmp", "export CI=1", "unset DEBUG", "source env.sh", ". env.sh"].flatMap((setup) =>
168 ["&&", " || ", ";", "\n", "|", " |& ", " & "].map(
169 (operator) => [`${setup}${operator}npm test`, "npm"] as const,
170 ),
171 ),
172 )("handles shell setup followed by every command separator: %s", (command, program) => {
173 expect(commandProgramName(command)).toBe(program);
174 });
175
176 it.each([
177 ["command git status", "git"],
178 ["command -p git status", "git"],
179 ["command -- git status", "git"],
180 ["builtin printf ok", "printf"],
181 ["builtin -- echo ok", "echo"],
182 ["command cd /tmp && npm test", "npm"],
183 ["builtin cd /tmp && pnpm test", "pnpm"],
184 ["exec node app.js", "node"],
185 ["exec -cl -a worker node app.js", "node"],
186 ["exec env CI=1 /opt/tools/check --verbose", "check"],
187 ['exec "C:\\Program Files\\nodejs\\node.exe" app.js', "node.exe"],
188 ["exec sh -c 'cd /tmp && npm test'", "npm"],
189 ["exec sh -c 'cd /tmp\nnpm test'", "npm"],
190 ["exec bash -c 'set -e\nnpm test'", "npm"],
191 ])("unwraps shell command wrappers: %s", (command, program) => {
192 expect(commandProgramName(command)).toBe(program);
193 });
194
195 it.each([
196 ["timeout 10 pnpm test", "pnpm"],
197 ["timeout 10s python3 script.py", "python3"],
198 ["gtimeout 1.5 node app.js", "node"],
199 ["nohup npx expo start >/tmp/metro.log 2>&1 &", "npx"],
200 ["nohup -- env CI=1 bun test", "bun"],
201 ["arch -x86_64 ./build/app-under-test", "app-under-test"],
202 ["arch -arch arm64 /opt/tools/check", "check"],
203 ["bundle exec pod install", "pod"],
204 ["timeout 30 nohup env CI=1 node app.js", "node"],
205 ["timeout 60 script -q /dev/null env CI=1 node app.js", "node"],
206 ])("unwraps process-launch wrappers: %s", (command, program) => {
207 expect(commandProgramName(command)).toBe(program);
208 });
209
210 it.each([
211 ["& 'C:\\Program Files\\nodejs\\node.exe' script.js", "node.exe"],
212 ['& "$env:WINDIR\\Microsoft.NET\\Framework64\\v4\\csc.exe" file.cs', "csc.exe"],
213 ])("resolves literal PowerShell call operators: %s", (command, program) => {
214 expect(commandProgramName(command)).toBe(program);
215 });
216
217 it.each([
218 ["$env:CI='1'; npm test", "npm"],
219 ["$value = 'configured'; node app.js", "node"],
220 ["$process = Get-Process node; $process.Id", "Get-Process"],
221 ["$tmp = Join-Path $env:TEMP repo; git clone example", "Join-Path"],
222 ["$html = (Invoke-WebRequest https://example.com).Content", "Invoke-WebRequest"],
223 ["$process = Start-Process -FilePath .\\app.exe -PassThru; $process.Id", "app.exe"],
224 ])("labels commands inside simple PowerShell assignments: %s", (command, program) => {
225 expect(commandProgramName(command)).toBe(program);
226 });
227
228 it.each([
229 ["cmd /c bcdedit /enum", "bcdedit"],
230 ["cmd /c cd C:\\work && npm test", "npm"],
231 ['cmd.exe /d /s /c "cd C:\\work && npm test"', "npm"],
232 [
233 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\\work\\scripts\\doctor.ps1"',
234 "doctor.ps1",
235 ],
236 ['pwsh -NoProfile -Command "Set-Location C:\\work; pnpm test"', "pnpm"],
237 ["pwsh -Command Set-Location C:\\work; pnpm test", "pnpm"],
238 ])("unwraps Windows shell launchers: %s", (command, program) => {
239 expect(commandProgramName(command)).toBe(program);
240 });
241
242 it.each([
243 ["Set-Location C:\\work; npm test", "npm"],
244 ["Push-Location C:\\work; node app.js", "node"],
245 ["Start-Process -FilePath node -ArgumentList server.js", "node"],
246 ["Start-Process -ArgumentList '-FilePath helper' node", "node"],
247 ["Start-Process -ErrorAction Stop -FilePath node", "node"],
248 ["Start-Process -NoNewWindow -WorkingDirectory C:\\work node", "node"],
249 ['Start-Process "C:\\Program Files\\Example\\app.exe"', "app.exe"],
250 ['Start-Process -FilePath ".\\dist\\Example App.exe" -PassThru', "Example App.exe"],
251 [".\\.venv\\Scripts\\python.exe script.py", "python.exe"],
252 ["$env:LOCALAPPDATA\\Programs\\tool.exe --version", "tool.exe"],
253 ['"=== CHECK FILE ==="; Get-Content file.txt', "Get-Content"],
254 ["$x = @'\ndata'; Get-Fake\n'@\nGet-Process", "Get-Process"],
255 ["@'\nprint('ok; still data')\n'@ | python -", "python"],
256 ])("handles common PowerShell setup and launch commands: %s", (command, program) => {
257 expect(commandProgramName(command)).toBe(program);
258 });
259
260 it.each([
261 ["timeout --help", "timeout"],
262 ["nohup --version", "nohup"],
263 ["arch", "arch"],
264 ["bundle install", "bundle"],
265 ["script output.log", "script"],
266 ["/usr/bin/timeout 10 node app.js", "timeout"],
267 ])("keeps process-launch wrappers when no safe payload is present: %s", (command, program) => {
268 expect(commandProgramName(command)).toBe(program);
269 });
270
271 it.each([
272 "command -v git",
273 "command -V git",
274 "command -a git",
275 "command -pv git",
276 "builtin -p",
277 "exec",
278 "exec > output.log",
279 "exec --",
280 "exec cd /tmp && npm test",
281 "exec env CI=1 cd /tmp && npm test",
282 "exec CI=1 npm test",
283 "env CI=1 cd /tmp && npm test",
284 "env CI=1; npm test",
285 "sudo cd /tmp && npm test",
286 "command CI=1 npm test",
287 "command false && npm test",
288 "cmd /c false && npm test",
289 "cd /tmp <<EOF\nunterminated heredoc",
290 "cd /tmp && >/tmp/log",
291 "(xcrun simctl io booted recordVideo /tmp/video.mp4 &) ; wait",
292 "export CI=1 && (bundle exec pod install || pod install)",
293 "$PY scripts/check.py",
294 "${TOOL} --version",
295 "%TOOL% --version",
296 "!TOOL! --version",
297 "& $tool --version",
298 "& { Get-Process }",
299 "$value = 'configured'",
300 "$headers = @{ 'Accept' = 'application/json'; 'Content-Type' = 'application/json' }; Invoke-WebRequest https://example.com",
301 '"sha256(value)=$hash"',
302 "broken{",
303 "@echo off",
304 ":: comment",
305 "time -- npm test",
306 "time -v npm test",
307 "coproc npm test",
308 "coproc worker { npm test; }",
309 "cd [first|second] && pnpm test",
310 "npm) --version",
311 "try { Invoke-WebRequest https://example.com } catch { Write-Error $_ }",
312 "for($i=0; $i -lt 2; $i++){ Start-Sleep 1 }",
313 "# comment only",
314 ])("does not treat shell lookup and commandless wrapper forms as executions: %s", (command) => {
315 expect(commandProgramName(command)).toBeNull();
316 });
317
318 it.each([
319 ["parallel -j4", "parallel"],
320 ["hash --help", "hash"],
321 ["process --help", "process"],
322 ["rem comment", "rem"],
323 ["Exec node app.js", "Exec"],
324 ["CD /tmp && npm test", "CD"],
325 ])("does not hide legitimate or case-distinct program names: %s", (command, program) => {
326 expect(commandProgramName(command)).toBe(program);
327 });
328
329 it.each([
330 ["env sh -c 'cd /tmp && npm test'", "npm"],
331 ["sudo zsh -lc 'export CI=1 && pnpm test'", "pnpm"],
332 ])("parses shell setup inside an explicitly launched shell: %s", (command, program) => {
333 expect(commandProgramName(command)).toBe(program);
334 });
335
336 it.each([
337 ["nocorrect pnpm test", "pnpm"],
338 ["noglob bun test", "bun"],
339 ["time node app.js", "node"],
340 ["time -p deno test", "deno"],
341 ["time nocorrect npm test", "npm"],
342 ])("skips shell precommand modifiers: %s", (command, program) => {
343 expect(commandProgramName(command)).toBe(program);
344 });
345
346 it("uses the command after leading shell comments", () => {
347 expect(commandProgramName("# first comment\n # second comment\ngit status")).toBe("git");
348 });
349
350 it.each([
351 ["CI=1 # note\nnpm test", "npm"],
352 ["CI=1 # it's configured\nnpm test", "npm"],
353 ['CI=1 # "unterminated quote\nbun test', "bun"],
354 [">/tmp/log # note\npnpm test", "pnpm"],
355 [">/tmp/log # it's configured\ndeno test", "deno"],
356 ])("skips comments after commandless shell setup: %s", (command, program) => {
357 expect(commandProgramName(command)).toBe(program);
358 });
359
360 it.each([
361 ["./cd /tmp && npm test", "cd"],
362 ["/opt/exec node app.js", "exec"],
363 ["/usr/bin/time npm test", "time"],
364 ["/usr/bin/test -f package.json", "test"],
365 ])("does not treat qualified paths as shell syntax: %s", (command, program) => {
366 expect(commandProgramName(command)).toBe(program);
367 });
368
369 it("skips a shell array assignment before the command", () => {
370 expect(commandProgramName("items=(one two); npm test")).toBe("npm");
371 });
372
373 it.each([
374 ['EMU="/opt/android/emulator"; "$EMU" -list-avds', "emulator"],
375 ["AAPT=/opt/android/aapt2\n$AAPT dump badging app.apk", "aapt2"],
376 [
377 'SSH=(ssh -i /tmp/key -o IdentitiesOnly=yes); HOST=user@example; "${SSH[@]}" "$HOST" uptime',
378 "ssh",
379 ],
380 ["SCP=(/usr/bin/scp -i /tmp/key); if true; then ${SCP[@]} file user@example:/tmp; fi", "scp"],
381 ['TOOL="/Applications/My Tool/bin/check"; "$TOOL" --verbose', "check"],
382 ])("resolves literal command aliases from earlier shell segments: %s", (command, program) => {
383 expect(commandProgramName(command)).toBe(program);
384 });
385
386 it.each([
387 'TOOL=$(pick-command); "$TOOL" --version',
388 'TOOL="git status"; "$TOOL"',
389 "# TOOL=git\n$TOOL status",
390 'TOOL=git true; "$TOOL" status',
391 'TOOL=git; TOOL=$(pick-command); "$TOOL" status',
392 'TOOL=git; unset TOOL; "$TOOL" status',
393 ])("does not evaluate dynamic or non-persistent command aliases: %s", (command) => {
394 expect(commandProgramName(command)).toBeNull();
395 });
396
397 it("does not retain aliases assigned inside control flow", () => {
398 expect(commandProgramName('TOOL=git; if false; then\nTOOL=npm\nfi\n"$TOOL" status')).toBe(
399 "git",
400 );
401 });
402
403 it.each([
404 ["ROOT=${BASE:-path with spaces}; npm test", "npm"],
405 ["ROOT=`printf 'path with spaces'`; pnpm test", "pnpm"],
406 ])("keeps expansions inside assignment words: %s", (command, program) => {
407 expect(commandProgramName(command)).toBe(program);
408 });
409
410 it.each([
411 [">/tmp/log && npm test", "npm"],
412 ["2>/tmp/error.log; pnpm test", "pnpm"],
413 ["cd /tmp && >/tmp/log npm test", "npm"],
414 ["cd /tmp && > /tmp/log pnpm test", "pnpm"],
415 ["cd /tmp && 2>&1 bun test", "bun"],
416 ["cd /tmp && 2>& 1 node app.js", "node"],
417 ["cd /tmp && &>/tmp/log git status", "git"],
418 ["cd /tmp && *>>/tmp/log vp test", "vp"],
419 ["cd /tmp && {output}>/tmp/log deno test", "deno"],
420 ["cd /tmp && <<<input ruby script.rb", "ruby"],
421 ])("skips redirections before the next command: %s", (command, program) => {
422 expect(commandProgramName(command)).toBe(program);
423 });
424
425 it.each([
426 ["cd /tmp <<EOF\nnot-a-command\nEOF\nnpm test", "npm"],
427 ["cd /tmp <<'EOF'\nnot-a-command\nEOF\npnpm test", "pnpm"],
428 ["cd /tmp <<'EOF'\nnot-a-command\\\nEOF\npnpm test", "pnpm"],
429 ["cd /tmp <<-EOF\n\tnot-a-command\n\tEOF\nbun test", "bun"],
430 ["cd /tmp <<A <<B\none\nA\ntwo\nB\ngit status", "git"],
431 ["cd /tmp <<EOF; # setup\nnot-a-command\nEOF\nnpm test", "npm"],
432 ["cd /tmp <<EOF && pnpm test\nnot-a-command\nEOF\nbun test", "pnpm"],
433 ])("skips heredoc bodies before finding the next command: %s", (command, program) => {
434 expect(commandProgramName(command)).toBe(program);
435 });
436
437 it.each(["cd apps/web && [ -f package.json ]", "cd apps/web || exit 1", "cd one && cd two"])(
438 "falls back when no useful program follows cd: %s",
439 (command) => {
440 expect(commandProgramName(command)).toBeNull();
441 },
442 );
443
444 it.each([
445 "if test -f package.json\nthen\n npm test\nfi",
446 "[[ -d first && -d second ]] && npm test",
447 'for file in *\ndo\n echo "$file"\ndone',
448 "while true\ndo\n sleep 1\ndone",
449 "cd /tmp; build () { npm test; }",
450 ])("does not label commands inside multiline shell control flow: %s", (command) => {
451 expect(commandProgramName(command)).toBeNull();
452 });
453
454 it("bounds nested shell unwrapping", () => {
455 let command = "git status";
456 for (let depth = 0; depth < 9; depth += 1) {
457 command = `sh -c '${command.replaceAll("'", "'\\''")}'`;
458 }
459 expect(commandProgramName(command)).toBeNull();
460 });
461
462 it("does not spend the shell nesting budget on setup commands", () => {
463 const command = [
464 ...Array.from({ length: 10 }, (_, index) => `export VALUE_${index}=configured`),
465 "npm test",
466 ].join("\n");
467
468 expect(commandProgramName(command)).toBe("npm");
469 });
470
471 it("bounds the number of top-level setup segments", () => {
472 const command = [
473 ...Array.from({ length: 2_000 }, (_, index) => `export VALUE_${index}=configured`),
474 "npm test",
475 ].join(";");
476
477 expect(commandProgramName(command)).toBeNull();
478 });
479
480 it("bounds nested command wrappers", () => {
481 expect(commandProgramName(`${"command ".repeat(9)}git status`)).toBeNull();
482 });
483});
484
485describe("commandDisplayText", () => {
486 it.each([
487 ["/bin/zsh -lc 'git diff --stat'", "git diff --stat"],
488 ['bash -c "printf \\"%s\\" hello; git status"', 'printf "%s" hello; git status'],
489 ["/usr/bin/fish --command 'rg TODO src | head -20'", "rg TODO src | head -20"],
490 ["zsh -lc 'pwd\nls'", "pwd\nls"],
491 ["git status", "git status"],
492 ["zsh script.sh", "zsh script.sh"],
493 ["zsh -lc 'echo $1' name value", "zsh -lc 'echo $1' name value"],
494 ["zsh -lc 'pwd'; git status", "zsh -lc 'pwd'; git status"],
495 ["zsh -lc 'unterminated", "zsh -lc 'unterminated"],
496 ])("shows the script without hiding meaningful outer commands: %s", (input, expected) => {
497 expect(commandDisplayText(input)).toBe(expected);
498 });
499});
500
501describe("commands recorded from each provider", () => {
502 // Inputs are copied from the recorded replay transcripts in
503 // apps/server/src/orchestration-v2/testkit/fixtures (turn_interrupt_mid_tool,
504 // todo_list, grok_background_bash and
505 // opencode_running_child_approval). In turn_interrupt_mid_tool every provider
506 // ran the same script, and only Codex wraps it in a login shell.
507 const interruptScript = `node -e "console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)"`;
508 it.each([
509 ["Claude", interruptScript, interruptScript],
510 ["Cursor", interruptScript, interruptScript],
511 ["Pi", interruptScript, interruptScript],
512 [
513 "Codex",
514 `/bin/bash -lc "node -e \\"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\\""`,
515 interruptScript,
516 ],
517 [
518 "Codex",
519 "/bin/bash -lc 'cat package.json && cat tsconfig.json'",
520 "cat package.json && cat tsconfig.json",
521 ],
522 [
523 "Grok",
524 "for i in 1 2 3; do sleep 8; echo tock $i; done",
525 "for i in 1 2 3; do sleep 8; echo tock $i; done",
526 ],
527 [
528 "OpenCode",
529 "printf 'running child approval' > running-child.txt",
530 "printf 'running child approval' > running-child.txt",
531 ],
532 ])("%s shows the script it ran: %s", (_provider, input, script) => {
533 expect(commandDisplayText(input)).toBe(script);
534 expect(commandHighlightLanguage(commandDisplayText(input))).toBe("shellscript");
535 });
536});
537
538describe("commandDisplayText with redacted Codex commands", () => {
539 // Codex replaces secrets in command text before sending it
540 // (codex-rs/secrets/src/sanitizer.rs). Usually the quoting survives.
541 it("unwraps a redacted command whose quoting is intact", () => {
542 expect(
543 commandDisplayText(
544 `/bin/zsh -lc 'curl -H "Authorization: Bearer [REDACTED_SECRET]" https://example.com'`,
545 ),
546 ).toBe('curl -H "Authorization: Bearer [REDACTED_SECRET]" https://example.com');
547 });
548
549 // Its secret-assignment pattern can also swallow the backslash of an escaped
550 // quote: `token=config[\"apiToken\"]` becomes `token=[REDACTED_SECRET]"apiToken\"]`.
551 // The bare quote ends the -lc argument early, so where the script ends is unknown.
552 it("keeps the wrapper when redaction broke the quoting", () => {
553 const input = `/bin/zsh -lc "python3 -c 'token=[REDACTED_SECRET]"apiToken\\"]; print(token)'"`;
554 expect(commandDisplayText(input)).toBe(input);
555 });
556});
557
558describe("commandDisplayText control characters", () => {
559 it.each([
560 ["printf '\u001b[31mred\u001b[0m'", "printf '␛[31mred␛[0m'"],
561 ["zsh -lc 'printf \"\u001b[1m\"'", 'printf "␛[1m"'],
562 ["echo bell\u0007 delete\u007f", "echo bell␇ delete␡"],
563 ["printf 'a\\tb'\r\necho done", "printf 'a\\tb'\r\necho done"],
564 ["cat <<'EOF'\n\tindented\nEOF", "cat <<'EOF'\n\tindented\nEOF"],
565 ])("shows %j as %j", (input, expected) => {
566 expect(commandDisplayText(input)).toBe(expected);
567 });
568});
569
570describe("commandHighlightLanguage", () => {
571 it.each([
572 [
573 '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "Get-ChildItem -Recurse"',
574 "powershell",
575 ],
576 [
577 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\\work\\scripts\\doctor.ps1"',
578 "powershell",
579 ],
580 ["& pwsh -c 'Get-Date'", "powershell"],
581 ["PWSH.EXE -Command Get-Date", "powershell"],
582 ["pwsh-preview -c 'Get-Date'", "shellscript"],
583 ["git status; pwsh -c 'Get-Date'", "shellscript"],
584 ["cat <<'EOF' > notes.txt\npwsh\nEOF", "shellscript"],
585 ["", "shellscript"],
586 // Windows agents also run PowerShell directly, without a pwsh wrapper.
587 ["Get-Content package.json | Select-String version", "powershell"],
588 ["$env:CI='1'; npm test", "powershell"],
589 ["$tmp = Join-Path $env:TEMP repo; git clone example", "powershell"],
590 ['"=== CHECK FILE ==="; Get-Content file.txt', "powershell"],
591 ["& 'C:\\Python312\\python.exe' -c 'print(1)'", "powershell"],
592 ["Install-Module Pester -Scope CurrentUser", "powershell"],
593 ["ConvertTo-Json @{ a = 1 }", "powershell"],
594 ["update-alternatives --list java", "shellscript"],
595 ["install-info --version", "shellscript"],
596 ["Make-Thing now", "shellscript"],
597 ["echo Get-Content; git status", "shellscript"],
598 ["FOO=1 npm test", "shellscript"],
599 ["echo $HOME && ls", "shellscript"],
600 ])("%s", (command, language) => {
601 expect(commandHighlightLanguage(command)).toBe(language);
602 });
603});