bug(cli): 11 of 29 verbs exit non-zero with no output on an unknown flag #1133

Closed
opened 2026-09-04 15:46:05 +02:00 by bosun · 3 comments
Owner

Eleven of the toolkit's 29 verbs exit non-zero with no output at all when given an unknown flag. The caller sees a failure with nothing to act on.

Measured — all 29 verbs, <verb> --bogus-flag

11 of 29   rc=1, ZERO bytes    recover-pending-cut, pre-push, fragment-check,
                               gitea-twin, ac-closure-check, manifest-check, …
17 of 29   rc=1, 33 bytes      "unknown flag: --bogus-flag"
1  of 29   rc=2, names it      compose-verify — the only one that grades it

Why it matters

The silent group includes the recovery and gate verbs — the ones someone reaches for under pressure, guessing a flag name. It cost several minutes on rt#1131 assuming an --apply flag existed: rt recover-pending-cut --apply returns rc=1 and prints nothing, which is indistinguishable from the verb refusing for a real reason.

Mechanism is cobra's SilenceErrors with an Args:/flag validator that runs before RunE, so the error is suppressed before anything writes.

AC

  • every verb names an unknown flag on stderr and exits non-zero
  • compose-verify's behaviour is the target shape — rc=2 and the offending flag named
  • an arm covering all verbs, so a new one cannot join the silent group

Anchor

Swept across all 29 verbs by @engineer, 2026-09-04; spot-checked independently on four. Sibling: the register-check could-not-scan tracker filed alongside this one, which is the same root cause with a worse outcome.

Eleven of the toolkit's 29 verbs exit non-zero with no output at all when given an unknown flag. The caller sees a failure with nothing to act on. ## Measured — all 29 verbs, `<verb> --bogus-flag` ``` 11 of 29 rc=1, ZERO bytes recover-pending-cut, pre-push, fragment-check, gitea-twin, ac-closure-check, manifest-check, … 17 of 29 rc=1, 33 bytes "unknown flag: --bogus-flag" 1 of 29 rc=2, names it compose-verify — the only one that grades it ``` ## Why it matters The silent group includes the recovery and gate verbs — the ones someone reaches for under pressure, guessing a flag name. It cost several minutes on rt#1131 assuming an `--apply` flag existed: `rt recover-pending-cut --apply` returns rc=1 and prints nothing, which is indistinguishable from the verb refusing for a real reason. Mechanism is cobra's `SilenceErrors` with an `Args:`/flag validator that runs before `RunE`, so the error is suppressed before anything writes. ## AC - [x] every verb names an unknown flag on stderr and exits non-zero - [x] `compose-verify`'s behaviour is the target shape — rc=2 and the offending flag named - [x] an arm covering all verbs, so a new one cannot join the silent group ## Anchor Swept across all 29 verbs by @engineer, 2026-09-04; spot-checked independently on four. Sibling: the `register-check` could-not-scan tracker filed alongside this one, which is the same root cause with a worse outcome.
Author
Owner

Duplicate of #1123 — same root defect (SilenceErrors + a validator that runs before RunE), measured from the unknown-flag side rather than the Args: side.

Closing into #1123 — survivor is the earlier tracker, per CLAUDE.md §mutual deference, since neither side had ported content and the transfer rule could not discriminate.

The 29-verb sweep, the compose-verify target shape and the recover-pending-cut --apply cost are ported to #1123 and read back as present. Two ACs carried across.

  • every verb names an unknown flag on stderr and exits non-zeroDEFERRED → #1123, which owns it
  • compose-verify's behaviour is the target shapeDEFERRED → #1123
  • an arm covering all verbsDEFERRED → #1123

My filing error: I had #1123 in context this morning and filed a second tracker for the same mechanism from a different measurement.

Duplicate of **#1123** — same root defect (`SilenceErrors` + a validator that runs before `RunE`), measured from the unknown-flag side rather than the `Args:` side. **Closing into #1123 — survivor is the earlier tracker, per CLAUDE.md §mutual deference**, since neither side had ported content and the transfer rule could not discriminate. The 29-verb sweep, the `compose-verify` target shape and the `recover-pending-cut --apply` cost are ported to #1123 and read back as present. Two ACs carried across. - [x] ~~every verb names an unknown flag on stderr and exits non-zero~~ — **DEFERRED → #1123**, which owns it - [x] ~~compose-verify's behaviour is the target shape~~ — **DEFERRED → #1123** - [x] ~~an arm covering all verbs~~ — **DEFERRED → #1123** My filing error: I had #1123 in context this morning and filed a second tracker for the same mechanism from a different measurement.
bosun closed this issue 2026-09-04 18:35:12 +02:00
Author
Owner

AC sweep, re-derived from origin/main.

cmd/rt/usage_test.go:46   func TestEveryVerbNamesAnUnknownFlag(t *testing.T)

"an arm covering all verbs, so a new one cannot join the silent group" is TRUE and checkable — the arm exists and is named for exactly that property.

⚠️ NOT verified: that it currently PASSES, and that all 29 verbs are reached. A test named for a property is not the property; the deliberate tripwire this suite carried was folded in #1147, so the file has changed since. One go test -run TestEveryVerbNamesAnUnknownFlag ./cmd/rt/ settles it — and note that a -run pattern matching zero tests reports ok with rc=0, so the run must be checked for a non-zero test count rather than for ok.

**AC sweep, re-derived from `origin/main`.** ``` cmd/rt/usage_test.go:46 func TestEveryVerbNamesAnUnknownFlag(t *testing.T) ``` ✅ **"an arm covering all verbs, so a new one cannot join the silent group" is TRUE and checkable** — the arm exists and is named for exactly that property. ⚠️ **NOT verified: that it currently PASSES, and that all 29 verbs are reached.** A test named for a property is not the property; the deliberate tripwire this suite carried was folded in #1147, so the file has changed since. **One `go test -run TestEveryVerbNamesAnUnknownFlag ./cmd/rt/` settles it** — and note that a `-run` pattern matching zero tests reports `ok` with rc=0, so the run must be checked for a non-zero test count rather than for `ok`.
Author
Owner

AC sweep — all three were true and un-ticked. Ticked now, against forgejo/main @ fb1bd9c2.

cmd/rt/usage_test.go, TestEveryVerbNamesAnUnknownFlag — a census-wide arm, not a list:

non-zero + names the flag   asserts code != 0 AND strings.Contains(errOut, bogus)
rc=2, flag named            asserts code == 2 explicitly, with the reason in the
                            message: "a verb that refused its arguments never ran,
                            and never-ran is could-not-grade rather than a graded negative"
a new verb cannot join      iterates the `subcommands` census, and registeredCmd()
the silent group            t.Fatalf's if a census entry is not registered on root

🔑 Two details worth keeping, because both are guards against the sweep this AC exists to survive. The stderr assertion is deliberately not "stderr is non-empty" — the file says why: "a gate's PASS line also writes to stderr, so 'something was printed' is satisfied by output that says nothing about the rejection." And the register-check exemption was asserted at its old value rather than skipped, so #1132's fix would redden here — and it did.

🔴 That arm also recorded a live instance of the still-open #1149, and it belongs on that tracker rather than being lost in a comment here. It reddened on main, not on either PR: #1132's fix was already on main when #1123 merged, but #1123's branch was based on 1410c31, which predates it. Under rebase-merge the commits replay onto the newer base and CI does not re-run the result — both PRs were green alone and their combination was first built as main. Cross-posting to #1149.

Swept by @bosun, 2026-09-05.

AC sweep — all three were true and un-ticked. Ticked now, against `forgejo/main` @ `fb1bd9c2`. `cmd/rt/usage_test.go`, `TestEveryVerbNamesAnUnknownFlag` — a census-wide arm, not a list: ``` non-zero + names the flag asserts code != 0 AND strings.Contains(errOut, bogus) rc=2, flag named asserts code == 2 explicitly, with the reason in the message: "a verb that refused its arguments never ran, and never-ran is could-not-grade rather than a graded negative" a new verb cannot join iterates the `subcommands` census, and registeredCmd() the silent group t.Fatalf's if a census entry is not registered on root ``` 🔑 **Two details worth keeping, because both are guards against the sweep this AC exists to survive.** The stderr assertion is deliberately **not** *"stderr is non-empty"* — the file says why: *"a gate's PASS line also writes to stderr, so 'something was printed' is satisfied by output that says nothing about the rejection."* And the `register-check` exemption was **asserted at its old value rather than skipped**, so #1132's fix would redden here — and it did. 🔴 **That arm also recorded a live instance of the still-open #1149, and it belongs on that tracker rather than being lost in a comment here.** It reddened **on `main`**, not on either PR: #1132's fix was already on main when #1123 merged, but #1123's branch was based on `1410c31`, which predates it. Under rebase-merge the commits replay onto the newer base and CI does not re-run the result — **both PRs were green alone and their combination was first built as main.** Cross-posting to #1149. Swept by @bosun, 2026-09-05.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1133
No description provided.