Skip to content

Commit 152bb1c

Browse files
James Mtendamemacursoragent
andcommitted
fix(zoo-gateway): address PR review feedback on auth, seeding, and errors
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f11ec84 commit 152bb1c

7 files changed

Lines changed: 85 additions & 24 deletions

File tree

src/activate/__tests__/handleUri.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,24 @@ describe("handleUri", () => {
177177

178178
expect(order).toEqual(["a:start", "a:end", "b:start", "b:end"])
179179
})
180+
181+
it("continues fan-out when one instance fails to persist the callback token", async () => {
182+
mockHandleZooCodeAuthCallback.mockResolvedValue(true)
183+
184+
const failingProvider = {
185+
handleZooCodeCallback: vi.fn(async () => {
186+
throw new Error("profile store unavailable")
187+
}),
188+
} as any
189+
const healthyProvider = { handleZooCodeCallback: vi.fn() } as any
190+
mockGetAllInstances.mockReturnValue([failingProvider, healthyProvider])
191+
192+
await handleUri({
193+
path: "/auth-callback",
194+
query: "token=zoo_ext_test_token",
195+
} as any)
196+
197+
expect(failingProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token")
198+
expect(healthyProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token")
199+
})
180200
})

src/activate/handleUri.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ export const handleUri = async (uri: vscode.Uri) => {
6363
// is cheap (at most a handful of instances) and avoids the race.
6464
const allInstances = ClineProvider.getAllInstances()
6565
for (const instance of allInstances) {
66-
await instance.handleZooCodeCallback(token)
66+
try {
67+
await instance.handleZooCodeCallback(token)
68+
} catch (error) {
69+
console.error(
70+
"Failed to persist Zoo Gateway token for a provider instance:",
71+
error instanceof Error ? error.message : error,
72+
)
73+
}
6774
}
6875
}
6976
}

src/api/providers/__tests__/zoo-gateway.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ vitest.mock("../fetchers/modelCache", () => ({
5656
getModelsFromCache: vitest.fn().mockReturnValue(undefined),
5757
}))
5858

59+
const mockGetCachedZooCodeToken = vitest.hoisted(() => vitest.fn<() => string | undefined>(() => undefined))
60+
5961
vitest.mock("../../../services/zoo-code-auth", () => ({
6062
getZooCodeBaseUrl: vitest.fn(() => "https://www.zoocode.dev"),
61-
getCachedZooCodeToken: vitest.fn(() => undefined),
63+
getCachedZooCodeToken: mockGetCachedZooCodeToken,
6264
clearZooCodeToken: vitest.fn(async () => undefined),
6365
}))
6466

@@ -91,6 +93,7 @@ describe("ZooGatewayHandler", () => {
9193

9294
beforeEach(() => {
9395
vitest.clearAllMocks()
96+
mockGetCachedZooCodeToken.mockReturnValue(undefined)
9497
mockCreate.mockClear()
9598
showErrorMessage.mockReset()
9699
showErrorMessage.mockResolvedValue(undefined)
@@ -126,6 +129,21 @@ describe("ZooGatewayHandler", () => {
126129
expect(OpenAI).not.toHaveBeenCalled()
127130
})
128131

132+
it("prefers the secret-storage cache over a persisted profile token", () => {
133+
mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_cached_token")
134+
135+
new ZooGatewayHandler({
136+
zooSessionToken: "zoo_ext_stale_profile_token",
137+
zooGatewayModelId: mockOptions.zooGatewayModelId,
138+
})
139+
140+
expect(OpenAI).toHaveBeenCalledWith(
141+
expect.objectContaining({
142+
apiKey: "zoo_ext_cached_token",
143+
}),
144+
)
145+
})
146+
129147
it("initializes OpenAI with Zoo enrichment headers and session token", () => {
130148
const handler = new ZooGatewayHandler({
131149
...mockOptions,

src/api/providers/zoo-gateway.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,9 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio
9999
constructor(options: ApiHandlerOptions) {
100100
const baseURL = options.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1`
101101

102-
// Prefer the profile-persisted token; fall back to the secret-storage cache so
103-
// requests work when the user is signed in but the profile hasn't been seeded yet
104-
// (e.g. auth callback arrived before any webview instance was open).
105-
const sessionToken = options.zooSessionToken || getCachedZooCodeToken()
102+
// Prefer the secret-storage cache so a 401 clear takes effect immediately; fall back
103+
// to the profile-persisted token when the user is signed in but seeding hasn't run yet.
104+
const sessionToken = getCachedZooCodeToken() || options.zooSessionToken
106105
if (!sessionToken) {
107106
throw new Error("Zoo Gateway requires authentication. Please sign in to Zoo Code first.")
108107
}
@@ -210,7 +209,12 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio
210209
}
211210
}
212211
} catch (error) {
213-
void surfaceGatewayApiError(error)
212+
void surfaceGatewayApiError(error).catch((surfaceError) => {
213+
console.error(
214+
"Failed to surface Zoo Gateway error:",
215+
surfaceError instanceof Error ? surfaceError.message : surfaceError,
216+
)
217+
})
214218
throw error
215219
}
216220
}
@@ -234,7 +238,12 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio
234238
const response = await this.client.chat.completions.create(requestOptions)
235239
return response.choices[0]?.message.content || ""
236240
} catch (error) {
237-
void surfaceGatewayApiError(error)
241+
void surfaceGatewayApiError(error).catch((surfaceError) => {
242+
console.error(
243+
"Failed to surface Zoo Gateway error:",
244+
surfaceError instanceof Error ? surfaceError.message : surfaceError,
245+
)
246+
})
238247
if (error instanceof Error) {
239248
throw new Error(`Zoo Gateway completion error: ${error.message}`)
240249
}

src/core/webview/ClineProvider.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -871,9 +871,10 @@ export class ClineProvider
871871
* Called once per webview init; handleZooCodeCallback is idempotent so repeated calls are safe.
872872
*/
873873
private async ensureZooGatewayProfileSeeded(): Promise<void> {
874-
const { getCachedZooCodeToken } = await import("../../services/zoo-code-auth")
874+
const { getCachedZooCodeToken, getZooCodeBaseUrl } = await import("../../services/zoo-code-auth")
875875
const token = getCachedZooCodeToken()
876876
if (!token) return
877+
const expectedGatewayBaseUrl = `${getZooCodeBaseUrl()}/api/gateway/v1`
877878

878879
// Check ALL zoo-gateway profiles — only skip seeding if every profile has the current token.
879880
// Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback
@@ -889,13 +890,12 @@ export class ClineProvider
889890
for (const entry of zooGatewayProfiles) {
890891
try {
891892
const fullProfile = await this.providerSettingsManager.getProfile({ name: entry.name })
892-
if (fullProfile.zooSessionToken !== token) {
893+
if (
894+
fullProfile.zooSessionToken !== token ||
895+
fullProfile.zooGatewayBaseUrl !== expectedGatewayBaseUrl
896+
) {
893897
allUpToDate = false
894-
this.log(
895-
fullProfile.zooSessionToken
896-
? "[ensureZooGatewayProfileSeeded] Token mismatch (stale session?), updating with current token"
897-
: "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token",
898-
)
898+
this.log("[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile is stale, updating")
899899
break
900900
}
901901
} catch {

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -955,13 +955,16 @@ export const webviewMessageHandler = async (
955955
if (!zooGatewayToken) {
956956
try {
957957
const allProfiles = await provider.providerSettingsManager.listConfig()
958-
const zooGatewayProfile = allProfiles.find((p) => p.apiProvider === "zoo-gateway")
959-
if (zooGatewayProfile) {
958+
const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway")
959+
for (const profileMeta of zooGatewayProfiles) {
960960
const fullProfile = await provider.providerSettingsManager.getProfile({
961-
name: zooGatewayProfile.name,
961+
name: profileMeta.name,
962962
})
963-
zooGatewayToken = fullProfile.zooSessionToken
964-
zooGatewayBaseUrl = fullProfile.zooGatewayBaseUrl ?? zooGatewayBaseUrl
963+
if (fullProfile.zooSessionToken) {
964+
zooGatewayToken = fullProfile.zooSessionToken
965+
zooGatewayBaseUrl = fullProfile.zooGatewayBaseUrl ?? zooGatewayBaseUrl
966+
break
967+
}
965968
}
966969
} catch (error) {
967970
console.debug("Failed to look up zoo-gateway profile for model fetch:", error)

webview-ui/src/components/welcome/WelcomeViewProvider.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ const DEFAULT_WELCOME_API_CONFIGURATION: ProviderSettings = {
2020
openRouterModelId: openRouterDefaultModelId,
2121
}
2222

23-
const getWelcomeApiConfiguration = (apiConfiguration?: ProviderSettings): ProviderSettings => {
23+
const getWelcomeApiConfiguration = (
24+
apiConfiguration?: ProviderSettings,
25+
zooCodeIsAuthenticated?: boolean,
26+
): ProviderSettings => {
2427
// validateApiConfiguration treats a missing apiProvider as valid (no switch case matches),
2528
// so we explicitly fall back here before delegating to it for incomplete-but-set configs.
2629
if (!apiConfiguration?.apiProvider) {
2730
return DEFAULT_WELCOME_API_CONFIGURATION
2831
}
2932

30-
const validationError = validateApiConfiguration(apiConfiguration)
33+
const validationError = validateApiConfiguration(apiConfiguration, undefined, undefined, zooCodeIsAuthenticated)
3134
if (validationError) {
3235
return DEFAULT_WELCOME_API_CONFIGURATION
3336
}
@@ -42,7 +45,8 @@ const WelcomeViewProvider = () => {
4245
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
4346
const [showProviderSetup, setShowProviderSetup] = useState(false)
4447
const [welcomeApiConfiguration, setWelcomeApiConfiguration] = useState<ProviderSettings>()
45-
const effectiveApiConfiguration = welcomeApiConfiguration ?? getWelcomeApiConfiguration(apiConfiguration)
48+
const effectiveApiConfiguration =
49+
welcomeApiConfiguration ?? getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated)
4650

4751
const setApiConfigurationFieldForApiOptions = useCallback(
4852
<K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => {
@@ -57,7 +61,7 @@ const WelcomeViewProvider = () => {
5761

5862
const handleGetStarted = useCallback(() => {
5963
if (!showProviderSetup) {
60-
const initialApiConfiguration = getWelcomeApiConfiguration(apiConfiguration)
64+
const initialApiConfiguration = getWelcomeApiConfiguration(apiConfiguration, zooCodeIsAuthenticated)
6165
setWelcomeApiConfiguration(initialApiConfiguration)
6266

6367
setApiConfiguration(initialApiConfiguration)

0 commit comments

Comments
 (0)