fix(renderer): dedupe runtime watcher and model badges
This commit is contained in:
parent
ea56d15712
commit
af8a5fc66e
15 changed files with 257 additions and 91 deletions
|
|
@ -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<string, TeamAgentRuntimeWatchEntry>();
|
||||
|
||||
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>
|
||||
): 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<RecentProjectCandidate> = {}): RecentP
|
|||
};
|
||||
}
|
||||
|
||||
function createLogger(): LoggerPort & {
|
||||
info: ReturnType<typeof vi.fn>;
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
type LoggerMock = LoggerPort & {
|
||||
info: Mock<LoggerPort['info']>;
|
||||
warn: Mock<LoggerPort['warn']>;
|
||||
error: Mock<LoggerPort['error']>;
|
||||
};
|
||||
|
||||
function createLogger(): LoggerMock {
|
||||
return {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn<LoggerPort['info']>(),
|
||||
warn: vi.fn<LoggerPort['warn']>(),
|
||||
error: vi.fn<LoggerPort['error']>(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof vi.fn>;
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
type LoggerMock = LoggerPort & {
|
||||
info: Mock<LoggerPort['info']>;
|
||||
warn: Mock<LoggerPort['warn']>;
|
||||
error: Mock<LoggerPort['error']>;
|
||||
};
|
||||
|
||||
function createLogger(): LoggerMock {
|
||||
return {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn<LoggerPort['info']>(),
|
||||
warn: vi.fn<LoggerPort['warn']>(),
|
||||
error: vi.fn<LoggerPort['error']>(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof vi.fn>;
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
type LoggerMock = LoggerPort & {
|
||||
info: Mock<LoggerPort['info']>;
|
||||
warn: Mock<LoggerPort['warn']>;
|
||||
error: Mock<LoggerPort['error']>;
|
||||
};
|
||||
|
||||
function createLogger(): LoggerMock {
|
||||
return {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn<LoggerPort['info']>(),
|
||||
warn: vi.fn<LoggerPort['warn']>(),
|
||||
error: vi.fn<LoggerPort['error']>(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof vi.fn>;
|
||||
let mockSearchSessions: ReturnType<typeof vi.fn>;
|
||||
let mockScan: Mock<() => Promise<Project[]>>;
|
||||
let mockSearchSessions: Mock<
|
||||
(projectId: string, query: string, maxResults?: number) => Promise<SearchSessionsResult>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a real ProjectScanner instance
|
||||
projectScanner = new ProjectScanner();
|
||||
|
||||
// Mock the scan() method
|
||||
mockScan = vi.fn();
|
||||
mockScan = vi.fn<() => Promise<Project[]>>();
|
||||
projectScanner.scan = mockScan;
|
||||
|
||||
// Mock the sessionSearcher.searchSessions() method
|
||||
mockSearchSessions = vi.fn();
|
||||
mockSearchSessions =
|
||||
vi.fn<
|
||||
(projectId: string, query: string, maxResults?: number) => Promise<SearchSessionsResult>
|
||||
>();
|
||||
// @ts-expect-error - Accessing private property for testing
|
||||
projectScanner.sessionSearcher = {
|
||||
searchSessions: mockSearchSessions,
|
||||
|
|
|
|||
|
|
@ -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<string | null>;
|
||||
},
|
||||
'inferInstalledCliVersionFromPath'
|
||||
).mockResolvedValue('2.1.101');
|
||||
vi.mocked(execCli)
|
||||
.mockResolvedValueOnce({ stdout: 'unknown', stderr: '' })
|
||||
.mockResolvedValueOnce({
|
||||
|
|
|
|||
|
|
@ -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<InboxMessage[]>;
|
||||
getLeadSessionJsonlPaths(projectDir: string): Promise<Map<string, string>>;
|
||||
extractLeadSessionTextsFromJsonl(
|
||||
jsonlPath: string,
|
||||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
): Promise<InboxMessage[]>;
|
||||
extractLeadSessionTexts(teamName: string, config: TeamConfig): Promise<InboxMessage[]>;
|
||||
};
|
||||
|
||||
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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||
|
||||
|
|
@ -4730,11 +4751,11 @@ describe('TeamDataService', () => {
|
|||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
) => Promise<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||
|
||||
|
|
@ -4782,12 +4803,12 @@ describe('TeamDataService', () => {
|
|||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
) => Promise<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||
|
||||
|
|
@ -4847,7 +4868,7 @@ describe('TeamDataService', () => {
|
|||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
) => Promise<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||
|
||||
|
|
@ -5073,12 +5094,12 @@ describe('TeamDataService', () => {
|
|||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
) => Promise<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).extractLeadSessionTextsFromJsonl.bind(firstService);
|
||||
const secondExtract = (
|
||||
|
|
@ -5146,7 +5167,7 @@ describe('TeamDataService', () => {
|
|||
leadName: string,
|
||||
leadSessionId: string,
|
||||
maxTexts: number
|
||||
) => Promise<Array<{ text: string }>>;
|
||||
) => Promise<InboxMessage[]>;
|
||||
}
|
||||
).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');
|
||||
|
|
|
|||
|
|
@ -72,7 +72,12 @@ function createStubbedServiceHarness() {
|
|||
const service = new TeamMemberRuntimeAdvisoryService(logsFinder as never);
|
||||
const advisoryByFilePath = new Map<string, MemberRuntimeAdvisory | null>();
|
||||
const readRecentApiRetryAdvisory = vi
|
||||
.spyOn(service as never, 'readRecentApiRetryAdvisory' as never)
|
||||
.spyOn(
|
||||
service as unknown as {
|
||||
readRecentApiRetryAdvisory: (filePath: string) => Promise<MemberRuntimeAdvisory | null>;
|
||||
},
|
||||
'readRecentApiRetryAdvisory'
|
||||
)
|
||||
.mockImplementation(async (...args: unknown[]) => {
|
||||
const filePath = String(args[0] ?? '');
|
||||
if (advisoryByFilePath.has(filePath)) {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(<ProviderModelBadges providerId="anthropic" models={['opus']} />);
|
||||
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 },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue