Skip to content

fix: return non-zero exit for unrecognized commands - #1051

Merged
mbevc1 merged 2 commits into
mainfrom
20260728_non_existing_cmds
Jul 28, 2026
Merged

fix: return non-zero exit for unrecognized commands#1051
mbevc1 merged 2 commits into
mainfrom
20260728_non_existing_cmds

Conversation

@mbevc1

@mbevc1 mbevc1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Cobra's TraverseChildren routing (set on the root command) hands an unrecognized command token to the nearest group command with no error; that non-runnable command then prints help and returns nil, so the CLI exits 0. A typo'd command such as kosli garbage list flows therefore silently reports success (issue #1043).

Add findUnknownCommand, called from innerMain before ExecuteC. It resolves the args with cobra's Traverse on a throwaway command tree (so the real command's flag state is untouched) and returns an error when resolution lands on a non-runnable group command while an unconsumed non-flag token that names no subcommand remains. Real commands, group commands invoked with no leftover (e.g. kosli list), and the existing unknown-flag path are all left unchanged.

Example output:

$ ./kosli garbage ls
Error: unknown command: garbage
available subcommands are: allow | archive | assert | attach-policy | attest | begin | completion | config | create | delete | detach-policy | diff | disable | enable | evaluate | fingerprint | get | join | list | log | rename | report | rotate | search | snapshot | status | tag | unarchive | update | version

Closes #1043

Checklist

@mbevc1 mbevc1 added the fix label Jul 28, 2026
@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mbevc1's task in 2m 36s —— View job


Re-review after commit d615c323

  • Read updated main.go / root_test.go and prior review context
  • Static analysis (go build ./..., go vet ./cmd/kosli/ — both clean)
  • Verify the two prior review fixes
  • Fresh pass for remaining issues

Verdict: both prior issues are correctly resolved ✅

🔴 Global corruption (prior High) — fixed. saved := global; defer func() { global = saved }() (main.go:81-82) correctly restores the package variable. I confirmed newRootCmd's only package-level mutation is global = new(GlobalOpts) (root.go:382) — the args parameter is never used inside the function body, so nothing else leaks from the probe. TestInnerMainPreservesGlobalAfterProbe is a genuine regression guard: it drives a real command through innerMain (which the golden harness bypasses) and asserts global.Host/global.ApiToken survive — this fails without the defer and passes with it.

🟡 Normalization mismatch (prior Medium) — fixed. The probe now resolves normalizeBoolFlagArgs(probe, args) (main.go:91), and the list --debug false negative case locks it in.

The core logic reads cleanly: Traverse on a throwaway tree, bail on err/Runnable(), find the first non-flag leftover, and defer to cobra when that token is actually a known subcommand/alias so the existing flag diagnostics still fire. The doc comment accurately describes each early-return.

Minor / non-blocking

  1. Duplicated unknown-command reporting (already noted as a follow-up by the author). The availableSubcommands construction and the unknown command: … / available subcommands are: … message now exist in both findUnknownCommand (main.go:119-126) and the unknown-flag block (main.go:183-189). Worth extracting a small helper (e.g. unknownCommandError(c, token)) in a later cleanup so the two paths can't drift. Fix this →

  2. Edge case (out of scope, just flagging): the check is !c.Runnable(). A command that is both runnable and has subcommands (RunE + AddCommand) would still short-circuit to nil, so a bogus positional there could reach a lenient Args validator and exit 0 — the same class as Bug: an unrecognized command token is ignored, and the CLI exits 0 #1043. I don't think kosli currently has such a command, so this is fine today; a one-line test asserting the assumption (or a comment) would future-proof it.

  3. Tiny perf: findUnknownCommand builds the full command tree a second time via newRootCmd. Negligible for a CLI startup path — no action needed, noting for completeness.

Nice work

  • Restoring global with defer right next to the newRootCmd call, with a comment explaining why, is exactly the right shape.
  • The two new tests each target the precise failure mode (global survival through innerMain; normalized bool flag not misread), and the author verified they fail without the fix — good red/green discipline per the repo's TDD guidance.

Looks ready to merge from my side; the items above are optional.

Comment thread cmd/kosli/main.go
Comment thread cmd/kosli/main.go
Comment thread cmd/kosli/root_test.go
@mbevc1
mbevc1 marked this pull request as draft July 28, 2026 15:19

mbevc1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review in 68c1089:

🔴 High — probe corrupted the package-level global. Confirmed real. findUnknownCommand now saves and restores global around the newRootCmd probe (saved := global; defer func() { global = saved }()), so the real command's persistent-flag bindings stay intact.

🟡 Medium — normalization mismatch. The probe now resolves normalizeBoolFlagArgs(probe, args) instead of the raw args, so kosli list --debug false no longer misreads false as an unknown command.

🟢 Tests. Added TestInnerMainPreservesGlobalAfterProbe (drives a successful fingerprint … --host … --api-token … through innerMain and asserts global.Host/global.ApiToken survive) plus a list --debug false negative case in TestFindUnknownCommand. I verified both fail without their respective fix and pass with it.

Verified with a real binary (the sandbox does allow execution here):

  • kosli garbage / garbage list flows / list garbage → exit 1, unknown command: …
  • kosli list --debug false → exit 0, prints list group help
  • kosli list flows --host https://example.kosli.com --org demo → request goes to https://example.kosli.com/api/v2/flows/demo (i.e. --host is honored — the exact global regression, now fixed)
  • kosli list → exit 0; kosli --badflag list flowsmissing subcommand (existing path unchanged)

The duplication between findUnknownCommand and the existing unknown-flag block is left as a noted follow-up.

mbevc1 added 2 commits July 28, 2026 16:37
Cobra's `TraverseChildren` routing (set on the root command) hands an
unrecognized command token to the nearest group command with no error;
that non-runnable command then prints help and returns nil, so the CLI
exits 0. A typo'd command such as `kosli garbage list flows` therefore
silently reports success (issue #1043).

Add `findUnknownCommand`, called from `innerMain` before `ExecuteC`. It
resolves the args with cobra's `Traverse` on a throwaway command tree (so
the real command's flag state is untouched) and returns an error when
resolution lands on a non-runnable group command while an unconsumed
non-flag token that names no subcommand remains. Real commands, group
commands invoked with no leftover (e.g. `kosli list`), and the existing
unknown-flag path are all left unchanged.
Address review feedback on the unknown-command detection:

- `findUnknownCommand` built its probe with `newRootCmd`, which reassigns
  the package-level `global` and rebinds the real command's
  persistent-flag pointers to a throwaway struct. Nothing restored it,
  so after the probe ran every global.* read (ApiToken, Host, ...) saw
  the throwaway defaults and real invocations silently ignored their
  global flags. Save and restore `global` around the probe.

- The probe resolved the raw args while `innerMain` executes normalized
  args, so a space-form bool flag on a group command
  (`kosli list --debug false`) had its value treated as a leftover
  positional and was wrongly reported as `unknown command: false`.
  Normalize the probe args with normalizeBoolFlagArgs too.

Add `TestInnerMainPreservesGlobalAfterProbe` (drives a successful real
command with global flags through `innerMain` and asserts the values
survive) and a space-form-bool negative case to TestFindUnknownCommand.
Both fail without the corresponding fix.
@mbevc1
mbevc1 force-pushed the 20260728_non_existing_cmds branch from 68c1089 to d615c32 Compare July 28, 2026 15:38
@mbevc1
mbevc1 marked this pull request as ready for review July 28, 2026 15:39
@mbevc1
mbevc1 merged commit 31b6f40 into main Jul 28, 2026
20 checks passed
@mbevc1
mbevc1 deleted the 20260728_non_existing_cmds branch July 28, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: an unrecognized command token is ignored, and the CLI exits 0

2 participants