feat(zoo-gateway): auth callback and multi-profile token sync#347
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughSynchronizes Zoo Gateway session tokens across all active editor instances, adds session-cleared-aware token resolution, centralizes gateway API error classification and UX surfacing, seeds and persists zoo-gateway provider profiles at webview init, and updates model fetchers, sign-out, tests, and i18n strings accordingly. ChangesZoo Gateway multi-instance token propagation and profile persistence
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
src/core/webview/__tests__/ClineProvider.spec.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. src/core/webview/webviewMessageHandler.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. 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. Comment |
adedf0a to
8634510
Compare
654117d to
1326a97
Compare
8634510 to
7bc8c0c
Compare
f245208 to
078bce0
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
proyectoauraorg
left a comment
There was a problem hiding this comment.
Review: Auth Callback + Multi-Profile Token Sync
Verdict: ✅ Approved (with security notes for future hardening)
Verified
- ✅
handleUri.spec.ts— 8 tests pass (multi-instance fan-out, sequential serialization) - ✅
ClineProvider.spec.ts— 82 tests pass (6 new auth profile sync tests) - ✅
webviewMessageHandler.spec.ts— 44 tests pass (2 new sign-out tests) - ✅ Sequential execution of callbacks avoids profile-store race conditions
- ✅ Sign-out clears tokens from ALL zoo-gateway profiles (correct symmetry)
- ✅ Model list fetch recovers credentials from non-active profiles
Security Notes (future hardening, not blockers)
- Token storage:
zooSessionTokenis stored in JSON-serializedProviderSettingson disk. VS Code's globalStorage is OS-protected, but migrating toSecretStoragewould add an extra layer of defense. - Base URL validation:
zooGatewayBaseUrlis derived fromgetZooCodeBaseUrl(). In production this is fine, but a misconfiguredZOO_CODE_BASE_URLenv var could route tokens to an unintended host. Consider adding domain validation.
Both are hardening improvements for a future iteration, not blockers for this PR.
Dependency
Depends on #345. After #345 merges, this should rebase cleanly.
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
65885a8 to
13c98d0
Compare
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
13c98d0 to
d4050f8
Compare
a40f6e9 to
983c133
Compare
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
d3165a0 to
7e54cf1
Compare
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
7e54cf1 to
57a128a
Compare
33892e3 to
6ae680e
Compare
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
3079c57 to
795d179
Compare
6ae680e to
946d6d1
Compare
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
795d179 to
8dab953
Compare
44ed33a to
bf91fde
Compare
e2e4536 to
babbe9e
Compare
faa5854 to
94b8bc6
Compare
5aa42cc to
2fbf902
Compare
94b8bc6 to
0dbdf07
Compare
taltas
left a comment
There was a problem hiding this comment.
Couple of small code cleaness tweaks.
| async function surfaceGatewayApiError(error: unknown): Promise<void> { | ||
| const status = getApiErrorStatus(error) | ||
| if (status === undefined) return | ||
| const code = getApiErrorCode(error) | ||
|
|
||
| if (status === 401) { | ||
| // Wipe before sign-in so the callback rebinds against an empty slot. | ||
| await clearZooCodeToken() | ||
| const action = await vscode.window.showErrorMessage( | ||
| t("common:zooAuth.errors.session_expired"), | ||
| t("common:zooAuth.buttons.sign_in"), | ||
| ) | ||
| if (action) { | ||
| void vscode.env.openExternal(vscode.Uri.parse(buildZooCodeSignInUrl())) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| const isBudgetExceeded = status === 429 && (code === "monthly_budget_exceeded" || code === "daily_budget_exceeded") | ||
| if (status === 402 || isBudgetExceeded) { | ||
| const message = isBudgetExceeded | ||
| ? t("common:zooAuth.errors.budget_exceeded") | ||
| : t("common:zooAuth.errors.out_of_credits") | ||
| const action = await vscode.window.showErrorMessage(message, t("common:zooAuth.buttons.add_credits")) | ||
| if (action) { | ||
| void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/dashboard/credits`)) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if (status === 403) { | ||
| const action = await vscode.window.showErrorMessage( | ||
| t("common:zooAuth.errors.account_unavailable"), | ||
| t("common:zooAuth.buttons.contact_support"), | ||
| ) | ||
| if (action) { | ||
| void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/support`)) | ||
| } | ||
| return | ||
| } | ||
| } |
There was a problem hiding this comment.
We should split this function out into transport behaviour and VS Code UX side effects:
| async function surfaceGatewayApiError(error: unknown): Promise<void> { | |
| const status = getApiErrorStatus(error) | |
| if (status === undefined) return | |
| const code = getApiErrorCode(error) | |
| if (status === 401) { | |
| // Wipe before sign-in so the callback rebinds against an empty slot. | |
| await clearZooCodeToken() | |
| const action = await vscode.window.showErrorMessage( | |
| t("common:zooAuth.errors.session_expired"), | |
| t("common:zooAuth.buttons.sign_in"), | |
| ) | |
| if (action) { | |
| void vscode.env.openExternal(vscode.Uri.parse(buildZooCodeSignInUrl())) | |
| } | |
| return | |
| } | |
| const isBudgetExceeded = status === 429 && (code === "monthly_budget_exceeded" || code === "daily_budget_exceeded") | |
| if (status === 402 || isBudgetExceeded) { | |
| const message = isBudgetExceeded | |
| ? t("common:zooAuth.errors.budget_exceeded") | |
| : t("common:zooAuth.errors.out_of_credits") | |
| const action = await vscode.window.showErrorMessage(message, t("common:zooAuth.buttons.add_credits")) | |
| if (action) { | |
| void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/dashboard/credits`)) | |
| } | |
| return | |
| } | |
| if (status === 403) { | |
| const action = await vscode.window.showErrorMessage( | |
| t("common:zooAuth.errors.account_unavailable"), | |
| t("common:zooAuth.buttons.contact_support"), | |
| ) | |
| if (action) { | |
| void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/support`)) | |
| } | |
| return | |
| } | |
| } | |
| type ZooGatewayApiErrorAction = | |
| | { kind: "sign_in" } | |
| | { kind: "add_credits" } | |
| | { kind: "contact_support" } | |
| | { kind: "none" } | |
| function classifyGatewayApiError(error: unknown): ZooGatewayApiErrorAction { | |
| const status = getApiErrorStatus(error) | |
| const code = getApiErrorCode(error) | |
| ... | |
| } | |
| async function surfaceGatewayApiError(error: unknown): Promise<void> { | |
| switch (classifyGatewayApiError(error).kind) { | |
| case "sign_in": | |
| ... | |
| return | |
| case "add_credits": | |
| ... | |
| return | |
| case "contact_support": | |
| ... | |
| return | |
| default: | |
| return | |
| } | |
| } |
This will make it easier to test and follow the logic.
There was a problem hiding this comment.
Done in 06fc185. Split into a pure classifyGatewayApiError(error) -> ZooGatewayApiErrorAction and a thin surfaceGatewayApiError that switches on the action for the VS Code side effects. The classifier is exported and now has focused unit tests covering 401/402/429-budget/429-non-budget/403/no-status, so the mapping is verified without the notification mocks.
| const allInstances = ClineProvider.getAllInstances() | ||
| for (const instance of allInstances) { | ||
| try { | ||
| await instance.handleZooCodeCallback(token) | ||
| } catch (error) { | ||
| console.error( | ||
| "Failed to persist Zoo Gateway token for a provider instance:", | ||
| error instanceof Error ? error.message : error, | ||
| ) | ||
| } |
There was a problem hiding this comment.
This is doing more than just routing now. We should split this into a helper function to keep it DRY.
| const allInstances = ClineProvider.getAllInstances() | |
| for (const instance of allInstances) { | |
| try { | |
| await instance.handleZooCodeCallback(token) | |
| } catch (error) { | |
| console.error( | |
| "Failed to persist Zoo Gateway token for a provider instance:", | |
| error instanceof Error ? error.message : error, | |
| ) | |
| } | |
| async function propagateZooGatewayCallback(token: string) { | |
| const allInstances = ClineProvider.getAllInstances() | |
| for (const instance of allInstances) { | |
| try { | |
| await instance.handleZooCodeCallback(token) | |
| } catch (error) { | |
| console.error( | |
| "Failed to persist Zoo Gateway token for a provider instance:", | |
| error instanceof Error ? error.message : error, | |
| ) | |
| } | |
| } | |
| } | |
| .... | |
| await propagateZooGatewayCallback(token) |
There was a problem hiding this comment.
Done in 06fc185. Extracted the fan-out into propagateZooGatewayCallback(token), so the /auth-callback case just calls that after setZooCodeUserInfo. The sequential read-modify-write and per-instance error isolation (plus the rationale comment) moved into the helper unchanged.
…llback fan-out Address review feedback on PR #347: - zoo-gateway.ts: split surfaceGatewayApiError into a pure classifyGatewayApiError (error -> action) plus a thin UX layer that switches on the action. The classifier is exported and covered by focused unit tests, so the status/code -> action mapping no longer needs the VS Code notification mocks to verify. - handleUri.ts: extract the per-instance token propagation loop into a propagateZooGatewayCallback helper, keeping the /auth-callback case focused on routing. Behaviour (sequential read-modify-write, per instance error isolation) is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor <cursoragent@cursor.com>
Clear the cached token on 401 and offer sign-in. On insufficient credits or budget limits, open the credits page. On account frozen/banned, open support. Errors still propagate to the task layer after the toast. Co-authored-by: Cursor <cursoragent@cursor.com>
…ocales Adds session_expired, out_of_credits, account_unavailable, budget_exceeded under zooAuth.errors and a new zooAuth.buttons block (sign_in, add_credits, contact_support) introduced by the gateway 401/402/403 UX so check-translations passes. Co-authored-by: Cursor <cursoragent@cursor.com>
…ov patch Adds vscode + i18n mocks and asserts the 401/402/403/429 paths in surfaceGatewayApiError: token clear + sign-in URL on 401, add-credits URL on 402 and budget-coded 429, support URL on 403, no-op on 429 without a budget code or on errors without a status. Also verifies the helper still runs before completePrompt rewraps the upstream error. Co-authored-by: Cursor <cursoragent@cursor.com>
…rors Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…l fetch Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The website's /api/extension/auth/verify route now returns 503 when the backend can't reach the database, instead of crashing. The extension previously treated any non-OK response from this endpoint as a definitively invalid token, which meant a transient backend hiccup would silently clear the user's session and force a fresh sign-in. verifyZooCodeToken now returns "unreachable" for 5xx responses (same classification as a network error), so initZooCodeAuth keeps the cached token in place and reports subscription status as "unknown" until the backend recovers. handleAuthCallback shows the could-not-verify message on 5xx so users see this is a temporary issue rather than a bad token. Co-authored-by: Cursor <cursoragent@cursor.com>
…llback fan-out Address review feedback on PR #347: - zoo-gateway.ts: split surfaceGatewayApiError into a pure classifyGatewayApiError (error -> action) plus a thin UX layer that switches on the action. The classifier is exported and covered by focused unit tests, so the status/code -> action mapping no longer needs the VS Code notification mocks to verify. - handleUri.ts: extract the per-instance token propagation loop into a propagateZooGatewayCallback helper, keeping the /auth-callback case focused on routing. Behaviour (sequential read-modify-write, per instance error isolation) is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
06fc185 to
42b3f0c
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/services/zoo-code-auth.ts (1)
55-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPropagate external token removals into
_sessionCleared.Line 56 updates
_cachedTokenwhen secret storage changes, but it never flips_sessionCleared. In another VS Code window, a sign-out or 401 clear will therefore leave this instance with_cachedToken === undefinedand_sessionCleared === false, soresolveZooGatewaySessionToken()falls back to the stale profile token thatsrc/api/providers/zoo-gateway.tsandsrc/api/providers/fetchers/zoo-gateway.tsstill pass in. That breaks multi-instance sign-out and can keep sending a revoked bearer until profile state is refreshed.Suggested fix
if (e.key === ZOO_CODE_TOKEN_KEY) { secretStorage?.get(ZOO_CODE_TOKEN_KEY).then((token) => { _cachedToken = token + _sessionCleared = !token // Reset subscription status when token changes _cachedSubscriptionStatus = "unknown" _lastSubscriptionCheck = 0 if (token) { checkSubscriptionStatus().catch(() => {}) } }) }🤖 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 `@src/services/zoo-code-auth.ts` around lines 55 - 65, When secretStorage changes for ZOO_CODE_TOKEN_KEY in the context.secrets.onDidChange handler, also propagate external removals into the auth state by setting _sessionCleared = true when the retrieved token is undefined (or when the change indicates removal); update the handler that currently sets _cachedToken, _cachedSubscriptionStatus, and _lastSubscriptionCheck to additionally flip _sessionCleared (and only clear it when a fresh token is present), so resolveZooGatewaySessionToken and related functions (e.g., resolveZooGatewaySessionToken, checkSubscriptionStatus) no longer fall back to stale profile tokens after a remote sign-out.src/api/providers/zoo-gateway.ts (1)
133-154:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRefresh
Authorizationper request using the latest resolved Zoo session token.
- The OpenAI client is constructed once with
apiKey: sessionToken || "not-provided"; laterensureAuthenticated()re-resolves the token but doesn’t update the client’s credential.createMessage(...)only forwardsX-Zoo-*enrichment headers, so the outgoing call does not overrideAuthorization.completePrompt(...)passes no per-requestheaders, so it will also keep using the stale client credential.Suggested fix
export class ZooGatewayHandler extends RouterProvider implements SingleCompletionHandler { constructor(options: ApiHandlerOptions) { const baseURL = options.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` const sessionToken = resolveZooGatewaySessionToken(options.zooSessionToken) @@ }) } - private ensureAuthenticated(): void { - if (!resolveZooGatewaySessionToken(this.options.zooSessionToken)) { + private ensureAuthenticated(): string { + const token = resolveZooGatewaySessionToken(this.options.zooSessionToken) + if (!token) { throw new Error(ZOO_GATEWAY_AUTH_ERROR) } + return token } @@ override async *createMessage( @@ ): ApiStream { - this.ensureAuthenticated() + const sessionToken = this.ensureAuthenticated() const { id: modelId, info } = await this.fetchModel() @@ // Build request headers with enrichment metadata - const requestHeaders: Record<string, string> = {} + const requestHeaders: Record<string, string> = { + Authorization: `Bearer ${sessionToken}`, + } if (metadata?.taskId) { requestHeaders["X-Zoo-Task-ID"] = metadata.taskId } if (metadata?.mode) { requestHeaders["X-Zoo-Mode"] = metadata.mode } @@ async completePrompt(prompt: string): Promise<string> { - this.ensureAuthenticated() + const sessionToken = this.ensureAuthenticated() const { id: modelId, info } = await this.fetchModel() @@ - const response = await this.client.chat.completions.create(requestOptions) + const response = await this.client.chat.completions.create(requestOptions, { + headers: { + Authorization: `Bearer ${sessionToken}`, + }, + }) return response.choices[0]?.message.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 `@src/api/providers/zoo-gateway.ts` around lines 133 - 154, The client is created once with apiKey: sessionToken which can go stale; fix by re-resolving the Zoo session token (call resolveZooGatewaySessionToken / ensureAuthenticated) inside each request path and inject an explicit per-request Authorization header (Authorization: Bearer <token>) into the headers you pass to the parent OpenAI client rather than relying on the initially constructed apiKey; specifically update createMessage(...) and completePrompt(...) to call resolveZooGatewaySessionToken (or ensureAuthenticated) at the start and merge Authorization into the per-request headers (alongside the existing X-Zoo-* enrichment headers) so every outgoing call uses the latest token.
🧹 Nitpick comments (3)
src/core/webview/__tests__/webviewMessageHandler.spec.ts (1)
1154-1179: ⚡ Quick winAssert the already-empty branch still disconnects and pushes updated state.
This test only proves
upsertProviderProfile()runs. If the handler regresses to skippingdisconnectZooCode()orpostStateToWebview()when disk is already clean, this branch still passes even though sign-out is incomplete for the user. Please pin those two side effects here as well. As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."🤖 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 `@src/core/webview/__tests__/webviewMessageHandler.spec.ts` around lines 1154 - 1179, The test only asserts upsertProviderProfile was called but must also verify the side effects disconnectZooCode and postStateToWebview occur when disk already has no token; update the spec for webviewMessageHandler to stub/mock mockClineProvider.disconnectZooCode and mockClineProvider.postStateToWebview (or their equivalents on the mocked provider), call webviewMessageHandler as before, and add expectations that disconnectZooCode() and postStateToWebview() were called (in addition to the existing upsertProviderProfile assertion) to ensure the handler still disconnects and pushes updated state.src/api/providers/__tests__/zoo-gateway.spec.ts (1)
209-247: ⚡ Quick winReplace one duplicate unauthenticated test with the token-refresh regression.
These two cases cover the same failure path. One of them would be more valuable as a regression that constructs
ZooGatewayHandlerbefore flipping the mocked cached token, then asserts the next request uses the refreshed bearer token. That would lock in the runtime-resolution behavior this PR is trying to add.As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."
🤖 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 `@src/api/providers/__tests__/zoo-gateway.spec.ts` around lines 209 - 247, The duplicate unauthenticated test should be replaced with a token-refresh regression: keep the first failing unauthenticated test, then change the second test to construct a ZooGatewayHandler({}) before toggling the mocked cached token value, simulate a refreshed bearer token (update whatever mock for cached token/provider returns), call handler.createMessage("You are helpful.", [{ role: "user", content: "Hello" }]) and drain the stream, and assert that mockCreate was invoked with an Authorization header containing the refreshed bearer token (or that the outgoing request used the new token); locate symbols ZooGatewayHandler, createMessage, mockCreate and the cached-token mock to implement this behavior. Ensure the test flips the cached token after handler instantiation to validate runtime-resolution of the token.src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts (1)
8-12: ⚡ Quick winExercise the new resolver path in this suite.
The added mock still behaves like the old implementation, so these tests never prove that
getZooGatewayModels()usesresolveZooGatewaySessionToken()rather thanoptions?.zooSessionTokendirectly. Add one case wherezooSessionTokenis omitted, the resolver returns a sentinel token, and the request asserts that sentinel inAuthorization.Suggested test shape
vitest.mock("../../../../services/zoo-code-auth", () => ({ getCachedZooCodeToken: vitest.fn(() => ""), getZooCodeBaseUrl: vitest.fn(() => "https://example.test"), - resolveZooGatewaySessionToken: vitest.fn((profileToken?: string) => profileToken || undefined), + resolveZooGatewaySessionToken: vitest.fn(), })) + +import { resolveZooGatewaySessionToken } from "../../../../services/zoo-code-auth" + +const mockedResolveZooGatewaySessionToken = vitest.mocked(resolveZooGatewaySessionToken) describe("Zoo Gateway Fetchers", () => { beforeEach(() => { vitest.clearAllMocks() }) @@ + it("uses the resolved session token when no profile token is provided", async () => { + mockedResolveZooGatewaySessionToken.mockReturnValueOnce("zoo_ext_cached_token") + mockedAxios.get.mockResolvedValueOnce(mockResponse) + + await getZooGatewayModels({ zooGatewayBaseUrl: baseUrl } as any) + + expect(mockedResolveZooGatewaySessionToken).toHaveBeenCalledWith(undefined) + expect(mockedAxios.get).toHaveBeenCalledWith( + `${baseUrl}/models`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer zoo_ext_cached_token" }), + }), + ) + })As per coding guidelines,
**/{__tests__,tests,test}/**/*.{test,spec}.{ts,tsx,js}should use package-local unit tests for pure logic, request construction, retry decisions, and error handling.Also applies to: 61-87
🤖 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 `@src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts` around lines 8 - 12, Add a new test in the zoo-gateway.spec.ts suite that omits the options.zooSessionToken when calling getZooGatewayModels(), configures the vitest mock for resolveZooGatewaySessionToken to return a sentinel string (e.g., "SENTINEL_TOKEN"), and asserts that the outgoing request includes Authorization: `Bearer SENTINEL_TOKEN`; locate the call site to getZooGatewayModels(), update the mock for resolveZooGatewaySessionToken in the test to return the sentinel, perform the request, and assert the Authorization header matches the sentinel to prove getZooGatewayModels() uses resolveZooGatewaySessionToken() rather than options?.zooSessionToken.
🤖 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 `@src/core/webview/__tests__/ClineProvider.spec.ts`:
- Around line 3763-3775: The test titled "logs and posts state when profile
persistence fails" only checks the error log but should also assert that
postStateToWebview was called; update the spec after calling
provider.handleZooCodeCallback("zoo_ext_token") to include an assertion that
provider.postStateToWebview was invoked (e.g.,
expect(provider.postStateToWebview).toHaveBeenCalled() or
toHaveBeenCalledWith(...) ). Ensure you reference the existing mocks/spies on
provider.getState (mockRejectedValue), provider.postStateToWebview
(mockResolvedValue), and the providerSettingsManager stub so the new assertion
verifies the state refresh happens even when getState fails.
In `@src/core/webview/__tests__/webviewMessageHandler.spec.ts`:
- Around line 371-372: The test asserts that "zoo-gateway" models are fetched
but the shared getState() mock never sets apiConfiguration.zooGatewayBaseUrl, so
either seed apiConfiguration.zooGatewayBaseUrl in the mocked getState for this
suite (and the other failing cases around the same area) or change the
expectations to not include "zoo-gateway" when that config is absent; update the
relevant tests in webviewMessageHandler.spec.ts (the suite using the mocked
getState and assertions that compare against mockModels for "zoo-gateway"
alongside "vercel-ai-gateway") to ensure the mocked state and expectations match
the production contract.
In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 2452-2487: The loop over
provider.providerSettingsManager.listConfig() should not be aborted by a single
profile failure: wrap the per-profile work (calls to
provider.providerSettingsManager.getProfile, provider.upsertProviderProfile, and
provider.providerSettingsManager.saveConfig) in its own try/catch so errors for
one entry are logged via provider.log but do not break the for loop; keep the
existing logic for isThisProfileActive (using currentSettings.apiProvider and
currentApiConfigName) and only call saveConfig when profile.zooSessionToken was
present, and always attempt upsertProviderProfile for the active profile even if
its persisted token was already cleared.
---
Outside diff comments:
In `@src/api/providers/zoo-gateway.ts`:
- Around line 133-154: The client is created once with apiKey: sessionToken
which can go stale; fix by re-resolving the Zoo session token (call
resolveZooGatewaySessionToken / ensureAuthenticated) inside each request path
and inject an explicit per-request Authorization header (Authorization: Bearer
<token>) into the headers you pass to the parent OpenAI client rather than
relying on the initially constructed apiKey; specifically update
createMessage(...) and completePrompt(...) to call resolveZooGatewaySessionToken
(or ensureAuthenticated) at the start and merge Authorization into the
per-request headers (alongside the existing X-Zoo-* enrichment headers) so every
outgoing call uses the latest token.
In `@src/services/zoo-code-auth.ts`:
- Around line 55-65: When secretStorage changes for ZOO_CODE_TOKEN_KEY in the
context.secrets.onDidChange handler, also propagate external removals into the
auth state by setting _sessionCleared = true when the retrieved token is
undefined (or when the change indicates removal); update the handler that
currently sets _cachedToken, _cachedSubscriptionStatus, and
_lastSubscriptionCheck to additionally flip _sessionCleared (and only clear it
when a fresh token is present), so resolveZooGatewaySessionToken and related
functions (e.g., resolveZooGatewaySessionToken, checkSubscriptionStatus) no
longer fall back to stale profile tokens after a remote sign-out.
---
Nitpick comments:
In `@src/api/providers/__tests__/zoo-gateway.spec.ts`:
- Around line 209-247: The duplicate unauthenticated test should be replaced
with a token-refresh regression: keep the first failing unauthenticated test,
then change the second test to construct a ZooGatewayHandler({}) before toggling
the mocked cached token value, simulate a refreshed bearer token (update
whatever mock for cached token/provider returns), call
handler.createMessage("You are helpful.", [{ role: "user", content: "Hello" }])
and drain the stream, and assert that mockCreate was invoked with an
Authorization header containing the refreshed bearer token (or that the outgoing
request used the new token); locate symbols ZooGatewayHandler, createMessage,
mockCreate and the cached-token mock to implement this behavior. Ensure the test
flips the cached token after handler instantiation to validate
runtime-resolution of the token.
In `@src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts`:
- Around line 8-12: Add a new test in the zoo-gateway.spec.ts suite that omits
the options.zooSessionToken when calling getZooGatewayModels(), configures the
vitest mock for resolveZooGatewaySessionToken to return a sentinel string (e.g.,
"SENTINEL_TOKEN"), and asserts that the outgoing request includes Authorization:
`Bearer SENTINEL_TOKEN`; locate the call site to getZooGatewayModels(), update
the mock for resolveZooGatewaySessionToken in the test to return the sentinel,
perform the request, and assert the Authorization header matches the sentinel to
prove getZooGatewayModels() uses resolveZooGatewaySessionToken() rather than
options?.zooSessionToken.
In `@src/core/webview/__tests__/webviewMessageHandler.spec.ts`:
- Around line 1154-1179: The test only asserts upsertProviderProfile was called
but must also verify the side effects disconnectZooCode and postStateToWebview
occur when disk already has no token; update the spec for webviewMessageHandler
to stub/mock mockClineProvider.disconnectZooCode and
mockClineProvider.postStateToWebview (or their equivalents on the mocked
provider), call webviewMessageHandler as before, and add expectations that
disconnectZooCode() and postStateToWebview() were called (in addition to the
existing upsertProviderProfile assertion) to ensure the handler still
disconnects and pushes updated state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 45d0dd2c-712e-4d9b-b203-354348b966ab
📒 Files selected for processing (31)
src/activate/__tests__/handleUri.spec.tssrc/activate/handleUri.tssrc/api/providers/__tests__/zoo-gateway.spec.tssrc/api/providers/fetchers/__tests__/zoo-gateway.spec.tssrc/api/providers/fetchers/zoo-gateway.tssrc/api/providers/zoo-gateway.tssrc/core/webview/ClineProvider.tssrc/core/webview/__tests__/ClineProvider.spec.tssrc/core/webview/__tests__/webviewMessageHandler.spec.tssrc/core/webview/webviewMessageHandler.tssrc/i18n/locales/ca/common.jsonsrc/i18n/locales/de/common.jsonsrc/i18n/locales/en/common.jsonsrc/i18n/locales/es/common.jsonsrc/i18n/locales/fr/common.jsonsrc/i18n/locales/hi/common.jsonsrc/i18n/locales/id/common.jsonsrc/i18n/locales/it/common.jsonsrc/i18n/locales/ja/common.jsonsrc/i18n/locales/ko/common.jsonsrc/i18n/locales/nl/common.jsonsrc/i18n/locales/pl/common.jsonsrc/i18n/locales/pt-BR/common.jsonsrc/i18n/locales/ru/common.jsonsrc/i18n/locales/tr/common.jsonsrc/i18n/locales/vi/common.jsonsrc/i18n/locales/zh-CN/common.jsonsrc/i18n/locales/zh-TW/common.jsonsrc/services/__tests__/zoo-code-auth.test.tssrc/services/zoo-code-auth.tswebview-ui/src/components/welcome/WelcomeViewProvider.tsx
| "vercel-ai-gateway": mockModels, | ||
| "zoo-gateway": mockModels, |
There was a problem hiding this comment.
Seed zooGatewayBaseUrl before expecting zoo-gateway models here.
This suite’s shared getState() mock never sets apiConfiguration.zooGatewayBaseUrl, but these assertions now expect zoo-gateway fetches to succeed anyway. That no longer matches the production contract described in this PR, so the test can pass against an impossible state and stop catching a missing-base-URL regression. Either add zooGatewayBaseUrl to the mocked config for these cases or keep zoo-gateway empty when that config is absent. As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."
Also applies to: 459-460, 517-518
🤖 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 `@src/core/webview/__tests__/webviewMessageHandler.spec.ts` around lines 371 -
372, The test asserts that "zoo-gateway" models are fetched but the shared
getState() mock never sets apiConfiguration.zooGatewayBaseUrl, so either seed
apiConfiguration.zooGatewayBaseUrl in the mocked getState for this suite (and
the other failing cases around the same area) or change the expectations to not
include "zoo-gateway" when that config is absent; update the relevant tests in
webviewMessageHandler.spec.ts (the suite using the mocked getState and
assertions that compare against mockModels for "zoo-gateway" alongside
"vercel-ai-gateway") to ensure the mocked state and expectations match the
production contract.
There was a problem hiding this comment.
Skipping. The premise does not hold against the current code: getZooGatewayModels self-resolves the base URL (options?.zooGatewayBaseUrl ?? getZooCodeBaseUrl()/api/gateway/v1), so zooGatewayBaseUrl is an optional override, not a required field. The candidate in requestRouterModels passes it through as-is and the mocked getModels returns mockModels for every provider regardless, so these assertions match the production contract. There is no missing-base-URL state to regress here.
There was a problem hiding this comment.
@JamesRobert20, understood — that's a clear and correct explanation. Since getZooGatewayModels falls back to getZooCodeBaseUrl()/api/gateway/v1 when no zooGatewayBaseUrl is provided, the field is genuinely optional and the test assertions are valid as-is. My original concern was incorrect. Thanks for the clarification.
✏️ Learnings added
Learnt from: JamesRobert20
Repo: Zoo-Code-Org/Zoo-Code PR: 347
File: src/core/webview/__tests__/webviewMessageHandler.spec.ts:371-372
Timestamp: 2026-06-03T01:59:47.117Z
Learning: In Zoo-Code (`src/api/providers/fetchers/zoo-gateway.ts`), `getZooGatewayModels` self-resolves the base URL via `options?.zooGatewayBaseUrl ?? getZooCodeBaseUrl() + "/api/gateway/v1"`. Therefore `zooGatewayBaseUrl` is an optional override, not a required field. In `requestRouterModels` (webviewMessageHandler), the candidate passes `zooGatewayBaseUrl` through as-is, and the zoo-gateway fetch succeeds even when the config omits that field. Do not flag missing `zooGatewayBaseUrl` in test mocks as a contract violation.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts : For DeepSeek V4 E2E tests, redirect the OpenAI-compatible DeepSeek client through aimock by setting `deepSeekBaseUrl: ${AIMOCK_URL}/v1`.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/**/*.test.ts : Always restore the default OpenRouter config in `suiteTeardown` when tests change persisted provider/model settings, so subsequent suites are unaffected.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/**/*.test.ts : For fetch-interceptor test suites, reset in-memory request/event capture in `setup()` or allocate a fresh per-test buffer instead of reusing shared mutable state; scope request-shape assertions to the current probe or test tag only; do not pull in older requests.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-14T15:41:10.836Z
Learning: Applies to **/webview-ui/**/{__tests__,tests,test}/**/*.{test,spec}.{ts,tsx} : Use `webview-ui` tests for React rendering, hooks, component state, forms, validation, and webview UI wiring.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/**/*.test.ts : When using a non-default provider (e.g. Anthropic) in E2E tests, point aimock at the endpoint by passing `anthropicBaseUrl: aimockUrl` (without a `/v1` suffix) to `api.setConfiguration()`.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/providers/xai.test.ts : For xAI Grok E2E tests, patch `globalThis.fetch` to intercept requests to the Responses API endpoint. When a local `fixtures/xai.json` recording exists, it can replay recorded real-API SSE events; otherwise fall back to hand-crafted SSE events using a hardcoded `readCallId`.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/providers/zai.test.ts : When adding a test to the Z.ai GLM suite, add a matching fixture to the `installZAiFetchInterceptor` call in `suiteSetup` using a short unique prefix (e.g. `"zai-glm-e2e-mytest:"`) that won't appear in `<environment_details>`.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: apps/vscode-e2e/AGENTS.md:0-0
Timestamp: 2026-05-21T22:27:19.778Z
Learning: Applies to apps/vscode-e2e/src/suite/providers/xai.test.ts : When adding a new test to the xAI Grok suite, update the hand-crafted interceptor response with a short unique probe tag (e.g. `"xai-e2e:grok-4.20"`) that won't appear in `<environment_details>`.
Learnt from: F915
Repo: Zoo-Code-Org/Zoo-Code PR: 421
File: src/api/providers/fetchers/modelCache.ts:100-126
Timestamp: 2026-06-01T13:21:27.362Z
Learning: In Zoo-Code (src/api/providers/fetchers/modelCache.ts), the Bailian provider intentionally uses a provider-only cache key ("bailian") rather than a per-baseUrl key. This is a documented design trade-off: full per-baseUrl keying would require changing shared infrastructure signatures (getModelsFromCache, writeModels, readModels, inFlightRefresh) used by 10+ providers. Safety is maintained by: (1) most users stay on a single region, (2) region switches call flushModels(refresh=true) which fetches fresh data first, (3) 5-minute memory cache TTL, (4) workspace-dependent URLs validated in the BailianHandler constructor. The planned future fix is a "bailian:<normalizedBaseUrl>" composite key. Do not re-flag this as a bug.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-14T15:41:10.836Z
Learning: Applies to **/{__tests__,tests,test}/**/*.{test,spec}.{ts,tsx,js} : Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling.
Learnt from: CR
Repo: Zoo-Code-Org/Zoo-Code PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-14T15:41:10.836Z
Learning: Applies to apps/vscode-e2e/**/*.{test,spec}.{ts,tsx} : Keep e2e tests focused on high-value smoke coverage across boundaries. Avoid placing detailed protocol, parsing, storage, retry, or edge-case assertions in e2e when they can be covered reliably at a lower layer.
…efresh Wrap per-profile work in the zoo-gateway sign-out cleanup loop in its own try/catch so one corrupted profile or failed write no longer aborts cleanup of the remaining profiles. Also assert postStateToWebview runs on the handleZooCodeCallback persistence-failure path. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Part 3 of the Zoo Gateway stack. Depends on PR2 (stacked on #344).
Test plan
Summary by CodeRabbit
New Features
Error Handling
Internationalization