Repository navigation
[Hackathon] fix(test): flags documented "with --history / --run / --all" are silently ignored without that flag (their siblings exit 5) #376
Description
Verified against (commit SHA or released version)
commit 988c0355bb05631e9c6c1785e443937ff2a88c0e (main, v0.14.0)
Discord Username / User ID
1149447580477042769
What does this improvement do?
Many options say in their own --help that they only work alongside another flag ("with --history: …", "with --run, …", "with --all: …"). The CLI already has a rule for these. When the enabling flag is missing it refuses with exit 5 and a nextAction naming the fix, instead of quietly doing something else. The test result action states the reason:
// `--env` narrows the history list; on the latest-result path it would be
// silently ignored (same rule as `test run --filter` without `--all`).That rule is applied today to test result --env, test create/create-batch --env, test run --filter, all four of test rerun's --all flags (#160 → #163), testlist run --report/--gh-output/--summary-file, test run --gh-output/--summary-file and ci doctor --timeout. But 14 options documented the same way are still accepted without their enabling flag. They exit 0 and change nothing:
| command | ignored without the enabling flag | its help says |
|---|---|---|
test result <id> |
--since, --source, --page-size, --cursor, --rerun, --no-rerun, --columns, --no-header |
"with --history: …" |
test create |
--wait, --timeout |
"with --run, poll until terminal status" / "with --run --wait, max seconds to wait" |
test create-batch |
--wait, --timeout, --max-concurrency |
"with --run, …" |
test run <id> |
--allow-empty |
"with --all: exit 0 when the run dispatches ZERO tests" |
What a user or agent gets:
test result test_x --since 24hprints the latest result, not the run history it asked for. Invalid values pass too:--page-size 0and--since garbageexit 0.test create --plan-from plan.json --wait(forgot--run) creates the test, runs nothing, and exits 0, the same exit code as a passing--run --wait. A CI step written that way goes green without running a test. Same forcreate-batch … --wait.test run test_x --allow-emptyexits 0 whatever the flag says, whiletest rerun test_x --allow-emptyis refused with exit 5.
Reproduction (offline, --dry-run; plan.json is any valid FE plan, dir/ holds a copy):
# refused today: exit 5
testsprite test result test_x --env staging --dry-run --output json; echo "exit=$?"
testsprite test create --plan-from plan.json --env staging --dry-run --output json; echo "exit=$?"
testsprite test rerun test_x --allow-empty --dry-run --output json; echo "exit=$?"
# accepted and ignored: exit 0
testsprite test result test_x --since 24h --dry-run --output json; echo "exit=$?"
testsprite test result test_x --page-size 0 --dry-run --output json; echo "exit=$?"
testsprite test create --plan-from plan.json --wait --timeout 30 --dry-run --output json; echo "exit=$?"
testsprite test create-batch --plan-from-dir dir --max-concurrency 5 --dry-run --output json; echo "exit=$?"
testsprite test run test_x --allow-empty --dry-run --output json; echo "exit=$?"Observed: 5, 5, 5, then 0, 0, 0, 0, 0. Each ignored invocation prints exactly what it prints without the flag. In the real (non-dry-run) path the checks sit in the command actions, before any request, so the result is the same: in runCreate, opts.wait is read only inside if (opts.run === true).
Expected. Each of the 14 is refused like its siblings: exit 5, VALIDATION_ERROR, details.field set to the flag, and a nextAction such as "--since only applies with --history (it filters the run list). Add --history, or remove --since."
Details / implementation notes
- Where. Every guard goes in front of existing ones of the same shape, using the existing
localValidationError(field, message):test resultaction (src/commands/test.ts, next to the--envcheck): the otherwith --history:flags.--rerun/--no-rerunand--no-headerneedcommand.getOptionValueSource(…) === 'cli', because Commander gives negatable flags a default.runCreate/runCreateBatch:--wait/--timeout(and--max-concurrencyin the batch) whenrun !== true, next to the existing--env requires --run.test runaction:--allow-emptywithout--all, next to--filter only applies with --all, with the wordingtest rerunalready uses.
- Deliberately not included:
test run --timeoutandtestlist run --timeoutwithout--wait. Both commands can poll without--wait(an automatic environment tunnel), so--timeoutis live there even though their help says "with --wait". - Tests: one case per flag in the existing spec of each command (
test.test.ts/ the create and run specs), asserting exit 5 anddetails.field, plus a control showing that the same flag with its enabling flag is still accepted. Each fails onmaintoday. - Optional, to keep this from coming back: a table-driven test that walks the Commander tree, collects every option whose description starts with
with --X, and checks it is refused without--X. The table would carry an explicit allow-list for the two live--timeoutcases above. - Compatibility: a script that passes one of these flags without its enabling flag gets exit 5 instead of 0. That is the same trade fix(rerun): reject explicit IDs with --all and --status/--skip-terminal without --all #163 made for
test rerun, and thenextActionnames the fix.
I already hold #372–#374, so this is free for anyone to pick up once it's triaged.
Confirmations
- I have searched existing issues and this is not a duplicate.
- I have provided my Discord identity above for reward coordination.
Activity
protonjls commentedon Oct 11, 2026
One more of the same class, found after filing.
testlist runaccepts--report-file/--report-suite-namewithout--report junit. Its help describes--report-fileexactly astest run/test rerundo ("output path for --report"), and those two refuse it through the sharedassertJUnitReportOptions(src/lib/junit-report.ts):testlist runwrites the file onlyif (opts.report === 'junit' && opts.reportFile !== undefined). So a CI step that forgets--report junitruns the whole list, exits 0, and leaves no JUnit file for the job's test-report step. An empty--report-file ""passes validation too and only fails at the write, after the poll. The fix is forvalidateTestlistRunOptions(src/commands/testlist.ts) to call the sharedassertJUnitReportOptionsinstead of its own partial check. Tests would go next to'--report junit without --wait → VALIDATION_ERROR'insrc/commands/testlist.spec.ts.