-
Notifications
You must be signed in to change notification settings - Fork 620
fix: migrate task shortcut errors from bare fmt.Errorf to structured … #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package task | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/larksuite/cli/internal/output" | ||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
Comment on lines
+16
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the expected message substrings in both structured-error tests. The tables already carry 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 |
||
|
|
||
| func TestBuildTaskUpdateBody_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: "", | ||
| wantCode: output.ExitValidation, | ||
| wantType: "validation", | ||
| wantSubstr: "--data must be a valid JSON object", | ||
| }, | ||
| { | ||
| name: "no fields to update returns ErrValidation", | ||
| data: "", | ||
| summary: "", | ||
| wantCode: output.ExitValidation, | ||
| wantType: "validation", | ||
| wantSubstr: "no fields to update", | ||
| }, | ||
| } | ||
|
|
||
| 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("due", "", "") | ||
|
|
||
| runtime := &common.RuntimeContext{Cmd: cmd} | ||
| _, err := buildTaskUpdateBody(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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -76,7 +76,7 @@ var UpdateTask = common.Shortcut{ | |||||||||||||||||||||||||||
| var result map[string]interface{} | ||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||
| if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil { | ||||||||||||||||||||||||||||
| return fmt.Errorf("failed to parse response for task %s: %v", taskId, parseErr) | ||||||||||||||||||||||||||||
| return output.Errorf(output.ExitAPI, "api_error", "failed to parse response for task %s: %v", taskId, parseErr) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -133,7 +133,7 @@ func buildTaskUpdateBody(runtime *common.RuntimeContext) (map[string]interface{} | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
134
to
137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate the file and understand its structure
find . -type f -name "task_update.go" | head -5Repository: larksuite/cli Length of output: 89 🏁 Script executed: # Check file size to determine how to read it
wc -l shortcuts/task/task_update.goRepository: 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 -180Repository: larksuite/cli Length of output: 6347 Add nil check after JSON unmarshal to prevent panic on When 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| // If data is provided, assume keys are update fields | ||||||||||||||||||||||||||||
| for k := range taskObj { | ||||||||||||||||||||||||||||
|
|
@@ -158,7 +158,7 @@ func buildTaskUpdateBody(runtime *common.RuntimeContext) (map[string]interface{} | |||||||||||||||||||||||||||
| if dueStr := runtime.Str("due"); dueStr != "" { | ||||||||||||||||||||||||||||
| dueObj, err := parseTaskTime(dueStr) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to parse due time: %v", err) | ||||||||||||||||||||||||||||
| return nil, output.ErrValidation("failed to parse due time: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| taskObj["due"] = dueObj | ||||||||||||||||||||||||||||
| if !contains(updateFields, "due") { | ||||||||||||||||||||||||||||
|
|
@@ -167,7 +167,7 @@ func buildTaskUpdateBody(runtime *common.RuntimeContext) (map[string]interface{} | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if len(updateFields) == 0 { | ||||||||||||||||||||||||||||
| return nil, fmt.Errorf("no fields to update") | ||||||||||||||||||||||||||||
| return nil, output.ErrValidation("no fields to update") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return map[string]interface{}{ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.