From 4548a01c974a7e576a1d19a15b8ace7751c8c92b Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 20:50:55 +0300 Subject: [PATCH] fix(cli-installer): serialize provider status checks by default --- src/main/ipc/cliInstaller.test.ts | 69 ++++++++++++++++++++++++++++--- src/main/ipc/cliInstaller.ts | 9 +++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/main/ipc/cliInstaller.test.ts b/src/main/ipc/cliInstaller.test.ts index f081fcfe..a3ef8319 100644 --- a/src/main/ipc/cliInstaller.test.ts +++ b/src/main/ipc/cliInstaller.test.ts @@ -5,7 +5,7 @@ import { CLI_INSTALLER_VERIFY_PROVIDER_MODELS, } from '@preload/constants/ipcChannels'; import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; -import { describe, expect, test, vi } from 'vitest'; +import { afterEach, describe, expect, test, vi } from 'vitest'; import { initializeCliInstallerHandlers, registerCliInstallerHandlers } from './cliInstaller'; @@ -35,6 +35,12 @@ interface Deferred { type IpcHandler = (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown; +const PARALLEL_PROVIDER_STATUS_ENV = 'CLAUDE_TEAM_PARALLEL_PROVIDER_STATUS'; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + function createDeferred(): Deferred { let resolve!: (value: T) => void; let reject!: (error: unknown) => void; @@ -137,7 +143,48 @@ function setupHandlers(service: CliInstallerService): ReturnType { - test('runs different provider status requests concurrently up to the provider limit', async () => { + test('runs provider status requests sequentially by default', async () => { + const started: CliProviderId[] = []; + const deferredByProvider = new Map>(); + const service = createInstallerService({ + getProviderStatus: vi.fn((providerId: CliProviderId) => { + started.push(providerId); + const deferred = createDeferred(); + deferredByProvider.set(providerId, deferred); + return deferred.promise; + }), + }); + const { invoke } = setupHandlers(service); + + const requests = (['anthropic', 'codex', 'opencode', 'gemini'] as CliProviderId[]).map( + (providerId) => + invoke>(CLI_INSTALLER_GET_PROVIDER_STATUS, providerId) + ); + + await flushMicrotasks(); + expect(started).toEqual(['anthropic']); + + deferredByProvider.get('anthropic')?.resolve(createProviderStatus('anthropic')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex']); + + deferredByProvider.get('codex')?.resolve(createProviderStatus('codex')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex', 'opencode']); + + deferredByProvider.get('opencode')?.resolve(createProviderStatus('opencode')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex', 'opencode', 'gemini']); + + deferredByProvider.get('gemini')?.resolve(createProviderStatus('gemini')); + + const results = await Promise.all(requests); + expect(results.every((result) => result.success)).toBe(true); + }); + + test('runs different provider status requests concurrently when the parallel flag is enabled', async () => { + vi.stubEnv(PARALLEL_PROVIDER_STATUS_ENV, '1'); + const started: CliProviderId[] = []; const deferredByProvider = new Map>(); const service = createInstallerService({ @@ -258,12 +305,18 @@ describe('cliInstaller IPC provider runtime scheduling', () => { ); await flushMicrotasks(); - expect(started).toEqual(['anthropic', 'codex', 'opencode']); + expect(started).toEqual(['anthropic']); initializeCliInstallerHandlers(replacementService); deferredByProvider.get('anthropic')?.resolve(createProviderStatus('anthropic')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex']); + deferredByProvider.get('codex')?.resolve(createProviderStatus('codex')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex', 'opencode']); + deferredByProvider.get('opencode')?.resolve(createProviderStatus('opencode')); await flushMicrotasks(); @@ -294,14 +347,20 @@ describe('cliInstaller IPC provider runtime scheduling', () => { ); await flushMicrotasks(); - expect(started).toEqual(['anthropic', 'codex', 'opencode']); + expect(started).toEqual(['anthropic']); deferredByProvider.get('anthropic')?.reject(new Error('provider failed')); await flushMicrotasks(); - expect(started).toEqual(['anthropic', 'codex', 'opencode', 'gemini']); + expect(started).toEqual(['anthropic', 'codex']); deferredByProvider.get('codex')?.resolve(createProviderStatus('codex')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex', 'opencode']); + deferredByProvider.get('opencode')?.resolve(createProviderStatus('opencode')); + await flushMicrotasks(); + expect(started).toEqual(['anthropic', 'codex', 'opencode', 'gemini']); + deferredByProvider.get('gemini')?.resolve(createProviderStatus('gemini')); const results = await Promise.all(requests); diff --git a/src/main/ipc/cliInstaller.ts b/src/main/ipc/cliInstaller.ts index 0b11a907..920579c7 100644 --- a/src/main/ipc/cliInstaller.ts +++ b/src/main/ipc/cliInstaller.ts @@ -48,6 +48,7 @@ const cachedStatus = new Map< let statusCacheGeneration = 0; const STATUS_CACHE_TTL_MS = 5_000; const MAX_PARALLEL_PROVIDER_RUNTIME_REQUESTS = 3; +const PARALLEL_PROVIDER_STATUS_ENV = 'CLAUDE_TEAM_PARALLEL_PROVIDER_STATUS'; const FRONTEND_MULTIMODEL_PROVIDER_IDS = new Set(['anthropic', 'codex', 'opencode']); function isFrontendMultimodelProviderId(providerId: CliProviderId): boolean { @@ -120,8 +121,14 @@ function resetProviderRuntimeRequestLimiter(): void { } } +function getProviderRuntimeRequestLimit(): number { + return process.env[PARALLEL_PROVIDER_STATUS_ENV] === '1' + ? MAX_PARALLEL_PROVIDER_RUNTIME_REQUESTS + : 1; +} + function acquireProviderRuntimeRequestSlot(): Promise { - if (activeProviderRuntimeRequestCount < MAX_PARALLEL_PROVIDER_RUNTIME_REQUESTS) { + if (activeProviderRuntimeRequestCount < getProviderRuntimeRequestLimit()) { activeProviderRuntimeRequestCount += 1; return Promise.resolve(); }