Skip to content

fix: migrate task shortcut errors from bare fmt.Errorf to structured …#740

Open
Zhang-986 wants to merge 1 commit intolarksuite:mainfrom
Zhang-986:fix/task-structured-errors
Open

fix: migrate task shortcut errors from bare fmt.Errorf to structured …#740
Zhang-986 wants to merge 1 commit intolarksuite:mainfrom
Zhang-986:fix/task-structured-errors

Conversation

@Zhang-986
Copy link
Copy Markdown

@Zhang-986 Zhang-986 commented May 1, 2026

Summary

Migrated 16 error paths in the task management shortcuts (shortcuts/task/) from bare fmt.Errorf to structured output.Errorf/output.ErrValidation, enabling AI agent consumers (Claude Code, Cursor, Gemini CLI) to programmatically parse and recover from errors via the JSON error envelope contract.

Per AGENTS.md: "RunE functions must return output.Errorf / output.ErrWithHint — never bare fmt.Errorf. AI agents parse stderr as JSON; bare errors break this contract."

Changes

Error migration (16 paths across 4 files)

task_util.go (2 paths — validation errors):

  • parseRelativeTime: invalid format → output.ErrValidation
  • parseRelativeTime: unknown unit → output.ErrValidation

shortcuts.go (4 paths — 3 validation + 1 API):

  • buildTaskCreateBody: invalid --data JSON → output.ErrValidation
  • buildTaskCreateBody: invalid --due time → output.ErrValidation
  • buildTaskCreateBody: missing summary → output.ErrValidation
  • CreateTask.Execute: response parse failure → output.Errorf(ExitAPI, "api_error", ...)

task_update.go (4 paths — 3 validation + 1 API):

  • UpdateTask.Execute: response parse failure → output.Errorf(ExitAPI, "api_error", ...)
  • buildTaskUpdateBody: invalid --data JSON → output.ErrValidation
  • buildTaskUpdateBody: invalid --due time → output.ErrValidation
  • buildTaskUpdateBody: no fields to update → output.ErrValidation

task_query_helpers.go (6 paths — all validation):

  • parseTimeRangeMillis: invalid start/end timestamp → output.ErrValidation
  • parseTimeRangeMillis: reversed time range → output.ErrValidation
  • parseTimeRangeRFC3339: invalid start/end timestamp → output.ErrValidation
  • parseTimeRangeRFC3339: reversed time range → output.ErrValidation

Test additions

  • task_util_test.go: Added TestParseRelativeTime_StructuredErrors — verifies *output.ExitError type, exit code, and error detail type.
  • task_body_test.go (new): Added TestBuildTaskCreateBody_StructuredErrors and TestBuildTaskUpdateBody_StructuredErrors — validates structured error output for invalid JSON data, missing summary, and no-fields-to-update cases.
  • task_query_helpers_test.go: Enhanced existing tests with *output.ExitError type assertions for reversed time range errors.

Test plan

  • go test ./shortcuts/task/... -race — all pass
  • go vet ./shortcuts/task/... — clean
  • gofmt -l shortcuts/task/ — no output
  • go mod tidy — no changes
  • golangci-lint run --new-from-rev=origin/main ./shortcuts/task/... — 0 issues
  • Dry-run E2E: not required (internal refactor, no shortcut flag/param changes)
  • Live E2E: not required (error behavior is structurally identical, just wrapped in ExitError)

Summary by CodeRabbit

  • Bug Fixes

    • Standardized error handling for input validation (invalid JSON, missing/blank summaries, invalid due dates/times, reversed time ranges, empty updates, invalid relative-time formats) to return consistent validation errors.
    • API response parsing failures now produce structured API errors instead of generic messages.
  • Tests

    • Added unit tests asserting structured error types and metadata for validation, time-range, relative-time, and API parsing failure scenarios.

Copilot AI review requested due to automatic review settings May 1, 2026 11:01
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 1, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added domain/task PR touches the task domain size/M Single-domain feat or fix with limited business impact labels May 1, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

The task shortcuts module standardizes error handling by converting parse/validation failures into structured output exit errors: validation failures now return output.ErrValidation(...) and API JSON parse failures return output.Errorf(output.ExitAPI, "api_error", ...). Tests were added to assert these structured error behaviors.

Changes

Cohort / File(s) Summary
Core command files
shortcuts/task/shortcuts.go, shortcuts/task/task_update.go
Replace generic fmt.Errorf errors with structured output.ErrValidation(...) for input validation (e.g., --data, due, missing fields) and with output.Errorf(output.ExitAPI, "api_error", ...) for API JSON parse failures; add internal/output import.
Query & util helpers
shortcuts/task/task_query_helpers.go, shortcuts/task/task_util.go
Convert time-range and relative-time parse errors to output.ErrValidation(...), adjusting error messages and import usage.
Unit tests for error behavior
shortcuts/task/task_body_test.go, shortcuts/task/task_query_helpers_test.go, shortcuts/task/task_util_test.go
Add/modify tests to assert returned errors are *output.ExitError, verify Code == output.ExitValidation, and check Detail.Type == "validation" where applicable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • LuckyTerry
  • liangshuo-1
  • tengchengwei

"I nibble bugs and tidy trails of code,
Turn vague errors into a structured ode,
Validation whispers, API faults disclosed,
Tests stand guard where messed-up inputs goad,
A carrot-coded cheer for cleaner mode!" 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change—migrating task shortcut errors from bare fmt.Errorf to structured error handling—though it is truncated with ellipsis.
Description check ✅ Passed The description is comprehensive and complete, covering all required sections: clear summary, detailed change breakdown, comprehensive test plan with execution results, and explicit E2E verification decisions.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates error returns in the task shortcuts (shortcuts/task/) from bare fmt.Errorf to structured internal/output errors (output.Errorf / output.ErrValidation) to preserve the CLI’s JSON error envelope contract for AI agent consumers.

Changes:

  • Replaced validation errors with output.ErrValidation in task body/time parsing helpers.
  • Replaced API response unmarshal errors with output.Errorf(output.ExitAPI, "api_error", ...).
  • Added/updated tests to assert returned errors are *output.ExitError with the expected exit codes and detail types.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
shortcuts/task/task_util.go Converts parseRelativeTime failures to structured validation errors.
shortcuts/task/shortcuts.go Converts create-body validation errors + create response parse errors to structured output errors.
shortcuts/task/task_update.go Converts update-body validation errors + update response parse errors to structured output errors.
shortcuts/task/task_query_helpers.go Converts time-range parsing validation failures to structured validation errors.
shortcuts/task/task_util_test.go Adds structured-error assertions for parseRelativeTime.
shortcuts/task/task_body_test.go Adds new structured-error tests for create/update body builders.
shortcuts/task/task_query_helpers_test.go Adds structured-error assertions for reversed time-range cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread shortcuts/task/task_query_helpers_test.go
Comment on lines +24 to +37
tests := []struct {
name string
input string
wantCode int
wantType string
wantSubstr string
}{
{
name: "invalid format returns ErrValidation",
input: "not-relative",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "invalid relative time format",
},
Comment thread shortcuts/task/task_body_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
shortcuts/task/task_query_helpers_test.go (1)

257-286: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mirror the structured validation assertion here.

The RFC3339 case only checks the exit code. Please also assert Detail.Type == "validation" like the millis case so this path is covered by the same contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/task/task_query_helpers_test.go` around lines 257 - 286, In the
parseTimeRangeRFC3339 test, extend the validation for the "reversed range fails
fast" case to mirror the millis-case assertions: after asserting the error is an
*output.ExitError and that exitErr.Code == output.ExitValidation (within the
t.Run for parseTimeRangeRFC3339), also assert exitErr.Detail.Type ==
"validation" so the RFC3339 path enforces the same contract; reference
parseTimeRangeRFC3339, output.ExitError, output.ExitValidation, and
exitErr.Detail.Type when adding this assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/task/task_body_test.go`:
- Around line 16-76: Add assertions in TestBuildTaskCreateBody_StructuredErrors
to verify the error message contains the expected substring (tt.wantSubstr).
After you obtain exitErr from buildTaskCreateBody, assert that
exitErr.Detail.Message (or exitErr.Error() if Message is empty) contains
tt.wantSubstr using strings.Contains; reference the test name
TestBuildTaskCreateBody_StructuredErrors, the table field wantSubstr, and the
error variable exitErr to locate where to add the check so the structured-error
tests fail on message regressions.

In `@shortcuts/task/task_util_test.go`:
- Around line 23-62: The test TestParseRelativeTime_StructuredErrors defines
wantSubstr but never asserts the human-readable message; update the test to
assert that exitErr.Detail.Message (the error text on the structured detail)
contains tt.wantSubstr using strings.Contains and t.Errorf when it does not, so
the validation message is checked for the expected substring after the existing
non-nil and Type assertions around parseRelativeTime.

---

Outside diff comments:
In `@shortcuts/task/task_query_helpers_test.go`:
- Around line 257-286: In the parseTimeRangeRFC3339 test, extend the validation
for the "reversed range fails fast" case to mirror the millis-case assertions:
after asserting the error is an *output.ExitError and that exitErr.Code ==
output.ExitValidation (within the t.Run for parseTimeRangeRFC3339), also assert
exitErr.Detail.Type == "validation" so the RFC3339 path enforces the same
contract; reference parseTimeRangeRFC3339, output.ExitError,
output.ExitValidation, and exitErr.Detail.Type when adding this assertion.
🪄 Autofix (Beta)

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

Run ID: 2819e420-0d47-4916-ba99-fe6033c41dbd

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb71c6 and d534423.

📒 Files selected for processing (7)
  • shortcuts/task/shortcuts.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_update.go
  • shortcuts/task/task_util.go
  • shortcuts/task/task_util_test.go

Comment on lines +16 to +76
func TestBuildTaskCreateBody_StructuredErrors(t *testing.T) {
tests := []struct {
name string
data string
summary string
wantCode int
wantType string
wantSubstr string
}{
{
name: "invalid JSON data returns ErrValidation",
data: "not-json",
summary: "test",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "--data must be a valid JSON object",
},
{
name: "missing summary returns ErrValidation",
data: "",
summary: "",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "task summary is required",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().String("data", tt.data, "")
cmd.Flags().String("summary", tt.summary, "")
cmd.Flags().String("description", "", "")
cmd.Flags().String("assignee", "", "")
cmd.Flags().String("follower", "", "")
cmd.Flags().String("due", "", "")
cmd.Flags().String("tasklist-id", "", "")
cmd.Flags().String("idempotency-key", "", "")

runtime := &common.RuntimeContext{Cmd: cmd}
_, err := buildTaskCreateBody(runtime)
if err == nil {
t.Fatal("expected error, got nil")
}

var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *output.ExitError; error = %v", err, err)
}
if exitErr.Code != tt.wantCode {
t.Errorf("exit code = %d, want %d", exitErr.Code, tt.wantCode)
}
if exitErr.Detail == nil {
t.Fatal("expected non-nil error detail")
}
if exitErr.Detail.Type != tt.wantType {
t.Errorf("error type = %q, want %q", exitErr.Detail.Type, tt.wantType)
}
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the expected message substrings in both structured-error tests.

The tables already carry wantSubstr, but the assertions only check type/code. Please assert the error text as well so a message regression doesn't pass unnoticed.

Suggested fix
 import (
 	"errors"
+	"strings"
 	"testing"
@@
 			if exitErr.Detail.Type != tt.wantType {
 				t.Errorf("error type = %q, want %q", exitErr.Detail.Type, tt.wantType)
 			}
+			if !strings.Contains(err.Error(), tt.wantSubstr) {
+				t.Errorf("error = %q, want substring %q", err.Error(), tt.wantSubstr)
+			}
 		})
 	}
 }

Also applies to: 78-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/task/task_body_test.go` around lines 16 - 76, Add assertions in
TestBuildTaskCreateBody_StructuredErrors to verify the error message contains
the expected substring (tt.wantSubstr). After you obtain exitErr from
buildTaskCreateBody, assert that exitErr.Detail.Message (or exitErr.Error() if
Message is empty) contains tt.wantSubstr using strings.Contains; reference the
test name TestBuildTaskCreateBody_StructuredErrors, the table field wantSubstr,
and the error variable exitErr to locate where to add the check so the
structured-error tests fail on message regressions.

Comment thread shortcuts/task/task_util_test.go
@Zhang-986 Zhang-986 force-pushed the fix/task-structured-errors branch from 07765cd to 0f3be77 Compare May 1, 2026 11:35
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@shortcuts/task/task_update.go`:
- Around line 134-137: After unmarshalling dataStr into taskObj using
json.Unmarshal in the block that checks runtime.Str("data"), add a nil-check for
taskObj and either initialize it to an empty map (e.g., taskObj =
make(map[string]interface{})) or return a validation error if the caller passed
JSON null; this prevents panics when later code assigns taskObj["summary"],
taskObj["description"], and taskObj["due"]. Locate the json.Unmarshal call and
runtime.Str("data") check and ensure taskObj is non-nil before any field
assignments, preserving the existing output.ErrValidation error behavior on
malformed JSON.
🪄 Autofix (Beta)

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

Run ID: c87f24c0-be2a-44a6-820a-f82f562fb80f

📥 Commits

Reviewing files that changed from the base of the PR and between 07765cd and 0f3be77.

📒 Files selected for processing (7)
  • shortcuts/task/shortcuts.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_update.go
  • shortcuts/task/task_util.go
  • shortcuts/task/task_util_test.go
✅ Files skipped from review due to trivial changes (2)
  • shortcuts/task/task_util_test.go
  • shortcuts/task/task_query_helpers.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • shortcuts/task/task_util.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go

Comment on lines 134 to 137
if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &taskObj); err != nil {
return nil, fmt.Errorf("--data must be a valid JSON object: %v", err)
return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and understand its structure
find . -type f -name "task_update.go" | head -5

Repository: larksuite/cli

Length of output: 89


🏁 Script executed:

# Check file size to determine how to read it
wc -l shortcuts/task/task_update.go

Repository: larksuite/cli

Length of output: 91


🏁 Script executed:

# Read the file to see the context around lines 134-137
cat -n shortcuts/task/task_update.go | head -180

Repository: larksuite/cli

Length of output: 6347


Add nil check after JSON unmarshal to prevent panic on --data null.

When --data null is provided, json.Unmarshal sets taskObj to nil. Subsequent field assignments at lines 145, 152, and 163 (taskObj["summary"], taskObj["description"], taskObj["due"]) will panic on a nil map.

Suggested fix
 if dataStr := runtime.Str("data"); dataStr != "" {
   if err := json.Unmarshal([]byte(dataStr), &taskObj); err != nil {
     return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
   }
+  if taskObj == nil {
+    return nil, output.ErrValidation("--data must be a non-null JSON object")
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &taskObj); err != nil {
return nil, fmt.Errorf("--data must be a valid JSON object: %v", err)
return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
}
if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &taskObj); err != nil {
return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
}
if taskObj == nil {
return nil, output.ErrValidation("--data must be a non-null JSON object")
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/task/task_update.go` around lines 134 - 137, After unmarshalling
dataStr into taskObj using json.Unmarshal in the block that checks
runtime.Str("data"), add a nil-check for taskObj and either initialize it to an
empty map (e.g., taskObj = make(map[string]interface{})) or return a validation
error if the caller passed JSON null; this prevents panics when later code
assigns taskObj["summary"], taskObj["description"], and taskObj["due"]. Locate
the json.Unmarshal call and runtime.Str("data") check and ensure taskObj is
non-nil before any field assignments, preserving the existing
output.ErrValidation error behavior on malformed JSON.

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

Labels

domain/task PR touches the task domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants