diff --git a/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts b/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts index 61d0047d..34f06676 100644 --- a/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts +++ b/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts @@ -5,6 +5,15 @@ import { isTeamProvisioningActive, selectTeamDataForName } from '@renderer/store import { useShallow } from 'zustand/react/shallow'; const TEAM_AGENT_RUNTIME_REFRESH_MS = 10_000; +const ACTIVE_TEAM_AGENT_RUNTIME_REFRESH_MS = 5_000; + +interface TeamAgentRuntimeWatchEntry { + refCount: number; + timer: number; + inFlight: boolean; +} + +const teamAgentRuntimeWatchEntries = new Map(); export function shouldWatchTeamAgentRuntime(input: { enabled: boolean; @@ -19,6 +28,13 @@ export function shouldWatchTeamAgentRuntime(input: { return input.leadActivity === 'active' || input.leadActivity === 'idle'; } +export function __resetTeamAgentRuntimeWatcherForTests(): void { + for (const entry of teamAgentRuntimeWatchEntries.values()) { + window.clearInterval(entry.timer); + } + teamAgentRuntimeWatchEntries.clear(); +} + interface TeamAgentRuntimeWatcherOptions { teamName: string; enabled: boolean; @@ -54,12 +70,38 @@ export function useTeamAgentRuntimeWatcher({ }); if (!shouldWatch) return; - void fetchTeamAgentRuntime(teamName); - const timer = window.setInterval(() => { - void fetchTeamAgentRuntime(teamName); - }, TEAM_AGENT_RUNTIME_REFRESH_MS); + const existingEntry = teamAgentRuntimeWatchEntries.get(teamName); + if (existingEntry) { + existingEntry.refCount += 1; + return () => { + existingEntry.refCount -= 1; + if (existingEntry.refCount <= 0) { + window.clearInterval(existingEntry.timer); + teamAgentRuntimeWatchEntries.delete(teamName); + } + }; + } + + const refreshIntervalMs = + leadActivity === 'active' + ? ACTIVE_TEAM_AGENT_RUNTIME_REFRESH_MS + : TEAM_AGENT_RUNTIME_REFRESH_MS; + const entry: TeamAgentRuntimeWatchEntry = { + refCount: 1, + timer: window.setInterval(() => { + refreshTeamAgentRuntime(teamName, fetchTeamAgentRuntime); + }, refreshIntervalMs), + inFlight: false, + }; + teamAgentRuntimeWatchEntries.set(teamName, entry); + refreshTeamAgentRuntime(teamName, fetchTeamAgentRuntime); + return () => { - window.clearInterval(timer); + entry.refCount -= 1; + if (entry.refCount <= 0) { + window.clearInterval(entry.timer); + teamAgentRuntimeWatchEntries.delete(teamName); + } }; }, [ effectiveIsTeamAlive, @@ -70,3 +112,21 @@ export function useTeamAgentRuntimeWatcher({ teamName, ]); } + +function refreshTeamAgentRuntime( + teamName: string, + fetchTeamAgentRuntime: (teamName: string) => Promise +): void { + const entry = teamAgentRuntimeWatchEntries.get(teamName); + if (!entry || entry.inFlight) return; + + entry.inFlight = true; + void fetchTeamAgentRuntime(teamName) + .catch(() => undefined) + .finally(() => { + const latestEntry = teamAgentRuntimeWatchEntries.get(teamName); + if (latestEntry === entry) { + latestEntry.inFlight = false; + } + }); +} diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index 22d86baf..69d12f2c 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -667,7 +667,17 @@ function getSupplementalVisibleModels( return models; } - return [...models, ...ANTHROPIC_VISIBLE_MODEL_FALLBACKS]; + const existingLabels = new Set( + models + .map((model) => getTeamModelBadgeLabel(providerId, model)?.trim().toLowerCase()) + .filter((label): label is string => Boolean(label)) + ); + const supplementalModels = ANTHROPIC_VISIBLE_MODEL_FALLBACKS.filter((model) => { + const label = getTeamModelBadgeLabel(providerId, model)?.trim().toLowerCase(); + return !label || !existingLabels.has(label); + }); + + return [...models, ...supplementalModels]; } export function getVisibleTeamProviderModels( diff --git a/test/features/codex-account/main/CodexLoginSessionManager.test.ts b/test/features/codex-account/main/CodexLoginSessionManager.test.ts index 6494f32b..2e204822 100644 --- a/test/features/codex-account/main/CodexLoginSessionManager.test.ts +++ b/test/features/codex-account/main/CodexLoginSessionManager.test.ts @@ -37,21 +37,21 @@ function createSession(overrides?: { }); const close = overrides?.close ?? vi.fn().mockResolvedValue(undefined); - const session = { + const session: CodexAppServerSession = { initializeResponse: { userAgent: 'codex-test', codexHome: '/Users/tester/.codex', platformFamily: 'darwin', platformOs: 'macos', }, - request, - notify: vi.fn().mockResolvedValue(undefined), + request: request as CodexAppServerSession['request'], + notify: vi.fn().mockResolvedValue(undefined) as CodexAppServerSession['notify'], onNotification: vi.fn((listener: (method: string, params: unknown) => void) => { listeners.add(listener); return () => listeners.delete(listener); - }), - close, - } satisfies CodexAppServerSession; + }) as CodexAppServerSession['onNotification'], + close: close as CodexAppServerSession['close'], + }; return { session, diff --git a/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts b/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts index 9a5d96aa..9744e886 100644 --- a/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts +++ b/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { ListDashboardRecentProjectsUseCase } from '@features/recent-projects/core/application/use-cases/ListDashboardRecentProjectsUseCase'; @@ -33,15 +34,17 @@ function makeCandidate(overrides: Partial = {}): RecentP }; } -function createLogger(): LoggerPort & { - info: ReturnType; - warn: ReturnType; - error: ReturnType; -} { +type LoggerMock = LoggerPort & { + info: Mock; + warn: Mock; + error: Mock; +}; + +function createLogger(): LoggerMock { return { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), }; } diff --git a/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts index 20350c5e..784f0e20 100644 --- a/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { CodexRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter'; @@ -6,15 +7,17 @@ import type { LoggerPort } from '@features/recent-projects/core/application/port import type { CodexAppServerClient } from '@features/recent-projects/main/infrastructure/codex/CodexAppServerClient'; import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver'; -function createLogger(): LoggerPort & { - info: ReturnType; - warn: ReturnType; - error: ReturnType; -} { +type LoggerMock = LoggerPort & { + info: Mock; + warn: Mock; + error: Mock; +}; + +function createLogger(): LoggerMock { return { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), }; } diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index ee4efee0..e2b2badf 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -4,19 +4,22 @@ import path from 'node:path'; import { CodexSessionFileRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; import type { LoggerPort } from '@features/recent-projects/core/application/ports/LoggerPort'; import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver'; -function createLogger(): LoggerPort & { - info: ReturnType; - warn: ReturnType; - error: ReturnType; -} { +type LoggerMock = LoggerPort & { + info: Mock; + warn: Mock; + error: Mock; +}; + +function createLogger(): LoggerMock { return { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), }; } diff --git a/test/main/ipc/globalSearch.test.ts b/test/main/ipc/globalSearch.test.ts index a25da8e9..1d46dd2b 100644 --- a/test/main/ipc/globalSearch.test.ts +++ b/test/main/ipc/globalSearch.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { ProjectScanner } from '../../../src/main/services/discovery/ProjectScanner'; @@ -9,19 +10,24 @@ import type { Project, SearchSessionsResult } from '../../../src/main/types'; */ describe('Global Search - ProjectScanner.searchAllProjects', () => { let projectScanner: ProjectScanner; - let mockScan: ReturnType; - let mockSearchSessions: ReturnType; + let mockScan: Mock<() => Promise>; + let mockSearchSessions: Mock< + (projectId: string, query: string, maxResults?: number) => Promise + >; beforeEach(() => { // Create a real ProjectScanner instance projectScanner = new ProjectScanner(); // Mock the scan() method - mockScan = vi.fn(); + mockScan = vi.fn<() => Promise>(); projectScanner.scan = mockScan; // Mock the sessionSearcher.searchSessions() method - mockSearchSessions = vi.fn(); + mockSearchSessions = + vi.fn< + (projectId: string, query: string, maxResults?: number) => Promise + >(); // @ts-expect-error - Accessing private property for testing projectScanner.sessionSearcher = { searchSessions: mockSearchSessions, diff --git a/test/main/services/infrastructure/CliInstallerService.test.ts b/test/main/services/infrastructure/CliInstallerService.test.ts index 7b1cf99f..c827d0d6 100644 --- a/test/main/services/infrastructure/CliInstallerService.test.ts +++ b/test/main/services/infrastructure/CliInstallerService.test.ts @@ -80,6 +80,7 @@ vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({ env: { HOME: '/Users/tester' }, connectionIssues: {}, })), + getProviderStatusStoredCredentialAllowlist: vi.fn(() => undefined), })); vi.mock('@main/utils/cliAuthDiagLog', () => ({ @@ -542,7 +543,12 @@ describe('CliInstallerService', () => { it('falls back to the installed launcher path when --version reports unknown', async () => { allowConsoleLogs(); vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/Users/tester/.local/bin/claude'); - vi.spyOn(service as never, 'inferInstalledCliVersionFromPath').mockResolvedValue('2.1.101'); + vi.spyOn( + service as unknown as { + inferInstalledCliVersionFromPath: (binaryPath: string) => Promise; + }, + 'inferInstalledCliVersionFromPath' + ).mockResolvedValue('2.1.101'); vi.mocked(execCli) .mockResolvedValueOnce({ stdout: 'unknown', stderr: '' }) .mockResolvedValueOnce({ diff --git a/test/main/services/team/TeamDataService.test.ts b/test/main/services/team/TeamDataService.test.ts index 9f1f1a34..bf3e88bd 100644 --- a/test/main/services/team/TeamDataService.test.ts +++ b/test/main/services/team/TeamDataService.test.ts @@ -25,6 +25,27 @@ import type { const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING'; const tempPaths: string[] = []; +type TeamDataServicePrivate = { + extractLeadAssistantTextsFromJsonlLines( + rawLines: readonly string[], + leadName: string, + leadSessionId: string, + maxTexts: number + ): Promise; + getLeadSessionJsonlPaths(projectDir: string): Promise>; + extractLeadSessionTextsFromJsonl( + jsonlPath: string, + leadName: string, + leadSessionId: string, + maxTexts: number + ): Promise; + extractLeadSessionTexts(teamName: string, config: TeamConfig): Promise; +}; + +function teamDataServicePrivate(service: TeamDataService): TeamDataServicePrivate { + return service as unknown as TeamDataServicePrivate; +} + function createLeadAssistantEntry( uuid: string, timestamp: string, @@ -4690,7 +4711,7 @@ describe('TeamDataService', () => { ), ]); - const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines'); const extract = ( service as unknown as { extractLeadSessionTextsFromJsonl: ( @@ -4698,7 +4719,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -4730,11 +4751,11 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadAssistantTextsFromJsonlLines.bind(service); const assistantSpy = vi - .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines') .mockImplementation(async (...args: unknown[]) => { const [rawLines, leadName, leadSessionId, maxTexts] = args as [ readonly string[], @@ -4752,7 +4773,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -4782,12 +4803,12 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadAssistantTextsFromJsonlLines.bind(service); let appended = false; const assistantSpy = vi - .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines') .mockImplementation(async (...args: unknown[]) => { const [rawLines, leadName, leadSessionId, maxTexts] = args as [ readonly string[], @@ -4818,7 +4839,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -4847,7 +4868,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadAssistantTextsFromJsonlLines.bind(service); let releaseFirstInvocation = () => {}; @@ -4856,7 +4877,7 @@ describe('TeamDataService', () => { firstInvocationStartedResolve = resolve; }); const assistantSpy = vi - .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines') .mockImplementation(async (...args: unknown[]) => { const [rawLines, leadName, leadSessionId, maxTexts] = args as [ readonly string[], @@ -4879,7 +4900,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -4922,7 +4943,7 @@ describe('TeamDataService', () => { ), ]); - const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines'); const extract = ( service as unknown as { extractLeadSessionTextsFromJsonl: ( @@ -4956,7 +4977,7 @@ describe('TeamDataService', () => { ), ]); - const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines'); const extract = ( service as unknown as { extractLeadSessionTextsFromJsonl: ( @@ -4964,7 +4985,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -4988,7 +5009,7 @@ describe('TeamDataService', () => { ]); await fs.appendFile(jsonlPath, '{"type":"assistant"', 'utf8'); - const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines'); const extract = ( service as unknown as { extractLeadSessionTextsFromJsonl: ( @@ -4996,7 +5017,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -5036,7 +5057,7 @@ describe('TeamDataService', () => { ), ]); - const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines'); const extract = ( service as unknown as { extractLeadSessionTextsFromJsonl: ( @@ -5044,7 +5065,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -5073,12 +5094,12 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadAssistantTextsFromJsonlLines.bind(service); let shouldFail = true; const assistantSpy = vi - .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines') .mockImplementation(async (...args: unknown[]) => { const [rawLines, leadName, leadSessionId, maxTexts] = args as [ readonly string[], @@ -5098,7 +5119,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(service); @@ -5124,10 +5145,10 @@ describe('TeamDataService', () => { ), ]); - const firstSpy = vi.spyOn(firstService as never, 'extractLeadAssistantTextsFromJsonlLines' as never); + const firstSpy = vi.spyOn(teamDataServicePrivate(firstService), 'extractLeadAssistantTextsFromJsonlLines'); const secondSpy = vi.spyOn( - secondService as never, - 'extractLeadAssistantTextsFromJsonlLines' as never + teamDataServicePrivate(secondService), + 'extractLeadAssistantTextsFromJsonlLines' ); const firstExtract = ( firstService as unknown as { @@ -5136,7 +5157,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(firstService); const secondExtract = ( @@ -5146,7 +5167,7 @@ describe('TeamDataService', () => { leadName: string, leadSessionId: string, maxTexts: number - ) => Promise>; + ) => Promise; } ).extractLeadSessionTextsFromJsonl.bind(secondService); @@ -5177,10 +5198,10 @@ describe('TeamDataService', () => { }; (service as unknown as { projectResolver: typeof projectResolver }).projectResolver = projectResolver; - vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockResolvedValue( + vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockResolvedValue( new Map([['lead-1', '/fast-project/lead-1.jsonl']]) ); - vi.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never).mockResolvedValue([ + vi.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl').mockResolvedValue([ { from: 'fast-lead', text: 'Fast path recovered lead thought from the known lead session.', @@ -5228,7 +5249,7 @@ describe('TeamDataService', () => { }; (service as unknown as { projectResolver: typeof projectResolver }).projectResolver = projectResolver; - vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation( + vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation( (...args: unknown[]) => { const [projectDir] = args as [string]; if (projectDir === '/actual-project') { @@ -5237,7 +5258,7 @@ describe('TeamDataService', () => { return Promise.resolve(new Map()); } ); - vi.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never).mockResolvedValue([ + vi.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl').mockResolvedValue([ { from: 'actual-lead', text: 'Fallback path recovered lead thought from the repaired context.', @@ -5292,7 +5313,7 @@ describe('TeamDataService', () => { }; (service as unknown as { projectResolver: typeof projectResolver }).projectResolver = projectResolver; - vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation( + vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation( (...args: unknown[]) => { const [projectDir] = args as [string]; if (projectDir === '/current-project') { @@ -5304,7 +5325,7 @@ describe('TeamDataService', () => { } ); const extractSpy = vi - .spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl') .mockResolvedValue([ { from: 'current-lead', @@ -5366,11 +5387,11 @@ describe('TeamDataService', () => { (service as unknown as { projectResolver: typeof projectResolver }).projectResolver = projectResolver; const getPathsSpy = vi - .spyOn(service as never, 'getLeadSessionJsonlPaths' as never) + .spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths') .mockResolvedValueOnce(new Map([['lead-history', '/same-project/lead-history.jsonl']])) .mockResolvedValueOnce(new Map([['lead-current', '/same-project/lead-current.jsonl']])); const extractSpy = vi - .spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never) + .spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl') .mockResolvedValue([ { from: 'current-lead', @@ -5974,7 +5995,7 @@ describe('TeamDataService', () => { ], }); - vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockResolvedValue([ + vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockResolvedValue([ { from: 'team-lead', text: 'Lead summary', @@ -6028,7 +6049,7 @@ describe('TeamDataService', () => { resolveMembers: resolveMembersSpy, }); - vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockResolvedValue([ + vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockResolvedValue([ { from: 'team-lead', text: 'Lead summary', @@ -6110,7 +6131,7 @@ describe('TeamDataService', () => { }, }); - vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockImplementation( + vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockImplementation( () => { order.push('leadTexts:start'); throw new Error('lead sync fail'); diff --git a/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts b/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts index e53c8d03..78ae4d78 100644 --- a/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts +++ b/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts @@ -72,7 +72,12 @@ function createStubbedServiceHarness() { const service = new TeamMemberRuntimeAdvisoryService(logsFinder as never); const advisoryByFilePath = new Map(); const readRecentApiRetryAdvisory = vi - .spyOn(service as never, 'readRecentApiRetryAdvisory' as never) + .spyOn( + service as unknown as { + readRecentApiRetryAdvisory: (filePath: string) => Promise; + }, + 'readRecentApiRetryAdvisory' + ) .mockImplementation(async (...args: unknown[]) => { const filePath = String(args[0] ?? ''); if (advisoryByFilePath.has(filePath)) { diff --git a/test/main/services/team/TeamTaskActivityIntervalService.test.ts b/test/main/services/team/TeamTaskActivityIntervalService.test.ts index 3dd9de8d..57150d1a 100644 --- a/test/main/services/team/TeamTaskActivityIntervalService.test.ts +++ b/test/main/services/team/TeamTaskActivityIntervalService.test.ts @@ -1169,11 +1169,14 @@ describe('TeamTaskActivityIntervalService', () => { describe('resumeActiveIntervalsForMembers (batch)', () => { it('returns zero changes for an empty members list without scanning the tasks directory', () => { const service = new TeamTaskActivityIntervalService(); - const mutateSpy = vi.spyOn( + const lockSpy = vi.spyOn( service as unknown as { - mutateTeamTasks: TeamTaskActivityIntervalService['resumeActiveIntervalsForMember']; + mutateTeamTasksWithLock: ( + teamName: string, + run: () => { changedTasks: number; failed?: boolean } + ) => { changedTasks: number; failed?: boolean }; }, - 'mutateTeamTasks' + 'mutateTeamTasksWithLock' ); const result = service.resumeActiveIntervalsForMembers( @@ -1183,7 +1186,7 @@ describe('TeamTaskActivityIntervalService', () => { ); expect(result).toEqual({ changedTasks: 0 }); - expect(mutateSpy).not.toHaveBeenCalled(); + expect(lockSpy).not.toHaveBeenCalled(); }); it('returns zero changes when all member names are blank', () => { @@ -1326,11 +1329,14 @@ describe('TeamTaskActivityIntervalService', () => { }); const service = new TeamTaskActivityIntervalService(); - const mutateSpy = vi.spyOn( + const lockSpy = vi.spyOn( service as unknown as { - mutateTeamTasks: TeamTaskActivityIntervalService['resumeActiveIntervalsForMember']; + mutateTeamTasksWithLock: ( + teamName: string, + run: () => { changedTasks: number; failed?: boolean } + ) => { changedTasks: number; failed?: boolean }; }, - 'mutateTeamTasks' + 'mutateTeamTasksWithLock' ); const result = service.resumeActiveIntervalsForMembers( @@ -1339,7 +1345,7 @@ describe('TeamTaskActivityIntervalService', () => { '2026-05-08T10:20:00.000Z' ); - expect(mutateSpy).toHaveBeenCalledTimes(1); + expect(lockSpy).toHaveBeenCalledTimes(1); expect(result.changedTasks).toBe(3); }); @@ -1477,7 +1483,7 @@ describe('TeamTaskActivityIntervalService', () => { ]); }); - it('routes single-member resumeActiveIntervalsForMember through the batch implementation', async () => { + it('resumes single-member intervals through the member noop-cache path', async () => { await writeTask('alpha', { id: 'work-task', subject: 'Build', @@ -1490,17 +1496,19 @@ describe('TeamTaskActivityIntervalService', () => { }); const service = new TeamTaskActivityIntervalService(); - const batchSpy = vi.spyOn(service, 'resumeActiveIntervalsForMembers'); const result = service.resumeActiveIntervalsForMember( 'alpha', 'bob', '2026-05-08T10:20:00.000Z' ); + const task = await readTask('alpha', 'work-task'); - expect(batchSpy).toHaveBeenCalledTimes(1); - expect(batchSpy).toHaveBeenCalledWith('alpha', ['bob'], '2026-05-08T10:20:00.000Z'); expect(result.changedTasks).toBe(1); + expect(task.workIntervals).toEqual([ + { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' }, + { startedAt: '2026-05-08T10:20:00.000Z' }, + ]); }); }); }); diff --git a/test/renderer/components/cli/CliStatusVisibility.test.ts b/test/renderer/components/cli/CliStatusVisibility.test.ts index faa60e6e..27439ca2 100644 --- a/test/renderer/components/cli/CliStatusVisibility.test.ts +++ b/test/renderer/components/cli/CliStatusVisibility.test.ts @@ -129,6 +129,7 @@ vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', async () typeof import('@renderer/components/runtime/ProviderRuntimeBackendSelector') >('@renderer/components/runtime/ProviderRuntimeBackendSelector'); return { + buildProviderRuntimeBackendSummaryText: actual.buildProviderRuntimeBackendSummaryText, getProviderRuntimeBackendSummary: actual.getProviderRuntimeBackendSummary, }; }); diff --git a/test/renderer/components/runtime/ProviderModelBadges.test.tsx b/test/renderer/components/runtime/ProviderModelBadges.test.tsx index 3366ffa7..c7fdbf0b 100644 --- a/test/renderer/components/runtime/ProviderModelBadges.test.tsx +++ b/test/renderer/components/runtime/ProviderModelBadges.test.tsx @@ -341,6 +341,16 @@ describe('ProviderModelBadges', () => { expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1); }); + it('does not render duplicate Anthropic Opus 4.8 model badges when the runtime reports the opus alias', () => { + const host = render(); + const renderedModelLabels = Array.from(host.firstElementChild?.children ?? []) + .map((badge) => badge.firstElementChild?.textContent ?? '') + .filter(Boolean); + + expect(renderedModelLabels.filter((label) => label === 'Opus 4.8')).toHaveLength(1); + expect(renderedModelLabels).toContain('Opus 4.8 (1M)'); + }); + it('collapses long model lists and expands them inline without an internal scroll area', () => { const models = Array.from( { length: 18 }, diff --git a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts index 12e2d37a..9ff78742 100644 --- a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts +++ b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts @@ -201,6 +201,19 @@ vi.mock('@renderer/components/ui/tabs', () => ({ })); vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({ + buildProviderRuntimeBackendSummaryText: () => ({ + auto: 'Auto', + autoCurrently: (backend: string) => `Auto (currently: ${backend})`, + audienceInternal: 'Internal', + states: { + locked: 'Locked', + disabled: 'Disabled', + authRequired: 'Auth required', + runtimeMissing: 'Runtime missing', + degraded: 'Degraded', + unavailable: 'Unavailable', + }, + }), ProviderRuntimeBackendSelector: ({ provider, onSelect, diff --git a/test/renderer/utils/teamModelCatalog.test.ts b/test/renderer/utils/teamModelCatalog.test.ts index e5350516..512df3f0 100644 --- a/test/renderer/utils/teamModelCatalog.test.ts +++ b/test/renderer/utils/teamModelCatalog.test.ts @@ -1,4 +1,5 @@ import { + getTeamModelBadgeLabel, getVisibleTeamProviderModels, isAnthropicOneMillionContextTeamModel, isAnthropicSonnetOneMillionContextTeamModel, @@ -44,6 +45,22 @@ describe('teamModelCatalog', () => { ]); }); + it('does not add duplicate Anthropic Opus 4.8 fallback badges when the runtime reports the opus alias', () => { + const models = getVisibleTeamProviderModels('anthropic', [ + 'opus', + 'claude-opus-4-6', + 'sonnet', + 'haiku', + ]); + + expect(models).toContain('opus'); + expect(models).not.toContain('claude-opus-4-8'); + expect(models).toContain('claude-opus-4-8[1m]'); + + const labels = models.map((model) => getTeamModelBadgeLabel('anthropic', model)); + expect(labels.filter((label) => label === 'Opus 4.8')).toHaveLength(1); + }); + it('orders OpenCode free models before paid models', () => { expect( getVisibleTeamProviderModels(