From 9d4233bfe32c19a524efbbd6968281153040c526 Mon Sep 17 00:00:00 2001 From: WJzz1 <1515065785@qq.com> Date: Mon, 25 May 2026 12:08:18 +0800 Subject: [PATCH] fix(contact): add actionable hint when fanout search all-fail with no API code (#1054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In buildFanoutResponse, when every fanout query fails AND the first failure has no Lark API code (i.e. transport, parse, panic, or context-cancel), the returned ExitError was carrying an empty Hint. This is the only output.ErrWithHint call in shortcuts/ that ships an empty hint. AGENTS.md states: "every error message you write will be parsed by an AI to decide its next action. Make errors structured, actionable, and specific." An empty hint gives the agent nothing to do. Populate the hint with the actionable next step for this branch — retry, and if it persists, narrow --queries to a single term to isolate the failing input. The companion test exercises the no-code path and asserts the hint is non-empty and mentions "retry". Co-authored-by: Wang-Yeah623 --- .../contact/contact_search_user_fanout.go | 6 +++- shortcuts/contact/contact_search_user_test.go | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/shortcuts/contact/contact_search_user_fanout.go b/shortcuts/contact/contact_search_user_fanout.go index ec1d5e84..e3dd8d40 100644 --- a/shortcuts/contact/contact_search_user_fanout.go +++ b/shortcuts/contact/contact_search_user_fanout.go @@ -176,7 +176,11 @@ func buildFanoutResponse(queries []string, results []fanoutResult) (*fanoutRespo if firstErrCode != 0 { return nil, output.ErrAPI(firstErrCode, msg, "") } - return nil, output.ErrWithHint(output.ExitInternal, "fanout", msg, "") + // No structured API code — the failure was transport, parse, panic, or + // cancellation. Suggest the actionable next step rather than shipping + // an empty hint that would leave the calling agent with nothing to do. + return nil, output.ErrWithHint(output.ExitInternal, "fanout", msg, + "retry the command; if it persists, narrow --queries to a single term to isolate the failing input") } return out, nil } diff --git a/shortcuts/contact/contact_search_user_test.go b/shortcuts/contact/contact_search_user_test.go index a3fd4e1a..5e14f197 100644 --- a/shortcuts/contact/contact_search_user_test.go +++ b/shortcuts/contact/contact_search_user_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -18,6 +19,7 @@ import ( "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/httpmock" + "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" "github.com/spf13/cobra" ) @@ -1133,6 +1135,33 @@ func TestFanoutAssemble_AllFailed_ReturnsError(t *testing.T) { } } +// When all queries fail with no structured Lark API code (transport, parse, +// panic, ctx-canceled), the returned ExitError must carry an actionable +// hint so the calling agent has a next step to try instead of giving up. +func TestFanoutAssemble_AllFailed_NoCode_HasActionableHint(t *testing.T) { + results := []fanoutResult{ + {Index: 0, Query: "alice", ErrMsg: "transport: connection refused"}, + {Index: 1, Query: "bob", ErrMsg: "transport: timeout"}, + } + _, err := buildFanoutResponse([]string{"alice", "bob"}, results) + if err == nil { + t.Fatalf("expected error when all queries failed") + } + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T", err) + } + if exitErr.Detail == nil { + t.Fatalf("expected Detail, got nil") + } + if exitErr.Detail.Hint == "" { + t.Errorf("expected non-empty Hint so agents have a next step; got empty") + } + if !strings.Contains(exitErr.Detail.Hint, "retry") { + t.Errorf("hint should suggest retry as the first action; got %q", exitErr.Detail.Hint) + } +} + // Codes from the first failure must propagate through output.ErrAPI so the // CLI's exit-code classifier sees the real signal (e.g., 99991663 rate limit) // instead of 0, which would mean "success" in the Lark protocol.