fix(cli-app): parse app:exec loosely when command separator is missing (PER-9997)#2334
Conversation
Some environments strip the `--` separator before argv reaches the CLI (notably npm's npx PowerShell shim on Windows), which made `percy app:exec -- dotnet test` fail with "ParseError: Unexpected argument 'dotnet'" while the equivalent `percy exec` invocation only warned and ran the command. Reuse the exec command's loose definition for app:exec so both commands degrade the same way when the separator is missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prklm10
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Failed - see PR comment.
| // some environments (e.g. npx PowerShell shims) strip the `--` separator; | ||
| // the command should warn and run instead of throwing a parse error | ||
| expect(exec.definition.loose).toEqual(ExecPlugin.default.definition.loose); | ||
| await expectAsync(exec(['dotnet', 'test'])).not.toBeRejectedWithError( |
There was a problem hiding this comment.
[High] New spec is non-hermetic and doesn't assert the fixed behavior
This spec mocks neither which.sync nor cross-spawn (unlike packages/cli-exec/test/exec.test.js, which mocks both), so on any machine with the .NET SDK on PATH — precisely the environments this fix targets — exec(['dotnet', 'test']) resolves and spawns a real dotnet test subprocess: slow, flaky, side-effectful. It also only asserts that one specific error message is absent; any other downstream failure (e.g. Command not found "dotnet") also passes, so it doesn't guard the regression it claims to cover.
Suggestion: Assert directly against the parser (import parse from @percy/cli-command and check argv parses as command='dotnet', args=['test'] with the loose warning logged), or at minimum mock which.sync and cross-spawn like cli-exec/test/exec.test.js does and spy on log.warn to confirm the loose message is emitted.
Reviewer: stack:code-reviewer
There was a problem hiding this comment.
Fixed in 0fa31dd: the spec now mocks which.sync (no real binary resolution or subprocess), asserts the rejection Command not found "dotnet" — proving dotnet was parsed as the command rather than rejected as an unexpected argument — and asserts the loose warning is emitted on stderr.
|
|
||
| // some environments (e.g. npx PowerShell shims) strip the `--` separator; | ||
| // fall back to loose parsing like `percy exec` instead of hard failing | ||
| loose: ExecPlugin.default.definition.loose, |
There was a problem hiding this comment.
[Medium] Blanket loose masks genuine flag typos with a less actionable error
loose swallows all unrecognized input, not just the missing--- case: a flag typo such as --allowed-hostname (previously a clear ParseError: Unknown option '--allowed-hostname', deliberately tested since #1043/#2261) now falls through to which.sync('--allowed-hostname') and produces the confusing Command not found "--allowed-hostname". This silently weakens a previously-tested guarantee specific to app:exec (which sets skipDiscovery: true, unlike plain exec).
Suggestion: Call the trade-off out explicitly in the PR description/changelog as a deliberate match of percy exec behavior, and rename/split the updated does not accept asset discovery options spec so its title matches what it now verifies.
Reviewer: stack:code-reviewer
There was a problem hiding this comment.
Addressed in 0fa31dd: the trade-off is now documented in the PR description as deliberate (matching percy exec's long-standing behavior), the old does not accept asset discovery options spec was split so the exec case is named for what it verifies (degrades unrecognized exec options into the command (loose parsing trade-off)), and the stale usage text was updated to [options] [--] <command> in both exec and app:exec.
Claude Code PR ReviewPR: #2334 • Head: 9ab5b6f • Reviewers: stack:code-reviewer SummaryCopies the Review Table
Findings
Verdict: FAIL — the one-line production fix is correct and consistent with |
- Mock which.sync in the loose-parsing spec so no real binary is resolved or spawned; assert the loose warning is logged and that the argv is parsed as a command (rejected with 'Command not found "dotnet"' rather than a parse error), so the spec actually guards the regression. - Split the exec case out of 'does not accept asset discovery options' into its own spec named for what it now verifies: unrecognized exec options degrade into command lookup under loose parsing. - Update usage text to '[options] [--] <command>' for exec and app:exec since the separator is now optional (degrades to a warning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude Code PR ReviewPR: #2334 • Head: 0fa31dd • Reviewers: stack:code-reviewer (re-review of fixes for the prior FAIL on 9ab5b6f) SummaryCopies Review Table
FindingsNo blocking findings. All three findings from the prior review of 9ab5b6f are resolved and were independently verified by the reviewer:
Informational, non-blocking: there is no regression guard asserting the new usage string itself — very low risk. Verdict: PASS — all prior findings resolved and independently verified (including mutation-testing the new specs); the production change is minimal, correct, and now backed by hermetic, meaningful tests. |
Claude Code PR ReviewPR: #2334 • Head: 0fa31dd • Reviewers: stack:code-reviewer SummaryReuses Review Table
FindingsNo blocking findings. All findings from the review of the previous head (9ab5b6f) are resolved on this commit and were independently verified by the reviewer (local test run, mutation check confirming the new specs fail without the fix). Informational, non-blocking:
Verdict: PASS — the fix is minimal and correct, the tests are hermetic and verified to guard the regression, and the UX trade-off is explicitly documented; only a cosmetic, non-blocking coverage note remains. |
Summary
--separator before argv reaches the CLI (notably npm's npx PowerShell shim on Windows), which madepercy app:exec -- dotnet testfail withParseError: Unexpected argument 'dotnet'while the equivalentpercy execinvocation only warned and ran the command.loosedefinition forapp:execso both commands degrade the same way when the separator is missing.usagehelp text of bothexecandapp:execto[options] [--] <command>since the separator is now optional for both.Known trade-off (deliberate)
With
looseparsing,app:execno longer hard-rejects unrecognized flags: a typo such as--allowed-hostnameused to fail withUnknown option '--allowed-hostname'and now falls through to command lookup, failing withCommand not found "--allowed-hostname"after the loose warning. This matches the long-standing behavior ofpercy execand is the accepted cost of tolerating a stripped--separator; it is covered by thedegrades unrecognized exec options into the command (loose parsing trade-off)spec.Test plan
yarn testinpackages/cli-app— 45/45 specs pass. Theparses loosely when the command separator is missingspec is hermetic (mockswhich.sync, no real subprocess) and asserts both the loose warning and that the argv is parsed as a command.yarn testinpackages/cli-exec— 40/40 specs pass.🤖 Generated with Claude Code