Skip to content

fix: report command-line mistakes as user errors, not internal faults - #2267

Open
evandance wants to merge 1 commit into
mainfrom
refactor/error-classification
Open

fix: report command-line mistakes as user errors, not internal faults#2267
evandance wants to merge 1 commit into
mainfrom
refactor/error-classification

Conversation

@evandance

@evandance evandance commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Getting a command line wrong could report an internal fault. Omitting one of a pair of mutually required flags, passing both when only one is allowed, or typing a stray word where a flag belongs all came back as internal/unknown with exit 5 — the code reserved for "this should never happen" — so the most common way to mistype a command counted against the CLI's own health instead of telling the user what to fix. sheets +csv-put and every +verb shortcut were affected.

The cause was that classification guessed from the error's text: cobra reports these failures as plain sentences, the dispatcher matched them against a list of known phrasings, and anything unrecognized was assumed to be our own fault. Any check cobra gained, any wording it changed, and any validator added here fell outside that list silently.

Classification now follows where the failure happened rather than what it says, and the text list is gone. User-visible output is unchanged everywhere except the four cases being fixed.

Changes

  • Classification takes its category from the dispatch stage: cobra finishes validating the command line before a command body runs, so a failure before that point describes what the user typed, and an unclassified failure after it is a missing conversion on our side.
  • Positional-argument validators are typed where they are produced. A stray word on a shortcut now names the word and points at --help, which it previously did not do at all.
  • A single tree walk wraps every Args validator, so a validator added later is covered without its author having to register anything.
  • A plugin's Shutdown handler receives the same classification the user gets, as a copy — reading a failure can no longer alter the envelope or the exit code.
  • The last-resort branch rebuilds the error instead of reusing it, so a value that cannot serialize itself no longer leaves a non-zero exit with a silent stderr.
  • The plugin SDK docs state which failures never emit Shutdown, and show how to read Err safely.

Test Plan

  • Differential check over 31 error paths (every Args validator shape, both flag-group modes, shortcut positional args, required flags, unknown command/subcommand, flag-parse errors, bootstrap and completion bypasses, help, exit-code-only signals): 27 byte-identical to origin/main, and the only 4 that changed are the ones being fixed
  • Eight mutations each verified to turn a test red: removing the stage instrumentation, inverting the stage judgment, removing the clone handed to the hook, removing either signal pass-through, dropping the cause chain, reverting the positional rejection to untyped, and making the last-resort branch reuse the unrenderable value
  • An invariant test pins that identical error text classifies differently per stage and that unrelated texts in one stage classify identically, so text matching cannot return unnoticed
  • End-to-end coverage that an exit-code-only signal keeps its own exit code and writes nothing to stderr through the full entrypoint
  • A guard that walks the built tree and asserts no Args validator returns an unclassified error
  • gofmt, go vet, go build, golangci-lint, lintcheck, lint module tests, and the affected package tests

Related Issues

  • None

Summary by CodeRabbit

  • Bug Fixes

    • Improved command error classification for invalid input and command execution failures.
    • Positional-argument errors now provide structured validation details, including the offending argument and help guidance.
    • Preserved typed errors and causes while improving fallback handling for untyped failures.
    • Improved shutdown error reporting and isolation for lifecycle hooks.
    • Credential-provider lookup failures now include clearer internal-error context.
  • Documentation

    • Clarified which command failures are available during shutdown events and how they can be classified.

@evandance
evandance requested a review from liangshuo-1 as a code owner August 10, 2026 09:59
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now tracks whether failures occur during user-input validation or command execution. It normalizes untyped errors, passes isolated typed snapshots to shutdown hooks, preserves exit-code-only signals, and adds typed positional-argument and credential-provider errors.

Changes

Dispatch Error Classification

Layer / File(s) Summary
Instrument Cobra dispatch stages
cmd/error_stage.go, cmd/build.go
The command tree tracks validation and command-body entry. Positional validator errors become typed validation errors. Subcommands are instrumented before plugin hooks.
Normalize and render root errors
cmd/root.go, cmd/root_integration_test.go
Root execution normalizes errors by dispatch stage, passes cloned typed errors to shutdown hooks, and rebuilds typed errors when envelope serialization fails.
Classify command and shortcut failures
shortcuts/common/runner.go, internal/cmdutil/factory.go, shortcuts/common/runner_args_test.go
Shortcut positional failures and credential-provider lookup failures now return typed errors with causes and contextual diagnostics.
Validate classification and lifecycle behavior
cmd/error_stage_test.go, cmd/root_test.go
Tests cover validation and command-body classification, shutdown error isolation, exit-code-only signals, typed rendering fallback, and normalized error causes.
Document shutdown error propagation
extension/platform/lifecycle.go, extension/platform/README.md
Lifecycle documentation defines shutdown error snapshots, classification, excluded failure paths, and exit-code-only results.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CobraDispatch
  participant ErrorStageState
  participant RootErrorHandler
  participant ShutdownHook
  participant ErrorRenderer
  CobraDispatch->>ErrorStageState: reset and mark validation or command-body stage
  CobraDispatch->>RootErrorHandler: return dispatch error
  RootErrorHandler->>RootErrorHandler: normalize error by stage
  RootErrorHandler->>ShutdownHook: provide cloned typed error snapshot
  RootErrorHandler->>ErrorRenderer: render classified error
  ErrorRenderer-->>RootErrorHandler: report envelope serialization failure
  RootErrorHandler->>ErrorRenderer: render rebuilt typed error
Loading

Possibly related PRs

Suggested labels: bugfix

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: classifying command-line mistakes as user errors instead of internal faults.
Description check ✅ Passed The description includes all required sections and provides clear scope, changes, related issues, and extensive verification details.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/error-classification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmd/error_stage_test.go (1)

316-318: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the exact exit code.

rebuildTypedError with stageCommandBody produces an internal error, so the exit code is deterministic. Compare against output.ExitInternal instead of only excluding 1.

♻️ Proposed stronger assertion
-	if exit == 1 {
-		t.Errorf("exit = 1; the typed category's exit code must survive the rewrite")
-	}
+	if exit != int(output.ExitInternal) {
+		t.Errorf("exit = %d, want %d; the typed category's exit code must survive the rewrite",
+			exit, int(output.ExitInternal))
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/error_stage_test.go` around lines 316 - 318, Update the assertion in the
rebuildTypedError test to require exit to equal output.ExitInternal, replacing
the current check that only rejects 1. Keep the existing error message context
while asserting the deterministic internal-error exit code produced with
stageCommandBody.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/common/runner.go`:
- Around line 1238-1241: Update the validation error construction in the
positional-argument handling path to pass args[0] to the %q formatter instead of
the full args slice. Keep WithParam(args[0]) and the existing hint unchanged so
the message and parameter consistently report the first stray argument.

---

Nitpick comments:
In `@cmd/error_stage_test.go`:
- Around line 316-318: Update the assertion in the rebuildTypedError test to
require exit to equal output.ExitInternal, replacing the current check that only
rejects 1. Keep the existing error message context while asserting the
deterministic internal-error exit code produced with stageCommandBody.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f48227e1-359c-4a58-937e-01c00f04c684

📥 Commits

Reviewing files that changed from the base of the PR and between 2016120 and 57d80e1.

📒 Files selected for processing (11)
  • cmd/build.go
  • cmd/error_stage.go
  • cmd/error_stage_test.go
  • cmd/plugin_integration_test.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • extension/platform/README.md
  • extension/platform/lifecycle.go
  • internal/cmdutil/factory.go
  • shortcuts/common/runner.go

Comment thread shortcuts/common/runner.go
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@682bfbaf8b5be0183afd250313f108ac5401a452

🧩 Skill update

npx skills add larksuite/cli#refactor/error-classification -y -g

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.09677% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.39%. Comparing base (2016120) to head (682bfba).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cmd/error_stage.go 80.00% 4 Missing and 2 partials ⚠️
shortcuts/common/runner.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2267      +/-   ##
==========================================
+ Coverage   76.36%   76.39%   +0.02%     
==========================================
  Files        1011     1013       +2     
  Lines      111269   111529     +260     
==========================================
+ Hits        84970    85201     +231     
- Misses      19815    19835      +20     
- Partials     6484     6493       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@evandance
evandance force-pushed the refactor/error-classification branch from 57d80e1 to b929eaa Compare August 10, 2026 10:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/common/runner_args_test.go`:
- Around line 46-60: Extend the validation-error assertions in the runner
argument test using errs.ProblemOf(err) to verify Category is
errs.CategoryValidation and Subtype is errs.SubtypeInvalidArgument. Retain
errors.As into *errs.ValidationError and the existing Param assertion for the
first stray word, since ProblemOf does not expose Param.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c5f45baf-4411-4997-83b4-92b1250bbc00

📥 Commits

Reviewing files that changed from the base of the PR and between 57d80e1 and b929eaa.

📒 Files selected for processing (1)
  • shortcuts/common/runner_args_test.go

Comment thread shortcuts/common/runner_args_test.go
@evandance
evandance force-pushed the refactor/error-classification branch from b929eaa to ccb620a Compare August 10, 2026 10:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
cmd/error_stage_test.go (1)

241-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Probe more than one rejected argument shape.

This walk only detects validators that reject ten arguments. A lower-bound validator, or a custom validator that rejects an empty or single-argument input, can remain unwrapped without failing this test. Probe empty, single, and oversized argument lists, then require typed errors for every rejected probe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/error_stage_test.go` around lines 241 - 249, Expand the
argument-validation probes in the walk around c.Args to test empty,
single-argument, and oversized argument lists instead of only ten arguments. For
every rejected probe, increment the check and require errs.ProblemOf(err) to
succeed, recording c.CommandPath() as unguarded when it does not; preserve the
existing traversal behavior for accepted inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/error_stage_test.go`:
- Around line 359-369: Update the test around normalizeRootError to store the
raw errors passed for stageUserInput and stageCommandBody, then assert errors.Is
for each normalized result against its corresponding raw error. Keep the
existing ProblemOf classification checks and ensure both normalization paths
verify cause preservation.
- Around line 107-120: Update the test around the tamper plugin’s Shutdown
handler to maintain an invocation counter, increment it inside the handler, and
assert after executeWithCapturedOS returns that the counter equals one. Preserve
the existing error-rewrite behavior and exit-code assertions.
- Around line 319-321: Update the assertion in the error-stage test to compare
exit against output.ExitCodeOf(broken), ensuring the rewritten error preserves
the typed category’s exact exit code rather than merely checking that it is not
1.

---

Nitpick comments:
In `@cmd/error_stage_test.go`:
- Around line 241-249: Expand the argument-validation probes in the walk around
c.Args to test empty, single-argument, and oversized argument lists instead of
only ten arguments. For every rejected probe, increment the check and require
errs.ProblemOf(err) to succeed, recording c.CommandPath() as unguarded when it
does not; preserve the existing traversal behavior for accepted inputs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70984ba7-459e-4504-bfcc-ac6047d8a3f1

📥 Commits

Reviewing files that changed from the base of the PR and between b929eaa and ccb620a.

📒 Files selected for processing (1)
  • cmd/error_stage_test.go

Comment thread cmd/error_stage_test.go
Comment thread cmd/error_stage_test.go Outdated
Comment thread cmd/error_stage_test.go Outdated
Getting a flag wrong on a shortcut told the user the tool had broken. Leaving
out one of a pair of mutually required flags, passing both when only one is
allowed, or typing a stray word where a flag belongs all came back as an
internal fault with the exit code reserved for "this should never happen" —
so the most common way to mistype a command counted against the CLI's own
health instead of telling the user what to fix.

The cause was that classification guessed from the text of the error. Cobra
reports these failures as plain sentences, and the dispatcher matched them
against a list of known phrasings; anything unrecognized was assumed to be our
fault. Any check cobra gained, any wording it changed, and any validator added
here landed outside that list silently.

Classification now follows where the failure happened instead of what it says.
Cobra finishes validating the command line before a command body runs, so a
failure before that point is about what was typed, and an unclassified failure
after it is a gap on our side. Positional-argument rejections are typed where
they are produced and now name the offending word and point at --help.

A plugin's shutdown handler also sees the same classification the user gets,
and receives a copy of it, so watching failures cannot alter them.
@evandance
evandance force-pushed the refactor/error-classification branch from ccb620a to 682bfba Compare August 10, 2026 11:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/error_stage_test.go`:
- Around line 244-257: Update the rejecting-validator assertions in the
forEachCommand test to require errs.CategoryValidation and
errs.SubtypeInvalidArgument, rather than accepting any errs.Problem. When err
can be unwrapped as *errs.ValidationError, use errors.As to assert its Param
field as required; preserve the existing tracking of unclassified errors and
command paths.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b013e74-09f6-49a7-a03a-120400ba3ccf

📥 Commits

Reviewing files that changed from the base of the PR and between ccb620a and 682bfba.

📒 Files selected for processing (2)
  • cmd/error_stage_test.go
  • shortcuts/common/runner_args_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/common/runner_args_test.go

Comment thread cmd/error_stage_test.go
Comment on lines +244 to +257
forEachCommand(root, func(c *cobra.Command) {
if c.Args == nil {
return
}
// Feed enough positional words that any bounded validator rejects them.
err := c.Args(c, []string{"stray1", "stray2", "stray3", "stray4", "stray5",
"stray6", "stray7", "stray8", "stray9", "stray10"})
if err == nil {
return
}
checked++
if _, ok := errs.ProblemOf(err); !ok {
unguarded = append(unguarded, c.CommandPath())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the validation classification for every rejecting Args validator.

Lines 255-257 accept any typed errs.Problem. An errs.CategoryInternal error would pass this test and hide the dispatch regression.

Assert errs.CategoryValidation and errs.SubtypeInvalidArgument for each rejecting validator. Also assert Param through errors.As to *errs.ValidationError when the validator returns that type.

As per coding guidelines, error-path tests must assert typed metadata and cause preservation. Based on learnings, errs.ProblemOf does not expose Param; use errors.As to inspect *errs.ValidationError.Param.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/error_stage_test.go` around lines 244 - 257, Update the
rejecting-validator assertions in the forEachCommand test to require
errs.CategoryValidation and errs.SubtypeInvalidArgument, rather than accepting
any errs.Problem. When err can be unwrapped as *errs.ValidationError, use
errors.As to assert its Param field as required; preserve the existing tracking
of unclassified errors and command paths.

Sources: Coding guidelines, Learnings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant