Skip to content

fix: report rate limits and run shutdown hooks on interrupt - #2233

Open
sang-neo03 wants to merge 3 commits into
mainfrom
fix/request-lifecycle
Open

fix: report rate limits and run shutdown hooks on interrupt#2233
sang-neo03 wants to merge 3 commits into
mainfrom
fix/request-lifecycle

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lark API rate-limit errors currently expose retryable but discard the server-provided Retry-After delay, and process interrupts can bypass extension Shutdown hooks. This PR reports the delay in the structured error envelope, removes the dormant transport retry implementation, and makes root-command interruption cancel in-flight work before running Shutdown hooks with the expected signal exit code.

Changes

  • Add errclass.RetryAfterSeconds and pass response-header context through internal/client, shortcuts/common, and MCP response classification so rate-limit errors include retry_after_seconds only when the server supplies a positive value. The value is reported without a cap or automatic wait.
  • Remove cmdutil.RetryTransport from both SDK transport construction sites. The CLI contract already exposes error.retryable, code 99991400 is explicitly retryable, and code 1063006 is intentionally not short-term retryable; hidden transport retries would conceal those facts from callers.
  • Add transport_no_automatic_retry to internal/qualitygate/rules so a RoundTrip implementation cannot loop over calls to its underlying transport without an explicit documented exception.
  • Build the root context with signal notification, preserve command cancellation, run extension Shutdown hooks after the command returns, retain exit code 130 for SIGINT and 143 for SIGTERM, and restore default signal handling after the first signal so a second interrupt terminates a blocked Shutdown hook.
  • Pass command and request contexts through auth.GetValidAccessToken to the refresh HTTP request, including the multi-tenant sidecar example, and document that signature change in CHANGELOG.md.
  • Keep request timeouts outside this PR because selecting global bounds for foreground operations requires separate behavior and compatibility analysis. This change adds no client timeout, response-header timeout, CLI timeout flag, or timeout environment setting.
  • Cover hidden transport retries with TestBuildSDKTransportDoesNotRetryServerErrors and the repository-wide absence of RetryTransport; cover interrupt ordering with TestSIGINTRunsShutdownHookAndReturns130 and TestSecondSIGINTForcesExitDuringShutdown; cover rate-limit delays with the client, shortcut, and MCP response tests; and prevent reintroduction through TestCheckTransportRetryLoops and the repository scan test.

Test Plan

  • make unit-test passed with the race detector
  • go vet ./... passed
  • gofmt -l . produced no output
  • go mod tidy produced no changes
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main passed with 0 issues
  • make quality-gate passed
  • go test -count=1 ./tests/plugin_e2e -run 'Test(SIGINTRunsShutdownHookAndReturns130|SecondSIGINTForcesExitDuringShutdown)' passed
  • go test -count=1 -tags authsidecar_multi_tenant_demo ./sidecar/server-multi-tenant-demo passed
  • targeted request-lifecycle package tests passed for internal/errclass, internal/credential, internal/client, shortcuts/common, internal/cmdutil, internal/qualitygate/rules, internal/auth, and internal/identitydiag
  • go test -count=1 ./... completed with existing live E2E failures; the same failing packages and causes were confirmed on the base commit

Related Issues

N/A

Summary by CodeRabbit

  • Bug Fixes
    • Improved interruption handling: shutdown hooks now run when the CLI receives an interrupt, with standard signal-based exit codes.
    • A second interrupt can force termination if shutdown is blocked.
    • Request cancellation and deadlines now propagate through authentication and API operations.
    • Rate-limit errors now include server-provided retry timing and retry-safety guidance.
    • Removed automatic retries for server-error responses; requests are sent only once.

@sang-neo03
sang-neo03 requested a review from liangshuo-1 as a code owner August 7, 2026 08:06
@sang-neo03 sang-neo03 added the bugfix Bug fixes label Aug 7, 2026
@github-actions github-actions Bot added the size/XL Architecture-level or global-impact change label Aug 7, 2026
@sang-neo03
sang-neo03 force-pushed the fix/request-lifecycle branch from c650fca to fbf20ef Compare August 7, 2026 08:10
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR propagates request cancellation through token refresh and shutdown handling, adds signal-aware root execution, carries retry-after metadata through error classification, removes automatic transport retries, and adds a quality gate for retry loops.

Changes

Request cancellation and shutdown

Layer / File(s) Summary
Signal-aware root execution
cmd/root.go, tests/plugin_e2e/shutdown_signal_test.go
Root execution cancels on the first SIGINT or SIGTERM, runs shutdown hooks with detached cancellation, and force-terminates on a second signal. End-to-end tests cover both paths.
Context-aware UAT refresh
internal/auth/uat_client.go, internal/credential/default_provider.go, internal/identitydiag/diagnostics.go, sidecar/server-multi-tenant-demo/*
UAT refresh and token lookup now propagate caller contexts through locking, HTTP requests, and cancellation-specific error handling.
UAT cancellation validation
internal/auth/uat_client_refresh_test.go, CHANGELOG.md
Tests cover canceled refreshes, request counts, timing, token preservation, and existing refresh behavior. The changelog records the fix.

Rate-limit error classification

Layer / File(s) Summary
Classification context and retry-after construction
internal/errclass/*, internal/credential/tat_fetch.go
Classification context now carries retry delays. Shared helpers parse retry headers and enrich typed rate-limit errors. TAT fetching uses the shared parser.
API response classification flow
internal/client/*, cmd/api/api.go, cmd/service/service.go
API response handling propagates classification context through business, parsing, and status errors. HTTP 429 responses become typed rate-limit errors with retry metadata.
Shortcut and MCP classification
shortcuts/common/*
Shortcut and MCP response paths propagate identity and retry-after context and classify HTTP 429 responses as rate-limit errors.

Transport retry enforcement

Layer / File(s) Summary
Remove automatic transport retries
internal/cmdutil/factory_default.go, internal/cmdutil/transport.go, internal/cmdutil/transport_test.go
Direct and SDK transports no longer use RetryTransport. Tests verify that a POST returning 503 is sent once.
Detect RoundTrip retry loops
internal/qualitygate/rules/*
The quality gate scans eligible Go files for loop-based nested RoundTrip calls, validates waiver reasons, and reports diagnostics.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RootCommand
  participant UATRefresh
  participant HTTPAPI
  participant ErrorClassifier
  User->>RootCommand: Send SIGINT or SIGTERM
  RootCommand->>UATRefresh: Cancel request context
  UATRefresh->>HTTPAPI: Send context-bound refresh request
  HTTPAPI-->>UATRefresh: Return cancellation or API response
  HTTPAPI->>ErrorClassifier: Pass status and retry-after metadata
  ErrorClassifier-->>RootCommand: Return classified error
Loading

Possibly related PRs

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.97% 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 two primary changes: rate-limit reporting and shutdown hooks on interrupt.
Description check ✅ Passed The description includes the required summary, changes, test plan, and related issues sections with detailed verification results.
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/request-lifecycle

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 commented Aug 7, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#fix/request-lifecycle -y -g

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

PR Quality Summary

CI did not complete successfully. Use the failed check links below to decide whether this PR needs a code change or a rerun.

Failed checks

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

Caution

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

⚠️ Outside diff range comments (2)
shortcuts/common/mcp_client.go (1)

128-160: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prioritize HTTP 429 over unknown payload codes.

classifyMCPHTTPError() and CheckResponseWithContext() classify known business codes before applying the HTTP-status fallback, while the default HandleResponse does the opposite. For an HTTP 429 body with an unrecognized business code, this path can return SubtypeUnknown and lose retry_after_seconds.

  • shortcuts/common/mcp_client.go#L128-L160: Return errclass.NewRateLimitError(...) for HTTP 429 when the top-level or JSON-RPC error code is unrecognized.
  • shortcuts/common/runner.go#L357: Add an HTTP 429 fallback after the errclass.BuildAPIError() result before returning unknown payload errors.
  • shortcuts/common/mcp_client_test.go#L304-L344: Add HTTP 429 cases with unknown top-level and JSON-RPC error codes.
  • shortcuts common/runner_response_test.go#L17-L45: Add an HTTP 429 case with an unknown non-zero business code.
  • internal/client/response.go: Add a test case for HTTP 429 with an unknown non-zero business code flowing through HandleResponse.
🤖 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 `@shortcuts/common/mcp_client.go` around lines 128 - 160, Prioritize HTTP 429
rate-limit classification before unknown business-code handling: update
classifyMCPHTTPError in shortcuts/common/mcp_client.go:128-160 and the
errclass.BuildAPIError flow in shortcuts/common/runner.go:357 to return
errclass.NewRateLimitError for unknown top-level or JSON-RPC codes while
preserving retry_after_seconds; add coverage for unknown-code 429 responses in
shortcuts/common/mcp_client_test.go:304-344,
shortcuts/common/runner_response_test.go:17-45, and the HandleResponse tests in
internal/client/response.go.
internal/client/client.go (1)

506-519: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Project the response map into a typed error envelope.

Line 507 keeps untyped JSON data across the classification boundary. Add one projection function for the response-error shape before calling errclass.BuildAPIError. This prevents silent field-shape errors during error classification.

As per coding guidelines, “Parse map[string]interface{} into typed structs at the boundary, use one projection function per shape.”

🤖 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/client.go` around lines 506 - 519, Update
APIClient.CheckResponseWithContext to project the untyped resultMap into the
typed response-error envelope before calling errclass.BuildAPIError. Add and use
a single projection function for this response-error shape, handling invalid
field shapes explicitly rather than passing raw map data across the
classification boundary.

Source: Coding guidelines

🤖 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/auth/uat_client_refresh_test.go`:
- Around line 157-201: The cancellation assertion in
TestGetValidAccessTokenCancelsRefreshRequest must validate the typed error
contract as well as preserve context.Canceled. Use errs.ProblemOf(err) to assert
CategoryNetwork and SubtypeNetworkTransport, while retaining the existing
errors.Is(err, context.Canceled) check; do not assert Param for this
*errs.NetworkError path.

In `@internal/auth/uat_client.go`:
- Line 88: Update refreshWithLock so waiting for processLock acquisition
observes ctx cancellation instead of blocking in sync.Mutex.Lock; use the
existing cancellable or timeout-based lock-wait mechanism, then return the
context error when cancellation occurs before acquisition while preserving
normal refresh behavior after the lock is obtained.

In `@internal/client/response.go`:
- Around line 109-116: The response classification in
internal/client/response.go:109-116 must make HTTP 429 authoritative even when
check returns a nonzero business-code error; update check/errclass handling or
add the 429 classification before returning apiErr so the result preserves the
rate-limit subtype and retry delay. Add coverage in
internal/client/response_test.go:367-420 using an unrecognized nonzero JSON code
with HTTP 429, and assert via errs.ProblemOf that the category is API, subtype
is rate_limit, and retry_after_seconds is populated.

In `@internal/cmdutil/factory_default.go`:
- Line 205: Add a regression test targeting buildDirectHTTPTransport directly,
using a test server that returns HTTP 503 and counting received requests; send
one request through the returned transport and assert the server is called
exactly once, ensuring direct transport does not regain retry wrapping.

In `@internal/errclass/retry_after_test.go`:
- Around line 62-71: Update the error-path assertions in
internal/errclass/retry_after_test.go:62-71,
shortcuts/common/mcp_client_test.go:335-341, and
shortcuts/common/runner_response_test.go:36-42 to use errs.ProblemOf(err) for
CategoryAPI and SubtypeRateLimit. Retain errors.As in retry_after_test.go for
validating RetryAfterSeconds, and do not assert Param because these server-side
errors do not populate it.

In `@internal/errclass/retry_after.go`:
- Around line 18-26: Update RetryAfterSeconds in
internal/errclass/retry_after.go to preserve the existing gateway-header
priority, then parse a valid HTTP-date Retry-After value and return its positive
remaining delay. Add a valid HTTP-date test case in
internal/errclass/retry_after_test.go covering the expected positive retry
duration.

In `@internal/qualitygate/rules/transport_retry.go`:
- Around line 25-43: The new checker must return prescribed typed errors instead
of raw filesystem or parser errors. Update CheckTransportRetryLoops and the
related error paths in transportRuleFiles/checkTransportRetrySource to wrap
file-read failures with the repository’s typed file-I/O constructor and parser
or other lower-layer failures with the unknown-error constructor, preserving
each original error via WithCause(err). Add error-path tests asserting the typed
metadata and preserved cause.

In `@tests/plugin_e2e/shutdown_signal_test.go`:
- Around line 65-84: Add a SIGTERM end-to-end test alongside
TestSIGINTRunsShutdownHookAndReturns130 that skips Windows, starts the signal
fork, sends syscall.SIGTERM, and waits for termination. Assert the process exits
with code 143 and verify the shutdown marker contains “shutdown”, mirroring the
existing SIGINT coverage.

---

Outside diff comments:
In `@internal/client/client.go`:
- Around line 506-519: Update APIClient.CheckResponseWithContext to project the
untyped resultMap into the typed response-error envelope before calling
errclass.BuildAPIError. Add and use a single projection function for this
response-error shape, handling invalid field shapes explicitly rather than
passing raw map data across the classification boundary.

In `@shortcuts/common/mcp_client.go`:
- Around line 128-160: Prioritize HTTP 429 rate-limit classification before
unknown business-code handling: update classifyMCPHTTPError in
shortcuts/common/mcp_client.go:128-160 and the errclass.BuildAPIError flow in
shortcuts/common/runner.go:357 to return errclass.NewRateLimitError for unknown
top-level or JSON-RPC codes while preserving retry_after_seconds; add coverage
for unknown-code 429 responses in shortcuts/common/mcp_client_test.go:304-344,
shortcuts/common/runner_response_test.go:17-45, and the HandleResponse tests in
internal/client/response.go.
🪄 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: 62b0c88d-9939-41ab-8c28-691dfe6769ab

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff2bd1 and fbf20ef.

📒 Files selected for processing (28)
  • CHANGELOG.md
  • cmd/api/api.go
  • cmd/root.go
  • cmd/service/service.go
  • internal/auth/uat_client.go
  • internal/auth/uat_client_refresh_test.go
  • internal/client/client.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/transport.go
  • internal/cmdutil/transport_test.go
  • internal/credential/default_provider.go
  • internal/credential/tat_fetch.go
  • internal/errclass/classify.go
  • internal/errclass/retry_after.go
  • internal/errclass/retry_after_test.go
  • internal/identitydiag/diagnostics.go
  • internal/qualitygate/rules/run.go
  • internal/qualitygate/rules/transport_retry.go
  • internal/qualitygate/rules/transport_retry_test.go
  • shortcuts/common/mcp_client.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_response_test.go
  • sidecar/server-multi-tenant-demo/auth_bridge.go
  • sidecar/server-multi-tenant-demo/handler.go
  • tests/plugin_e2e/shutdown_signal_test.go
💤 Files with no reviewable changes (1)
  • internal/cmdutil/transport.go

Comment on lines +157 to +201
func TestGetValidAccessTokenCancelsRefreshRequest(t *testing.T) {
setupStoredTokenTest(t)
stored := newRefreshTestToken()
if err := SetStoredToken(stored); err != nil {
t.Fatalf("SetStoredToken() error = %v", err)
}

started := make(chan struct{}, 1)
var calls atomic.Int32
client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
calls.Add(1)
started <- struct{}{}
<-req.Context().Done()
return nil, req.Context().Err()
})}
ctx, cancel := context.WithCancel(context.Background())
resultCh := make(chan error, 1)
go func() {
_, err := GetValidAccessToken(ctx, client, newRefreshTestOptions(stored))
resultCh <- err
}()

select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("refresh request did not start")
}
cancel()

select {
case err := <-resultCh:
if !errors.Is(err, context.Canceled) {
t.Fatalf("GetValidAccessToken() error = %v, want context.Canceled cause", err)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("GetValidAccessToken() did not return within 100ms after cancellation")
}
if calls.Load() != 1 {
t.Fatalf("refresh request count = %d, want 1", calls.Load())
}
current := GetStoredToken(stored.AppId, stored.UserOpenId)
if current == nil || current.RefreshToken != stored.RefreshToken {
t.Fatalf("stored token = %#v, want canceled refresh to preserve it", current)
}
}

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 cancellation error classification.

Lines 186-190 verify the context.Canceled cause. They do not verify the new typed error contract. Assert errs.ProblemOf(err) returns errs.CategoryNetwork and errs.SubtypeNetworkTransport.

Param is not applicable because this path returns *errs.NetworkError, not a validation error. Based on learnings, errs.ProblemOf exposes problem-level fields and does not expose Param. As per coding guidelines, error-path tests must assert typed metadata and preserve the cause.

🤖 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/auth/uat_client_refresh_test.go` around lines 157 - 201, The
cancellation assertion in TestGetValidAccessTokenCancelsRefreshRequest must
validate the typed error contract as well as preserve context.Canceled. Use
errs.ProblemOf(err) to assert CategoryNetwork and SubtypeNetworkTransport, while
retaining the existing errors.Is(err, context.Canceled) check; do not assert
Param for this *errs.NetworkError path.

Sources: Coding guidelines, Learnings

// refreshWithLock serializes the complete refresh transaction with every
// stored-token writer and remover for this account.
func refreshWithLock(httpClient *http.Client, opts UATCallOptions) (*StoredUAToken, error) {
func refreshWithLock(ctx context.Context, httpClient *http.Client, opts UATCallOptions) (*StoredUAToken, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline internal/auth --items all --type function --match 'withTokenStorageLock|refreshWithLock|GetValidAccessToken'
rg -n -C 12 '\bwithTokenStorageLock\s*\(' internal/auth
rg -n -C 8 'GetValidAccessTokenCancelsRefreshRequest|ConcurrentRefreshesAreCoalesced' internal/auth/uat_client_refresh_test.go

Repository: larksuite/cli

Length of output: 15888


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' internal/auth/uat_client.go
sed -n '49,130p' internal/auth/token_lock.go
sed -n '157,225p' internal/auth/uat_client_refresh_test.go
sed -n '402,470p' internal/auth/uat_client_refresh_test.go
rg -n 'tokenStorageProcessLocks|Lock\(|LockContext|select|<-' internal/auth/token_lock.go internal/auth/token_lock_test.go

Repository: larksuite/cli

Length of output: 12482


Make the in-process token lock cancelable.

refreshWithLock passes cancellation only to the refresh HTTP request. The new tests show the HTTP side returns after cancellation, but when refreshWithLock is already queued behind another refresh, processLock.Lock() ignores the context. Use a cancellable/timeout-wait mechanism for the in-process sync.Mutex as well, so GetValidAccessToken stops while still waiting for lock acquisition.

🤖 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/auth/uat_client.go` at line 88, Update refreshWithLock so waiting
for processLock acquisition observes ctx cancellation instead of blocking in
sync.Mutex.Lock; use the existing cancellable or timeout-based lock-wait
mechanism, then return the context error when cancellation occurs before
acquisition while preserving normal refresh behavior after the lock is obtained.

Comment on lines +109 to +116
if apiErr := check(result, identity, cc); apiErr != nil {
return apiErr
}
// CheckResponse treats business code 0 as success, so a 4xx/5xx whose
// JSON body omits a non-zero code would otherwise be served as a
// successful result. Classify by HTTP status so it is never swallowed.
if resp.StatusCode >= 400 {
return httpStatusError(resp.StatusCode, resp.RawBody)
return httpStatusError(resp.StatusCode, resp.RawBody, cc)

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 | 🟠 Major | ⚡ Quick win

Make HTTP 429 status authoritative for JSON error responses.

A nonzero JSON business code currently returns before the HTTP 429 fallback. This can remove the rate-limit subtype and retry delay.

  • internal/client/response.go#L109-L116: classify http.StatusTooManyRequests before returning a business-code error, or pass HTTP status into errclass classification.
  • internal/client/response_test.go#L367-L420: add an unrecognized nonzero JSON code with HTTP 429 and assert API category, rate_limit subtype, and retry_after_seconds through errs.ProblemOf.
📍 Affects 2 files
  • internal/client/response.go#L109-L116 (this comment)
  • internal/client/response_test.go#L367-L420
🤖 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/response.go` around lines 109 - 116, The response
classification in internal/client/response.go:109-116 must make HTTP 429
authoritative even when check returns a nonzero business-code error; update
check/errclass handling or add the 429 classification before returning apiErr so
the result preserves the rate-limit subtype and retry delay. Add coverage in
internal/client/response_test.go:367-420 using an unrecognized nonzero JSON code
with HTTP 429, and assert via errs.ProblemOf that the category is API, subtype
is rate_limit, and retry_after_seconds is populated.

Source: Coding guidelines


func buildDirectHTTPTransport(base http.RoundTripper, platform bool) http.RoundTripper {
var builtIn http.RoundTripper = &RetryTransport{Base: base}
var builtIn http.RoundTripper = base

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a direct-transport single-request test.

Line 205 removes retries from direct HTTP transport. The supplied regression test calls buildSDKTransport only. It cannot fail if buildDirectHTTPTransport regains a retry wrapper. Add an HTTP 503 test that sends a request through buildDirectHTTPTransport and asserts one server call.

🤖 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/cmdutil/factory_default.go` at line 205, Add a regression test
targeting buildDirectHTTPTransport directly, using a test server that returns
HTTP 503 and counting received requests; send one request through the returned
transport and assert the server is called exactly once, ensuring direct
transport does not regain retry wrapping.

Source: Coding guidelines

Comment on lines +62 to +71
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
t.Fatalf("BuildAPIError() = %T %v, want *errs.APIError", err, err)
}
if apiErr.RetryAfterSeconds != test.want {
t.Fatalf("retry_after_seconds = %d, want %d", apiErr.RetryAfterSeconds, test.want)
}
if test.want > 0 && (!strings.Contains(apiErr.Hint, "server requested") || !strings.Contains(apiErr.Hint, "safe to repeat")) {
t.Fatalf("hint = %q, want server delay and repeat-safety guidance", apiErr.Hint)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert common typed metadata through errs.ProblemOf.

These error-path tests inspect *errs.APIError directly. Add errs.ProblemOf(err) assertions for CategoryAPI and SubtypeRateLimit. Retain errors.As for RetryAfterSeconds. Do not assert Param because these server-side errors do not populate it.

  • internal/errclass/retry_after_test.go#L62-L71: Assert category and subtype through errs.ProblemOf.
  • shortcuts/common/mcp_client_test.go#L335-L341: Assert category and subtype through errs.ProblemOf.
  • shortcuts/common/runner_response_test.go#L36-L42: Assert category and subtype through errs.ProblemOf.

As per coding guidelines, “Error-path tests must assert typed metadata through errs.ProblemOf.” Based on learnings, BuildAPIError server errors do not set Param.

📍 Affects 3 files
  • internal/errclass/retry_after_test.go#L62-L71 (this comment)
  • shortcuts/common/mcp_client_test.go#L335-L341
  • shortcuts/common/runner_response_test.go#L36-L42
🤖 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/retry_after_test.go` around lines 62 - 71, Update the
error-path assertions in internal/errclass/retry_after_test.go:62-71,
shortcuts/common/mcp_client_test.go:335-341, and
shortcuts/common/runner_response_test.go:36-42 to use errs.ProblemOf(err) for
CategoryAPI and SubtypeRateLimit. Retain errors.As in retry_after_test.go for
validating RetryAfterSeconds, and do not assert Param because these server-side
errors do not populate it.

Sources: Coding guidelines, Learnings

Comment on lines +18 to +26
func RetryAfterSeconds(header http.Header) int {
for _, name := range []string{"X-Ogw-Ratelimit-Reset", "Retry-After"} {
seconds, err := strconv.Atoi(strings.TrimSpace(header.Get(name)))
if err == nil && seconds > 0 {
return seconds
}
}
return 0
}

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

Support HTTP-date Retry-After values.

Retry-After can contain either delay seconds or an HTTP date. Line 20 discards valid date values, so clients omit a server-provided retry delay.

  • internal/errclass/retry_after.go#L18-L26: Parse a valid HTTP-date Retry-After value into a positive remaining delay after gateway-header handling.
  • internal/errclass/retry_after_test.go#L16-L44: Add a valid HTTP-date case and assert that it produces a positive retry delay.
📍 Affects 2 files
  • internal/errclass/retry_after.go#L18-L26 (this comment)
  • internal/errclass/retry_after_test.go#L16-L44
🤖 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/retry_after.go` around lines 18 - 26, Update
RetryAfterSeconds in internal/errclass/retry_after.go to preserve the existing
gateway-header priority, then parse a valid HTTP-date Retry-After value and
return its positive remaining delay. Add a valid HTTP-date test case in
internal/errclass/retry_after_test.go covering the expected positive retry
duration.

Comment on lines +25 to +43
func CheckTransportRetryLoops(repo string, changedFiles []string, changedOnly bool) ([]report.Diagnostic, error) {
files, err := transportRuleFiles(repo, changedFiles, changedOnly)
if err != nil {
return nil, err
}

var diagnostics []report.Diagnostic
for _, path := range files {
src, err := vfs.ReadFile(filepath.Join(repo, filepath.FromSlash(path)))
if err != nil {
return nil, err
}
fileDiagnostics, err := checkTransportRetrySource(path, src)
if err != nil {
return nil, err
}
diagnostics = append(diagnostics, fileDiagnostics...)
}
return diagnostics, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Return typed errors from the new checker.

These paths return raw file-system and parser errors. Run returns them unchanged. Wrap each failure with the repository typed file-I/O or unknown-error constructor and preserve the cause with .WithCause(err). Add error-path tests that assert the typed metadata and cause preservation.

As per coding guidelines, **/*.go: “Use the prescribed typed error constructors for … file I/O failures, and unknown lower-layer errors.”

Also applies to: 46-50, 137-185

🤖 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/qualitygate/rules/transport_retry.go` around lines 25 - 43, The new
checker must return prescribed typed errors instead of raw filesystem or parser
errors. Update CheckTransportRetryLoops and the related error paths in
transportRuleFiles/checkTransportRetrySource to wrap file-read failures with the
repository’s typed file-I/O constructor and parser or other lower-layer failures
with the unknown-error constructor, preserving each original error via
WithCause(err). Add error-path tests asserting the typed metadata and preserved
cause.

Source: Coding guidelines

Comment on lines +65 to +84
func TestSIGINTRunsShutdownHookAndReturns130(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("os.Interrupt cannot be sent to Windows processes")
}

fork := startSignalFork(t, false)
if err := fork.cmd.Process.Signal(os.Interrupt); err != nil {
t.Fatalf("send SIGINT: %v", err)
}
waitErr := waitForForkExit(t, fork, 10*time.Second)

var exitErr *exec.ExitError
if !errors.As(waitErr, &exitErr) || exitErr.ExitCode() != 130 {
t.Fatalf("SIGINT exit error = %v, exit code = %d, want 130; stdout=%s stderr=%s",
waitErr, fork.cmd.ProcessState.ExitCode(), fork.stdout.String(), fork.stderr.String())
}
if data, err := os.ReadFile(fork.shutdownMarker); err != nil || string(data) != "shutdown" {
t.Fatalf("Shutdown marker = %q, err=%v, want shutdown", data, 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add SIGTERM exit-code coverage.

This file covers SIGINT exit code 130 but does not cover SIGTERM exit code 143. Add an end-to-end test that sends syscall.SIGTERM, asserts exit code 143, and verifies the shutdown marker.

As per coding guidelines, “Every behavior change must have an accompanying test, and contract tests must assert the changed field or behavior directly so reverting the implementation causes failure.”

🤖 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 `@tests/plugin_e2e/shutdown_signal_test.go` around lines 65 - 84, Add a SIGTERM
end-to-end test alongside TestSIGINTRunsShutdownHookAndReturns130 that skips
Windows, starts the signal fork, sends syscall.SIGTERM, and waits for
termination. Assert the process exits with code 143 and verify the shutdown
marker contains “shutdown”, mirroring the existing SIGINT coverage.

Source: Coding guidelines

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.86179% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.34%. Comparing base (4ff2bd1) to head (fbf20ef).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
internal/qualitygate/rules/transport_retry.go 79.48% 15 Missing and 9 partials ⚠️
internal/errclass/retry_after.go 62.06% 10 Missing and 1 partial ⚠️
cmd/root.go 74.35% 9 Missing and 1 partial ⚠️
shortcuts/common/mcp_client.go 80.00% 3 Missing ⚠️
internal/qualitygate/rules/run.go 50.00% 1 Missing and 1 partial ⚠️
internal/client/client.go 66.66% 1 Missing ⚠️
internal/credential/default_provider.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2233      +/-   ##
==========================================
- Coverage   76.34%   76.34%   -0.01%     
==========================================
  Files         991      993       +2     
  Lines      106029   106204     +175     
==========================================
+ Hits        80952    81082     +130     
- Misses      18942    18976      +34     
- Partials     6135     6146      +11     

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

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

Labels

bugfix Bug fixes size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant