| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | commandDisplayText, |
| 5 | commandHighlightLanguage, |
| 6 | commandProgramName, |
| 7 | } from "./commandLabel.ts"; |
| 8 | |
| 9 | describe("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 | |
| 485 | describe("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 | |
| 501 | describe("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 | |
| 538 | describe("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 | |
| 558 | describe("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 | |
| 570 | describe("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 | }); |