Skip to content

fix: improve identity and rate limit recovery - #2193

Open
zhaojunlin0405 wants to merge 10 commits into
mainfrom
fix/identity-rate-limit-recovery
Open

fix: improve identity and rate limit recovery#2193
zhaojunlin0405 wants to merge 10 commits into
mainfrom
fix/identity-rate-limit-recovery

Conversation

@zhaojunlin0405

@zhaojunlin0405 zhaojunlin0405 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Improve machine-readable recovery for unsupported identities and platform rate limits. The CLI now preserves structured rate-limit metadata across API, pagination, shortcut, authentication, configuration, and TAT paths without automatically replaying requests.

Changes

  • Add the identity_not_supported error subtype and apply it to known bot/user compatibility checks.
  • Classify HTTP 429 and business rate-limit responses consistently, including Retry-After, retryability, request IDs, and quota semantics.
  • Preserve pagination progress and the underlying typed cause when later pages fail.
  • Avoid permanently caching retryable TAT failures while still coalescing concurrent token fetches and caching stable results.
  • Add contract, concurrency, pagination, response-classification, and shortcut regression tests.

Test Plan

  • make unit-test
  • go vet ./...
  • Harness validation: build, unit, integration, convention, and security checks passed
  • Sandbox E2E: 3/3 passed
  • Acceptance review: 5/5 scenarios passed
  • Security code review passed

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Added resumable pagination errors that preserve completed pages and continuation tokens.
    • Added structured rate-limit details, including retry timing, request IDs, and retry guidance.
    • Added clearer validation errors for unsupported user or bot identity selections.
  • Bug Fixes

    • Pagination failures no longer write raw error responses to standard output.
    • Improved handling of malformed responses, authorization failures, and streaming errors.
    • Credential and API requests now handle retryable rate limits more reliably.
  • Documentation

    • Documented identity validation, rate-limit behavior, and pagination recovery details.

@zhaojunlin0405 zhaojunlin0405 added the bug Something isn't working label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change centralizes API error classification, adds typed rate-limit metadata, introduces resumable pagination errors, standardizes identity validation errors, and coordinates concurrent TAT resolution.

Changes

Error contracts and pagination

Layer / File(s) Summary
Typed error contracts
errs/*
Adds PaginationError, retry-after fields, and the identity_not_supported subtype.
Pagination execution and CLI behavior
internal/client/client.go, cmd/api/*, cmd/service/*
Pagination preserves typed causes, completed pages, cursors, failed-page exclusion, and cancellation state. CLI tests require no stdout on failures.

Rate-limit classification and integrations

Layer / File(s) Summary
Shared response classification
internal/errclass/*, internal/client/api_response.go, internal/client/response.go
Classifies HTTP 429 and business rate limits with validated codes, log IDs, retry metadata, bounded bodies, and replay-safety hints.
Command and credential adoption
cmd/auth/*, cmd/event/*, internal/credential/tat_fetch.go, cmd/config/init_probe.go, shortcuts/common/*
Uses shared classification for authentication, event, bot-info, token fetching, and probe handling.

Identity validation

Layer / File(s) Summary
Identity selection rules
internal/cmdutil/factory.go, internal/cmdutil/factory_test.go
Tracks explicit identity selection and returns structured validation subtypes, parameters, and hints.
Shortcut identity handling
shortcuts/contact/*, shortcuts/drive/*, shortcuts/im/*, shortcuts/mail/*, shortcuts/wiki/*
Replaces generic invalid-argument results with identity-specific validation errors and centralizes Mail user-identity checks.

TAT resolution coordination

Layer / File(s) Summary
Single-flight resolution and caching
internal/credential/default_provider.go, internal/credential/default_provider_test.go
Shares concurrent resolution, caches successful and non-retryable results, retries retryable or canceled results, and propagates resolver panics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ClassifyAPIResponse
  participant ClassifyHTTPRateLimit
  participant APIError
  Caller->>ClassifyAPIResponse: Decode response and inspect status
  ClassifyAPIResponse->>ClassifyHTTPRateLimit: Classify HTTP 429 or business rate limit
  ClassifyHTTPRateLimit->>APIError: Attach code, log ID, retry metadata, and hints
  APIError-->>Caller: Return typed classified error
Loading

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.95% 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 clearly summarizes the main changes to identity handling and rate-limit recovery.
Description check ✅ Passed The description includes all required sections and clearly explains the changes, testing, and related issues.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/identity-rate-limit-recovery

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 domain/ccm PR touches the ccm domain domain/contact PR touches the contact domain domain/im PR touches the im domain domain/mail PR touches the mail domain size/XL Architecture-level or global-impact change labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@9cb2b4d06edd2ddf0f3c1aae6bf482b167c98a05

🧩 Skill update

npx skills add larksuite/cli#fix/identity-rate-limit-recovery -y -g

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.19481% with 122 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.26%. Comparing base (164d3cc) to head (9cb2b4d).

Files with missing lines Patch % Lines
errs/pagination.go 50.00% 26 Missing and 11 partials ⚠️
internal/errclass/http_rate_limit.go 78.61% 29 Missing and 5 partials ⚠️
internal/client/api_response.go 81.08% 8 Missing and 6 partials ⚠️
internal/recovery/render.go 30.00% 7 Missing ⚠️
shortcuts/mail/mail_forward.go 0.00% 7 Missing ⚠️
cmd/auth/auth.go 72.72% 6 Missing ⚠️
internal/client/client.go 89.47% 4 Missing and 2 partials ⚠️
internal/cmdutil/factory.go 83.33% 3 Missing and 1 partial ⚠️
shortcuts/common/runner.go 85.18% 2 Missing and 2 partials ⚠️
cmd/auth/login.go 66.66% 2 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2193      +/-   ##
==========================================
+ Coverage   76.21%   76.26%   +0.04%     
==========================================
  Files         986      989       +3     
  Lines      104268   104702     +434     
==========================================
+ Hits        79472    79852     +380     
- Misses      18752    18793      +41     
- Partials     6044     6057      +13     

☔ 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.

@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: 11

🧹 Nitpick comments (3)
internal/credential/default_provider_test.go (2)

261-276: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Force the coalescing path before releasing the resolver.

ready.Wait() only proves each goroutine started. It does not prove each goroutine reached resolveTAT before the leader finished. If the leader completes first, later callers read the populated cache and never call the resolver, so calls == 1 still passes. The test therefore passes for both single-flight coalescing and plain caching, and it does not fail if coalescing is removed.

Wait for followers to reach callers-1 before close(release), using the same poll pattern as lines 90-102.

♻️ Proposed change
 	ready.Wait()
 	close(begin)
 	select {
 	case <-started:
 	case <-time.After(2 * time.Second):
 		t.Fatal("timed out waiting for TAT resolution to start")
 	}
+	deadline := time.Now().Add(2 * time.Second)
+	for {
+		p.tatMu.Lock()
+		followers := p.tatFlight.followers
+		p.tatMu.Unlock()
+		if followers == callers-1 {
+			break
+		}
+		if time.Now().After(deadline) {
+			t.Fatalf("in-flight followers = %d, want %d", followers, callers-1)
+		}
+		runtime.Gosched()
+	}
 	close(release)
🤖 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 `@internal/credential/default_provider_test.go` around lines 261 - 276, Update
the concurrent TAT resolution test around resolveTAT to wait until the followers
count reaches callers-1, using the existing polling pattern from the earlier
test, before closing release. Keep the current timeout/failure behavior and only
release the resolver after all follower calls have entered the coalescing path.

38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert typed error metadata, not error identity, on the error paths. All three sites compare with errors.Is only. The caching decision at default_provider.go Line 225 depends on errs.IsRetryable, which reads Problem.Retryable. If resolveTAT later wrapped the resolver error and dropped the typed fields, errors.Is would still pass and the tests would not fail.

  • internal/credential/default_provider_test.go#L38-L40: read the problem with errs.ProblemOf(err) and assert Category == errs.CategoryAPI, Subtype == errs.SubtypeRateLimit, and Retryable == true, in addition to errors.Is.
  • internal/credential/default_provider_test.go#L105-L114: apply the same errs.ProblemOf assertions to each shared-flight outcome, so every follower is proven to receive the retryable classification and not only the same error value.
  • internal/credential/default_provider_test.go#L228-L233: assert Category == errs.CategoryConfig, Subtype == errs.SubtypeInvalidClient, and Retryable == false, which is the property that makes this error cacheable.

Based on the coding guideline "Error-path tests must assert typed metadata through errs.ProblemOf (category, subtype, and param) and verify cause preservation rather than relying only on message substrings."

🤖 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 `@internal/credential/default_provider_test.go` around lines 38 - 40, Update
all three error-path test sites in
internal/credential/default_provider_test.go:38-40, 105-114, and 228-233 to
inspect errs.ProblemOf(err) and assert the required category, subtype, and
retryable metadata in addition to errors.Is. Use API/RateLimit/true for
resolveTAT and shared-flight retryable outcomes, and Config/InvalidClient/false
for the cacheable configuration error; retain cause-preservation checks.

Source: Coding guidelines

internal/errclass/http_rate_limit.go (1)

188-203: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename or collapse resultCode into a boolean helper.

resultCode never returns the response code. It returns 99991400 or 0. Both call sites compare the result to 99991400, so the function is a boolean predicate with a numeric signature. Collapse it into IsBusinessRateLimit to remove the indirection.

♻️ Proposed refactor
-func resultCode(result any) int {
-	resultMap, ok := result.(map[string]any)
-	if !ok {
-		return 0
-	}
-	if exactBusinessRateLimitCode(resultMap["code"]) {
-		return 99991400
-	}
-	return 0
-}
-
 // IsBusinessRateLimit reports whether result carries the exact integer Lark
 // short-term rate-limit code.
 func IsBusinessRateLimit(result any) bool {
-	return resultCode(result) == 99991400
+	resultMap, ok := result.(map[string]any)
+	if !ok {
+		return false
+	}
+	return exactBusinessRateLimitCode(resultMap["code"])
 }

The ClassifyHTTPRateLimit call site at line 35 then becomes businessRateLimit := IsBusinessRateLimit(result).

🤖 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 `@internal/errclass/http_rate_limit.go` around lines 188 - 203, Collapse
resultCode into IsBusinessRateLimit by moving its map assertion and
exactBusinessRateLimitCode check into the boolean helper, returning true only
for the exact business rate-limit code and false otherwise. Update the
ClassifyHTTPRateLimit call site and any other callers to use IsBusinessRateLimit
directly, then remove the numeric resultCode helper and comparisons against
99991400.
🤖 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/auth/auth.go`:
- Around line 86-91: Update both response handlers in cmd/auth/auth.go (lines
86-91 and 168-182): replace each bare parse-error fmt.Errorf with
errs.NewInternalError(errs.SubtypeInvalidResponse, ...).WithCause(err), remove
the unused {"code","msg"} result projection, and pass nil to
client.ClassifyRateLimitResponse in both sites.

In `@errs/pagination.go`:
- Around line 109-113: Update buildJSON in errs/pagination.go at lines 109-113
so every typed fallback, including reserved-field collisions and encoding
failures, retains e.Cause as the wrapped cause while incorporating the collision
or encoding diagnostic into the internal error message. In
errs/pagination_test.go lines 126-150, update the error-path assertions to
verify metadata through errs.ProblemOf, assert SubtypeInvalidResponse, and
confirm errors.Is/errors.As can recover inner.

In `@internal/client/rate_limit.go`:
- Around line 43-62: Unify message selection for business code 99991400 across
the HTTP 429 path in errclass.ClassifyHTTPRateLimit and the shared handling in
the rate-limit flow. Choose either consistently preserving the upstream
result["msg"] or consistently using "request rate limit exceeded", then remove
the conflicting message-selection logic so transport status cannot change the
resulting message.
- Around line 75-93: Update ClassifyRateLimitResponse so
errclass.ParseRateLimitJSON is only called when the response status or
caller-provided classification indicates a rate-limit response, avoiding
reparsing successful payloads; preserve the existing result/classified handling
for malformed rate-limit bodies and the documented trust trade-off for caller
projections.

In `@internal/cmdutil/factory.go`:
- Line 46: Update the comment for identityExplicit to state that it is set
whenever the user explicitly selects any non-auto --as value, including unknown
or invalid identities, so CheckIdentity can attribute those values to the flag.

In `@internal/credential/default_provider.go`:
- Around line 178-227: The resolveTAT implementation must avoid caching results
caused by context cancellation or timeout, even when errs.IsRetryable reports
them as non-retryable; update the cache decision around tatResolver accordingly.
In the existing tatFlight follower path, wait on either flight.done or the
follower’s ctx.Done(), returning the follower’s context error when its context
is canceled while preserving normal flight result sharing otherwise. Scope any
cached result to the leader context as needed so cancellation results cannot be
reused by later ResolveToken calls.

In `@internal/errclass/http_rate_limit_test.go`:
- Around line 49-56: Extend the error-path assertions to validate typed metadata
through errs.ProblemOf while preserving errors.As checks for RetryAfterSeconds:
in internal/errclass/http_rate_limit_test.go:49-56,
internal/client/rate_limit_test.go:123-129,
internal/client/response_test.go:385-398, cmd/auth/auth_test.go:508-518, and
shortcuts/common/call_api_typed_test.go:117-129 assert CategoryAPI and
SubtypeRateLimit; in shortcuts/common/runner_botinfo_test.go:177-215 assert
CategoryAPI for all cases and SubtypeRateLimit for rate-limit cases; in
shortcuts/common/runner_botinfo_test.go:263-265 assert CategoryAPI and a
non-empty subtype. Do not assert Param on APIError.

In `@internal/errclass/http_rate_limit.go`:
- Around line 241-249: Rate-limit guidance construction is duplicated across
internal/errclass/http_rate_limit.go lines 241-249 and
internal/client/rate_limit.go lines 65-73, risking mismatched deduplication.
Export mergeRateLimitHint as MergeRateLimitHint in
internal/errclass/http_rate_limit.go lines 241-249, add an exported
RateLimitGuidance(seconds) constructor for the guidance text at line 63, and
update internal/client/rate_limit.go lines 65-73 and 60 to remove local
duplication and call these errclass helpers.

In `@shortcuts/im/coverage_additional_test.go`:
- Around line 329-332: Update the error assertions in the test around
errs.ProblemOf to validate the complete typed contract: retain the
SubtypeIdentityNotSupported check and also assert the validation category and
--user-id parameter values produced by shortcuts/im/helpers.go. Keep the
existing failure reporting style and use the corresponding errs symbols for
these fields.

In `@shortcuts/mail/large_attachment_test.go`:
- Around line 1225-1228: Update
TestMailRequireUserOpenIDChecksIdentityBeforeConfiguredUser to assert the
returned problem’s Hint field through the existing assertMailIdentityProblem
helper, ensuring the bot-identity recovery hint is verified in addition to the
category and subtype.

In `@shortcuts/mail/template_compose_test.go`:
- Line 10: Replace the os filesystem usage in the test fixture setup, including
the os import and each os.WriteFile call, with the applicable internal/vfs write
helper. Keep the existing fixture paths and contents unchanged while ensuring
all filesystem operations in this Go test use the repository filesystem API.

---

Nitpick comments:
In `@internal/credential/default_provider_test.go`:
- Around line 261-276: Update the concurrent TAT resolution test around
resolveTAT to wait until the followers count reaches callers-1, using the
existing polling pattern from the earlier test, before closing release. Keep the
current timeout/failure behavior and only release the resolver after all
follower calls have entered the coalescing path.
- Around line 38-40: Update all three error-path test sites in
internal/credential/default_provider_test.go:38-40, 105-114, and 228-233 to
inspect errs.ProblemOf(err) and assert the required category, subtype, and
retryable metadata in addition to errors.Is. Use API/RateLimit/true for
resolveTAT and shared-flight retryable outcomes, and Config/InvalidClient/false
for the cacheable configuration error; retain cause-preservation checks.

In `@internal/errclass/http_rate_limit.go`:
- Around line 188-203: Collapse resultCode into IsBusinessRateLimit by moving
its map assertion and exactBusinessRateLimitCode check into the boolean helper,
returning true only for the exact business rate-limit code and false otherwise.
Update the ClassifyHTTPRateLimit call site and any other callers to use
IsBusinessRateLimit directly, then remove the numeric resultCode helper and
comparisons against 99991400.
🪄 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: 09a38343-c21c-4d9a-8fde-f57374ecbe42

📥 Commits

Reviewing files that changed from the base of the PR and between ebdeda8 and 89708f9.

📒 Files selected for processing (58)
  • cmd/api/api_paginate_test.go
  • cmd/api/api_test.go
  • cmd/auth/auth.go
  • cmd/auth/auth_test.go
  • cmd/config/init_probe.go
  • cmd/config/init_probe_test.go
  • cmd/event/runtime.go
  • cmd/event/runtime_test.go
  • cmd/service/service_paginate_test.go
  • cmd/service/service_test.go
  • errs/ERROR_CONTRACT.md
  • errs/marshal_test.go
  • errs/pagination.go
  • errs/pagination_test.go
  • errs/subtypes.go
  • errs/types.go
  • errs/types_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/rate_limit.go
  • internal/client/rate_limit_test.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_test.go
  • internal/credential/default_provider.go
  • internal/credential/default_provider_test.go
  • internal/credential/tat_fetch.go
  • internal/credential/tat_fetch_test.go
  • internal/errclass/http_rate_limit.go
  • internal/errclass/http_rate_limit_test.go
  • shortcuts/common/call_api_typed_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_botinfo_test.go
  • shortcuts/contact/contact_get_user.go
  • shortcuts/contact/contact_get_user_test.go
  • shortcuts/drive/drive_member_add.go
  • shortcuts/drive/drive_member_add_test.go
  • shortcuts/im/builders_test.go
  • shortcuts/im/coverage_additional_test.go
  • shortcuts/im/helpers.go
  • shortcuts/im/im_chat_create.go
  • shortcuts/im/im_chat_list.go
  • shortcuts/im/im_chat_list_test.go
  • shortcuts/im/im_chat_messages_list.go
  • shortcuts/mail/helpers.go
  • shortcuts/mail/large_attachment.go
  • shortcuts/mail/large_attachment_test.go
  • shortcuts/mail/mail_forward.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/mail/template_compose.go
  • shortcuts/mail/template_compose_test.go
  • shortcuts/wiki/wiki_list_copy_test.go
  • shortcuts/wiki/wiki_member_helpers.go
  • shortcuts/wiki/wiki_member_test.go
  • shortcuts/wiki/wiki_node_create.go
  • shortcuts/wiki/wiki_node_create_test.go
  • shortcuts/wiki/wiki_node_list.go

Comment thread cmd/auth/auth.go Outdated
Comment thread errs/pagination.go Outdated
Comment thread internal/client/rate_limit.go Outdated
Comment thread internal/client/rate_limit.go Outdated
Comment thread internal/cmdutil/factory.go Outdated
Comment thread internal/errclass/http_rate_limit_test.go
Comment thread internal/errclass/http_rate_limit.go Outdated
Comment thread shortcuts/im/coverage_additional_test.go
Comment thread shortcuts/mail/large_attachment_test.go
Comment thread shortcuts/mail/template_compose_test.go Outdated

@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/auth/auth_test.go (1)

559-571: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use cmdutil.TestFactory for this mocked HTTP test.

This test performs an HTTP-mocked request but constructs lark.NewClient directly. Build the test client through cmdutil.TestFactory(t, config) so the test uses the standard configuration isolation and HTTP setup.

As per coding guidelines, “Use cmdutil.TestFactory(t, config) for test factories and set LARKSUITE_CLI_CONFIG_DIR to t.TempDir() with t.Setenv to isolate configuration state.”

🤖 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/auth/auth_test.go` around lines 559 - 571, Update the mocked HTTP test
around the direct lark.NewClient construction to create the client through
cmdutil.TestFactory(t, config). Configure the factory with the test app
credentials and mocked HTTP transport, and isolate CLI configuration by setting
LARKSUITE_CLI_CONFIG_DIR to t.TempDir() via t.Setenv; preserve the existing
request stub and test behavior.

Sources: Coding guidelines, Learnings

🤖 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 `@internal/client/rate_limit_test.go`:
- Around line 161-165: Extend the error assertions in the test around
ClassifyRateLimitResponse to verify cause preservation by declaring a
*json.SyntaxError target and asserting errors.As(err, &syntaxErr) succeeds. Keep
the existing errs.ProblemOf metadata checks unchanged, ensuring the trailing
JSON parsing error remains discoverable through the returned error.

In `@internal/credential/default_provider_test.go`:
- Around line 307-350: The leader goroutine in the resolveTAT test currently
sends only the error, so it does not verify the resolved token. Update
leaderDone and its producer to transmit both the returned token and error, then
use a bounded receive after closing release to assert the token equals
"leader-token" and the error is nil; retain the timeout to prevent hangs.

In `@internal/errclass/http_rate_limit.go`:
- Around line 60-61: Update the businessRateLimit branch in the API error
classification flow to set the reused apiErr.Subtype and apiErr.Code to the
rate-limit values alongside RateLimitMessage. Add a regression test covering a
pre-existing non-rate-limit *errs.APIError and assert its message, subtype, and
code are all converted to the business rate-limit classification.

---

Nitpick comments:
In `@cmd/auth/auth_test.go`:
- Around line 559-571: Update the mocked HTTP test around the direct
lark.NewClient construction to create the client through cmdutil.TestFactory(t,
config). Configure the factory with the test app credentials and mocked HTTP
transport, and isolate CLI configuration by setting LARKSUITE_CLI_CONFIG_DIR to
t.TempDir() via t.Setenv; preserve the existing request stub and test behavior.
🪄 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: 3ec25d55-3f07-489d-a097-b93e8e43db45

📥 Commits

Reviewing files that changed from the base of the PR and between 89708f9 and ed2e6e9.

📒 Files selected for processing (18)
  • cmd/auth/auth.go
  • cmd/auth/auth_test.go
  • errs/pagination.go
  • errs/pagination_test.go
  • internal/client/rate_limit.go
  • internal/client/rate_limit_test.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/factory.go
  • internal/credential/default_provider.go
  • internal/credential/default_provider_test.go
  • internal/errclass/http_rate_limit.go
  • internal/errclass/http_rate_limit_test.go
  • shortcuts/common/call_api_typed_test.go
  • shortcuts/common/runner_botinfo_test.go
  • shortcuts/im/coverage_additional_test.go
  • shortcuts/mail/large_attachment_test.go
  • shortcuts/mail/template_compose_test.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • internal/client/response.go
  • shortcuts/mail/template_compose_test.go
  • internal/cmdutil/factory.go
  • cmd/auth/auth.go
  • shortcuts/common/call_api_typed_test.go
  • internal/client/rate_limit.go
  • shortcuts/common/runner_botinfo_test.go
  • internal/errclass/http_rate_limit_test.go
  • internal/client/response_test.go
  • shortcuts/im/coverage_additional_test.go
  • internal/credential/default_provider.go
  • errs/pagination_test.go
  • shortcuts/mail/large_attachment_test.go

Comment thread internal/client/rate_limit_test.go Outdated
Comment thread internal/credential/default_provider_test.go Outdated
Comment thread internal/errclass/http_rate_limit.go

@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 `@internal/errclass/http_rate_limit_test.go`:
- Around line 162-174: Update
TestClassifyHTTPRateLimit_ReclassifiesExistingAPIErrorAsBusinessRateLimit to
inspect the returned error via errs.ProblemOf(err), asserting CategoryAPI and
SubtypeRateLimit. Keep the pointer identity check for in-place mutation, but
remove direct assertions of original.Subtype, original.Code, and
original.Message and do not assert Param on APIError.
🪄 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: 960002f7-8328-4228-9b6d-bdb4ac56b27f

📥 Commits

Reviewing files that changed from the base of the PR and between ed2e6e9 and 87dfbb5.

📒 Files selected for processing (5)
  • cmd/auth/auth_test.go
  • internal/client/rate_limit_test.go
  • internal/credential/default_provider_test.go
  • internal/errclass/http_rate_limit.go
  • internal/errclass/http_rate_limit_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/client/rate_limit_test.go
  • internal/errclass/http_rate_limit.go
  • cmd/auth/auth_test.go
  • internal/credential/default_provider_test.go

Comment on lines +162 to +174
func TestClassifyHTTPRateLimit_ReclassifiesExistingAPIErrorAsBusinessRateLimit(t *testing.T) {
original := errs.NewAPIError(errs.SubtypeUnknown, "original classification").WithCode(12345)
err := ClassifyHTTPRateLimit(http.StatusTooManyRequests, nil, map[string]any{
"code": 99991400,
}, original, time.Now())

if err != original {
t.Fatalf("classification = %T (%v), want original APIError pointer", err, err)
}
if original.Subtype != errs.SubtypeRateLimit || original.Code != 99991400 || original.Message != RateLimitMessage {
t.Fatalf("reclassified API error = %#v, want api/rate_limit code 99991400", original)
}
}

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 typed API error contract through errs.ProblemOf.

This test checks Subtype, Code, and Message directly on original. The pointer check verifies in-place mutation, but it does not exercise the required Problem metadata contract. Read the returned error with errs.ProblemOf(err) and assert CategoryAPI and SubtypeRateLimit. Do not assert Param for *errs.APIError.

As per coding guidelines, error-path tests must assert typed metadata through errs.ProblemOf. Based on learnings, APIError does not expose Param.

Proposed assertion
  if err != original {
    t.Fatalf("classification = %T (%v), want original APIError pointer", err, err)
  }
- if original.Subtype != errs.SubtypeRateLimit || original.Code != 99991400 || original.Message != RateLimitMessage {
+ problem, ok := errs.ProblemOf(err)
+ if !ok || problem.Category != errs.CategoryAPI || problem.Subtype != errs.SubtypeRateLimit {
+   t.Fatalf("problem = %#v, want api/rate_limit", problem)
+ }
+ if original.Code != 99991400 || original.Message != RateLimitMessage {
    t.Fatalf("reclassified API error = %#v, want api/rate_limit code 99991400", original)
  }
🤖 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 `@internal/errclass/http_rate_limit_test.go` around lines 162 - 174, Update
TestClassifyHTTPRateLimit_ReclassifiesExistingAPIErrorAsBusinessRateLimit to
inspect the returned error via errs.ProblemOf(err), asserting CategoryAPI and
SubtypeRateLimit. Keep the pointer identity check for in-place mutation, but
remove direct assertions of original.Subtype, original.Code, and
original.Message and do not assert Param on APIError.

Sources: Coding guidelines, Learnings

@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

Caution

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

⚠️ Outside diff range comments (1)
internal/client/api_response_test.go (1)

195-197: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the concrete trailing-content cause, not just its presence.

errors.Unwrap(err) == nil proves only that some cause is attached. If DecodeSingleJSON were changed to wrap a different error, this test would still pass. The sibling test at lines 179-182 already asserts a concrete *json.SyntaxError.

Assert the specific error that errclass.DecodeSingleJSON returns for trailing content, through errors.Is on the sentinel or errors.As on its type.

The repository guidelines require error-path tests to verify cause preservation rather than relying only on message substrings or presence checks.

💚 Proposed tightening
-	if errors.Unwrap(err) == nil {
-		t.Fatalf("error chain does not preserve trailing-content cause: %v", err)
-	}
+	if !errors.Is(err, errclass.ErrTrailingJSONContent) {
+		t.Fatalf("error chain does not preserve the trailing-content cause: %v", err)
+	}

Replace errclass.ErrTrailingJSONContent with the actual sentinel or error type that errclass.DecodeSingleJSON returns for trailing content.

🤖 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 `@internal/client/api_response_test.go` around lines 195 - 197, Update the
trailing-content assertion in the affected test to verify the concrete cause
returned by errclass.DecodeSingleJSON, using errors.Is with its actual sentinel
or errors.As with its actual error type. Replace the current errors.Unwrap(err)
nil-check while preserving the existing failure context.

Source: Coding guidelines

🧹 Nitpick comments (1)
internal/client/api_response.go (1)

89-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared rate-limit decoration instead of duplicating it.

Lines 89-104 repeat the business-rate-limit branch of errclass.ClassifyHTTPRateLimit (internal/errclass/http_rate_limit.go:32-72) almost line for line: the same code overwrite, the same RateLimitLogID derivation, the same hint merge, and the same WithRetryable().WithRetryAfter(...) call. ClassifyHTTPRateLimit cannot be called here only because it early-returns when the status is not 429. Two copies of one classification rule can drift.

Extract the decoration into an exported errclass helper and call it from both places. Also promote the literal 99991400 (lines 95, 98, and 118) to a named constant in errclass.

♻️ Sketch of the shared helper

In internal/errclass/http_rate_limit.go:

// BusinessRateLimitCode is the Lark business rate-limit error code.
const BusinessRateLimitCode = 99991400

// DecorateBusinessRateLimit normalizes an existing or new APIError into the
// canonical business rate-limit shape and attaches retry metadata.
func DecorateBusinessRateLimit(header http.Header, result any, classified error, now time.Time) *errs.APIError {
	var apiErr *errs.APIError
	if !errors.As(classified, &apiErr) {
		apiErr = errs.NewAPIError(errs.SubtypeRateLimit, RateLimitMessage)
	}
	apiErr.Subtype = errs.SubtypeRateLimit
	apiErr.Code = BusinessRateLimitCode
	apiErr.Message = RateLimitMessage
	apiErr.LogID = RateLimitLogID(result, header)
	seconds, source := ParseRetryAfter(header, now)
	apiErr.Hint = MergeRateLimitHint(apiErr.Hint, RateLimitGuidance(seconds))
	return apiErr.WithRetryable().WithRetryAfter(seconds, source)
}

Then in this file:

-		var apiErr *errs.APIError
-		var existing *errs.APIError
-		if errors.As(classified, &existing) {
-			apiErr = existing
-		}
-		if apiErr == nil {
-			apiErr = errs.NewAPIError(errs.SubtypeRateLimit, errclass.RateLimitMessage).WithCode(99991400)
-		}
-		apiErr.Subtype = errs.SubtypeRateLimit
-		apiErr.Code = 99991400
-		apiErr.Message = errclass.RateLimitMessage
-		apiErr.LogID = errclass.RateLimitLogID(result, resp.Header)
-
-		seconds, source := errclass.ParseRetryAfter(resp.Header, time.Now())
-		apiErr.Hint = errclass.MergeRateLimitHint(apiErr.Hint, errclass.RateLimitGuidance(seconds))
-		return apiErr.WithRetryable().WithRetryAfter(seconds, source)
+		return errclass.DecorateBusinessRateLimit(resp.Header, result, classified, time.Now())
🤖 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 `@internal/client/api_response.go` around lines 89 - 104, Extract the
duplicated business rate-limit normalization from the API response flow and
errclass.ClassifyHTTPRateLimit into an exported errclass helper, such as
DecorateBusinessRateLimit, preserving the shared code, message, log ID, retry
hint, and retryability behavior. Define and use a named
errclass.BusinessRateLimitCode constant instead of the 99991400 literal
throughout both paths, and update the API response branch to call the helper.
🤖 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 `@internal/client/client_test.go`:
- Around line 1082-1086: Extend
TestDoStream_HTTP400RejectsUntrustedFourKiBRateLimitPrefix with a case whose
complete body is exactly maxBody (4096 bytes), without relying on trailing data
or a read error beyond the limit. Assert that DoStream classifies it as
rate-limited and preserves the expected LogID.

---

Outside diff comments:
In `@internal/client/api_response_test.go`:
- Around line 195-197: Update the trailing-content assertion in the affected
test to verify the concrete cause returned by errclass.DecodeSingleJSON, using
errors.Is with its actual sentinel or errors.As with its actual error type.
Replace the current errors.Unwrap(err) nil-check while preserving the existing
failure context.

---

Nitpick comments:
In `@internal/client/api_response.go`:
- Around line 89-104: Extract the duplicated business rate-limit normalization
from the API response flow and errclass.ClassifyHTTPRateLimit into an exported
errclass helper, such as DecorateBusinessRateLimit, preserving the shared code,
message, log ID, retry hint, and retryability behavior. Define and use a named
errclass.BusinessRateLimitCode constant instead of the 99991400 literal
throughout both paths, and update the API response branch to call the helper.
🪄 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: 48f2e22c-8525-414d-9288-4574381f16a2

📥 Commits

Reviewing files that changed from the base of the PR and between ded1313 and 1872b77.

📒 Files selected for processing (8)
  • cmd/auth/auth.go
  • cmd/event/runtime.go
  • internal/client/api_response.go
  • internal/client/api_response_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/response.go
  • shortcuts/common/runner.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • cmd/auth/auth.go
  • cmd/event/runtime.go
  • internal/client/client.go
  • shortcuts/common/runner.go

Comment thread internal/client/client_test.go
@coderabbitai coderabbitai Bot mentioned this pull request Aug 5, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working domain/ccm PR touches the ccm domain domain/contact PR touches the contact domain domain/im PR touches the im domain domain/mail PR touches the mail domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant