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';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
const TEAM_AGENT_RUNTIME_REFRESH_MS = 10_000;
|
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: {
|
export function shouldWatchTeamAgentRuntime(input: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -19,6 +28,13 @@ export function shouldWatchTeamAgentRuntime(input: {
|
||||||
return input.leadActivity === 'active' || input.leadActivity === 'idle';
|
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 {
|
interface TeamAgentRuntimeWatcherOptions {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -54,12 +70,38 @@ export function useTeamAgentRuntimeWatcher({
|
||||||
});
|
});
|
||||||
if (!shouldWatch) return;
|
if (!shouldWatch) return;
|
||||||
|
|
||||||
void fetchTeamAgentRuntime(teamName);
|
const existingEntry = teamAgentRuntimeWatchEntries.get(teamName);
|
||||||
const timer = window.setInterval(() => {
|
if (existingEntry) {
|
||||||
void fetchTeamAgentRuntime(teamName);
|
existingEntry.refCount += 1;
|
||||||
}, TEAM_AGENT_RUNTIME_REFRESH_MS);
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timer);
|
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 () => {
|
||||||
|
entry.refCount -= 1;
|
||||||
|
if (entry.refCount <= 0) {
|
||||||
|
window.clearInterval(entry.timer);
|
||||||
|
teamAgentRuntimeWatchEntries.delete(teamName);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
effectiveIsTeamAlive,
|
effectiveIsTeamAlive,
|
||||||
|
|
@ -70,3 +112,21 @@ export function useTeamAgentRuntimeWatcher({
|
||||||
teamName,
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
export function getVisibleTeamProviderModels(
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,21 @@ function createSession(overrides?: {
|
||||||
});
|
});
|
||||||
const close = overrides?.close ?? vi.fn().mockResolvedValue(undefined);
|
const close = overrides?.close ?? vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
const session = {
|
const session: CodexAppServerSession = {
|
||||||
initializeResponse: {
|
initializeResponse: {
|
||||||
userAgent: 'codex-test',
|
userAgent: 'codex-test',
|
||||||
codexHome: '/Users/tester/.codex',
|
codexHome: '/Users/tester/.codex',
|
||||||
platformFamily: 'darwin',
|
platformFamily: 'darwin',
|
||||||
platformOs: 'macos',
|
platformOs: 'macos',
|
||||||
},
|
},
|
||||||
request,
|
request: request as CodexAppServerSession['request'],
|
||||||
notify: vi.fn().mockResolvedValue(undefined),
|
notify: vi.fn().mockResolvedValue(undefined) as CodexAppServerSession['notify'],
|
||||||
onNotification: vi.fn((listener: (method: string, params: unknown) => void) => {
|
onNotification: vi.fn((listener: (method: string, params: unknown) => void) => {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
return () => listeners.delete(listener);
|
return () => listeners.delete(listener);
|
||||||
}),
|
}) as CodexAppServerSession['onNotification'],
|
||||||
close,
|
close: close as CodexAppServerSession['close'],
|
||||||
} satisfies CodexAppServerSession;
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { Mock } from 'vitest';
|
||||||
|
|
||||||
import { ListDashboardRecentProjectsUseCase } from '@features/recent-projects/core/application/use-cases/ListDashboardRecentProjectsUseCase';
|
import { ListDashboardRecentProjectsUseCase } from '@features/recent-projects/core/application/use-cases/ListDashboardRecentProjectsUseCase';
|
||||||
|
|
||||||
|
|
@ -33,15 +34,17 @@ function makeCandidate(overrides: Partial<RecentProjectCandidate> = {}): RecentP
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLogger(): LoggerPort & {
|
type LoggerMock = LoggerPort & {
|
||||||
info: ReturnType<typeof vi.fn>;
|
info: Mock<LoggerPort['info']>;
|
||||||
warn: ReturnType<typeof vi.fn>;
|
warn: Mock<LoggerPort['warn']>;
|
||||||
error: ReturnType<typeof vi.fn>;
|
error: Mock<LoggerPort['error']>;
|
||||||
} {
|
};
|
||||||
|
|
||||||
|
function createLogger(): LoggerMock {
|
||||||
return {
|
return {
|
||||||
info: vi.fn(),
|
info: vi.fn<LoggerPort['info']>(),
|
||||||
warn: vi.fn(),
|
warn: vi.fn<LoggerPort['warn']>(),
|
||||||
error: vi.fn(),
|
error: vi.fn<LoggerPort['error']>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { Mock } from 'vitest';
|
||||||
|
|
||||||
import { CodexRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter';
|
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 { CodexAppServerClient } from '@features/recent-projects/main/infrastructure/codex/CodexAppServerClient';
|
||||||
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
|
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
|
||||||
|
|
||||||
function createLogger(): LoggerPort & {
|
type LoggerMock = LoggerPort & {
|
||||||
info: ReturnType<typeof vi.fn>;
|
info: Mock<LoggerPort['info']>;
|
||||||
warn: ReturnType<typeof vi.fn>;
|
warn: Mock<LoggerPort['warn']>;
|
||||||
error: ReturnType<typeof vi.fn>;
|
error: Mock<LoggerPort['error']>;
|
||||||
} {
|
};
|
||||||
|
|
||||||
|
function createLogger(): LoggerMock {
|
||||||
return {
|
return {
|
||||||
info: vi.fn(),
|
info: vi.fn<LoggerPort['info']>(),
|
||||||
warn: vi.fn(),
|
warn: vi.fn<LoggerPort['warn']>(),
|
||||||
error: vi.fn(),
|
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 { CodexSessionFileRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
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 { LoggerPort } from '@features/recent-projects/core/application/ports/LoggerPort';
|
||||||
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
|
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
|
||||||
|
|
||||||
function createLogger(): LoggerPort & {
|
type LoggerMock = LoggerPort & {
|
||||||
info: ReturnType<typeof vi.fn>;
|
info: Mock<LoggerPort['info']>;
|
||||||
warn: ReturnType<typeof vi.fn>;
|
warn: Mock<LoggerPort['warn']>;
|
||||||
error: ReturnType<typeof vi.fn>;
|
error: Mock<LoggerPort['error']>;
|
||||||
} {
|
};
|
||||||
|
|
||||||
|
function createLogger(): LoggerMock {
|
||||||
return {
|
return {
|
||||||
info: vi.fn(),
|
info: vi.fn<LoggerPort['info']>(),
|
||||||
warn: vi.fn(),
|
warn: vi.fn<LoggerPort['warn']>(),
|
||||||
error: vi.fn(),
|
error: vi.fn<LoggerPort['error']>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { Mock } from 'vitest';
|
||||||
|
|
||||||
import { ProjectScanner } from '../../../src/main/services/discovery/ProjectScanner';
|
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', () => {
|
describe('Global Search - ProjectScanner.searchAllProjects', () => {
|
||||||
let projectScanner: ProjectScanner;
|
let projectScanner: ProjectScanner;
|
||||||
let mockScan: ReturnType<typeof vi.fn>;
|
let mockScan: Mock<() => Promise<Project[]>>;
|
||||||
let mockSearchSessions: ReturnType<typeof vi.fn>;
|
let mockSearchSessions: Mock<
|
||||||
|
(projectId: string, query: string, maxResults?: number) => Promise<SearchSessionsResult>
|
||||||
|
>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Create a real ProjectScanner instance
|
// Create a real ProjectScanner instance
|
||||||
projectScanner = new ProjectScanner();
|
projectScanner = new ProjectScanner();
|
||||||
|
|
||||||
// Mock the scan() method
|
// Mock the scan() method
|
||||||
mockScan = vi.fn();
|
mockScan = vi.fn<() => Promise<Project[]>>();
|
||||||
projectScanner.scan = mockScan;
|
projectScanner.scan = mockScan;
|
||||||
|
|
||||||
// Mock the sessionSearcher.searchSessions() method
|
// 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
|
// @ts-expect-error - Accessing private property for testing
|
||||||
projectScanner.sessionSearcher = {
|
projectScanner.sessionSearcher = {
|
||||||
searchSessions: mockSearchSessions,
|
searchSessions: mockSearchSessions,
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({
|
||||||
env: { HOME: '/Users/tester' },
|
env: { HOME: '/Users/tester' },
|
||||||
connectionIssues: {},
|
connectionIssues: {},
|
||||||
})),
|
})),
|
||||||
|
getProviderStatusStoredCredentialAllowlist: vi.fn(() => undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@main/utils/cliAuthDiagLog', () => ({
|
vi.mock('@main/utils/cliAuthDiagLog', () => ({
|
||||||
|
|
@ -542,7 +543,12 @@ describe('CliInstallerService', () => {
|
||||||
it('falls back to the installed launcher path when --version reports unknown', async () => {
|
it('falls back to the installed launcher path when --version reports unknown', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/Users/tester/.local/bin/claude');
|
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)
|
vi.mocked(execCli)
|
||||||
.mockResolvedValueOnce({ stdout: 'unknown', stderr: '' })
|
.mockResolvedValueOnce({ stdout: 'unknown', stderr: '' })
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,27 @@ import type {
|
||||||
const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING';
|
const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING';
|
||||||
const tempPaths: string[] = [];
|
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(
|
function createLeadAssistantEntry(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
timestamp: 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 = (
|
const extract = (
|
||||||
service as unknown as {
|
service as unknown as {
|
||||||
extractLeadSessionTextsFromJsonl: (
|
extractLeadSessionTextsFromJsonl: (
|
||||||
|
|
@ -4698,7 +4719,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||||
|
|
||||||
|
|
@ -4730,11 +4751,11 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
||||||
const assistantSpy = vi
|
const assistantSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
|
||||||
.mockImplementation(async (...args: unknown[]) => {
|
.mockImplementation(async (...args: unknown[]) => {
|
||||||
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
||||||
readonly string[],
|
readonly string[],
|
||||||
|
|
@ -4752,7 +4773,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||||
|
|
||||||
|
|
@ -4782,12 +4803,12 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
||||||
let appended = false;
|
let appended = false;
|
||||||
const assistantSpy = vi
|
const assistantSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
|
||||||
.mockImplementation(async (...args: unknown[]) => {
|
.mockImplementation(async (...args: unknown[]) => {
|
||||||
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
||||||
readonly string[],
|
readonly string[],
|
||||||
|
|
@ -4818,7 +4839,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||||
|
|
||||||
|
|
@ -4847,7 +4868,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
||||||
let releaseFirstInvocation = () => {};
|
let releaseFirstInvocation = () => {};
|
||||||
|
|
@ -4856,7 +4877,7 @@ describe('TeamDataService', () => {
|
||||||
firstInvocationStartedResolve = resolve;
|
firstInvocationStartedResolve = resolve;
|
||||||
});
|
});
|
||||||
const assistantSpy = vi
|
const assistantSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
|
||||||
.mockImplementation(async (...args: unknown[]) => {
|
.mockImplementation(async (...args: unknown[]) => {
|
||||||
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
||||||
readonly string[],
|
readonly string[],
|
||||||
|
|
@ -4879,7 +4900,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).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 = (
|
const extract = (
|
||||||
service as unknown as {
|
service as unknown as {
|
||||||
extractLeadSessionTextsFromJsonl: (
|
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 = (
|
const extract = (
|
||||||
service as unknown as {
|
service as unknown as {
|
||||||
extractLeadSessionTextsFromJsonl: (
|
extractLeadSessionTextsFromJsonl: (
|
||||||
|
|
@ -4964,7 +4985,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||||
|
|
||||||
|
|
@ -4988,7 +5009,7 @@ describe('TeamDataService', () => {
|
||||||
]);
|
]);
|
||||||
await fs.appendFile(jsonlPath, '{"type":"assistant"', 'utf8');
|
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 = (
|
const extract = (
|
||||||
service as unknown as {
|
service as unknown as {
|
||||||
extractLeadSessionTextsFromJsonl: (
|
extractLeadSessionTextsFromJsonl: (
|
||||||
|
|
@ -4996,7 +5017,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).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 = (
|
const extract = (
|
||||||
service as unknown as {
|
service as unknown as {
|
||||||
extractLeadSessionTextsFromJsonl: (
|
extractLeadSessionTextsFromJsonl: (
|
||||||
|
|
@ -5044,7 +5065,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).extractLeadSessionTextsFromJsonl.bind(service);
|
||||||
|
|
||||||
|
|
@ -5073,12 +5094,12 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
).extractLeadAssistantTextsFromJsonlLines.bind(service);
|
||||||
let shouldFail = true;
|
let shouldFail = true;
|
||||||
const assistantSpy = vi
|
const assistantSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
|
||||||
.mockImplementation(async (...args: unknown[]) => {
|
.mockImplementation(async (...args: unknown[]) => {
|
||||||
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
|
||||||
readonly string[],
|
readonly string[],
|
||||||
|
|
@ -5098,7 +5119,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(service);
|
).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(
|
const secondSpy = vi.spyOn(
|
||||||
secondService as never,
|
teamDataServicePrivate(secondService),
|
||||||
'extractLeadAssistantTextsFromJsonlLines' as never
|
'extractLeadAssistantTextsFromJsonlLines'
|
||||||
);
|
);
|
||||||
const firstExtract = (
|
const firstExtract = (
|
||||||
firstService as unknown as {
|
firstService as unknown as {
|
||||||
|
|
@ -5136,7 +5157,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(firstService);
|
).extractLeadSessionTextsFromJsonl.bind(firstService);
|
||||||
const secondExtract = (
|
const secondExtract = (
|
||||||
|
|
@ -5146,7 +5167,7 @@ describe('TeamDataService', () => {
|
||||||
leadName: string,
|
leadName: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
maxTexts: number
|
maxTexts: number
|
||||||
) => Promise<Array<{ text: string }>>;
|
) => Promise<InboxMessage[]>;
|
||||||
}
|
}
|
||||||
).extractLeadSessionTextsFromJsonl.bind(secondService);
|
).extractLeadSessionTextsFromJsonl.bind(secondService);
|
||||||
|
|
||||||
|
|
@ -5177,10 +5198,10 @@ describe('TeamDataService', () => {
|
||||||
};
|
};
|
||||||
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
(service as unknown as { projectResolver: typeof projectResolver }).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']])
|
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',
|
from: 'fast-lead',
|
||||||
text: 'Fast path recovered lead thought from the known lead session.',
|
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 =
|
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
||||||
projectResolver;
|
projectResolver;
|
||||||
vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation(
|
vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation(
|
||||||
(...args: unknown[]) => {
|
(...args: unknown[]) => {
|
||||||
const [projectDir] = args as [string];
|
const [projectDir] = args as [string];
|
||||||
if (projectDir === '/actual-project') {
|
if (projectDir === '/actual-project') {
|
||||||
|
|
@ -5237,7 +5258,7 @@ describe('TeamDataService', () => {
|
||||||
return Promise.resolve(new Map());
|
return Promise.resolve(new Map());
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
vi.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never).mockResolvedValue([
|
vi.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl').mockResolvedValue([
|
||||||
{
|
{
|
||||||
from: 'actual-lead',
|
from: 'actual-lead',
|
||||||
text: 'Fallback path recovered lead thought from the repaired context.',
|
text: 'Fallback path recovered lead thought from the repaired context.',
|
||||||
|
|
@ -5292,7 +5313,7 @@ describe('TeamDataService', () => {
|
||||||
};
|
};
|
||||||
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
||||||
projectResolver;
|
projectResolver;
|
||||||
vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation(
|
vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation(
|
||||||
(...args: unknown[]) => {
|
(...args: unknown[]) => {
|
||||||
const [projectDir] = args as [string];
|
const [projectDir] = args as [string];
|
||||||
if (projectDir === '/current-project') {
|
if (projectDir === '/current-project') {
|
||||||
|
|
@ -5304,7 +5325,7 @@ describe('TeamDataService', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const extractSpy = vi
|
const extractSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl')
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
{
|
{
|
||||||
from: 'current-lead',
|
from: 'current-lead',
|
||||||
|
|
@ -5366,11 +5387,11 @@ describe('TeamDataService', () => {
|
||||||
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
|
||||||
projectResolver;
|
projectResolver;
|
||||||
const getPathsSpy = vi
|
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-history', '/same-project/lead-history.jsonl']]))
|
||||||
.mockResolvedValueOnce(new Map([['lead-current', '/same-project/lead-current.jsonl']]));
|
.mockResolvedValueOnce(new Map([['lead-current', '/same-project/lead-current.jsonl']]));
|
||||||
const extractSpy = vi
|
const extractSpy = vi
|
||||||
.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never)
|
.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl')
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
{
|
{
|
||||||
from: 'current-lead',
|
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',
|
from: 'team-lead',
|
||||||
text: 'Lead summary',
|
text: 'Lead summary',
|
||||||
|
|
@ -6028,7 +6049,7 @@ describe('TeamDataService', () => {
|
||||||
resolveMembers: resolveMembersSpy,
|
resolveMembers: resolveMembersSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockResolvedValue([
|
vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockResolvedValue([
|
||||||
{
|
{
|
||||||
from: 'team-lead',
|
from: 'team-lead',
|
||||||
text: 'Lead summary',
|
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');
|
order.push('leadTexts:start');
|
||||||
throw new Error('lead sync fail');
|
throw new Error('lead sync fail');
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,12 @@ function createStubbedServiceHarness() {
|
||||||
const service = new TeamMemberRuntimeAdvisoryService(logsFinder as never);
|
const service = new TeamMemberRuntimeAdvisoryService(logsFinder as never);
|
||||||
const advisoryByFilePath = new Map<string, MemberRuntimeAdvisory | null>();
|
const advisoryByFilePath = new Map<string, MemberRuntimeAdvisory | null>();
|
||||||
const readRecentApiRetryAdvisory = vi
|
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[]) => {
|
.mockImplementation(async (...args: unknown[]) => {
|
||||||
const filePath = String(args[0] ?? '');
|
const filePath = String(args[0] ?? '');
|
||||||
if (advisoryByFilePath.has(filePath)) {
|
if (advisoryByFilePath.has(filePath)) {
|
||||||
|
|
|
||||||
|
|
@ -1169,11 +1169,14 @@ describe('TeamTaskActivityIntervalService', () => {
|
||||||
describe('resumeActiveIntervalsForMembers (batch)', () => {
|
describe('resumeActiveIntervalsForMembers (batch)', () => {
|
||||||
it('returns zero changes for an empty members list without scanning the tasks directory', () => {
|
it('returns zero changes for an empty members list without scanning the tasks directory', () => {
|
||||||
const service = new TeamTaskActivityIntervalService();
|
const service = new TeamTaskActivityIntervalService();
|
||||||
const mutateSpy = vi.spyOn(
|
const lockSpy = vi.spyOn(
|
||||||
service as unknown as {
|
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(
|
const result = service.resumeActiveIntervalsForMembers(
|
||||||
|
|
@ -1183,7 +1186,7 @@ describe('TeamTaskActivityIntervalService', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({ changedTasks: 0 });
|
expect(result).toEqual({ changedTasks: 0 });
|
||||||
expect(mutateSpy).not.toHaveBeenCalled();
|
expect(lockSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns zero changes when all member names are blank', () => {
|
it('returns zero changes when all member names are blank', () => {
|
||||||
|
|
@ -1326,11 +1329,14 @@ describe('TeamTaskActivityIntervalService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const service = new TeamTaskActivityIntervalService();
|
const service = new TeamTaskActivityIntervalService();
|
||||||
const mutateSpy = vi.spyOn(
|
const lockSpy = vi.spyOn(
|
||||||
service as unknown as {
|
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(
|
const result = service.resumeActiveIntervalsForMembers(
|
||||||
|
|
@ -1339,7 +1345,7 @@ describe('TeamTaskActivityIntervalService', () => {
|
||||||
'2026-05-08T10:20:00.000Z'
|
'2026-05-08T10:20:00.000Z'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mutateSpy).toHaveBeenCalledTimes(1);
|
expect(lockSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(result.changedTasks).toBe(3);
|
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', {
|
await writeTask('alpha', {
|
||||||
id: 'work-task',
|
id: 'work-task',
|
||||||
subject: 'Build',
|
subject: 'Build',
|
||||||
|
|
@ -1490,17 +1496,19 @@ describe('TeamTaskActivityIntervalService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const service = new TeamTaskActivityIntervalService();
|
const service = new TeamTaskActivityIntervalService();
|
||||||
const batchSpy = vi.spyOn(service, 'resumeActiveIntervalsForMembers');
|
|
||||||
|
|
||||||
const result = service.resumeActiveIntervalsForMember(
|
const result = service.resumeActiveIntervalsForMember(
|
||||||
'alpha',
|
'alpha',
|
||||||
'bob',
|
'bob',
|
||||||
'2026-05-08T10:20:00.000Z'
|
'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(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')
|
typeof import('@renderer/components/runtime/ProviderRuntimeBackendSelector')
|
||||||
>('@renderer/components/runtime/ProviderRuntimeBackendSelector');
|
>('@renderer/components/runtime/ProviderRuntimeBackendSelector');
|
||||||
return {
|
return {
|
||||||
|
buildProviderRuntimeBackendSummaryText: actual.buildProviderRuntimeBackendSummaryText,
|
||||||
getProviderRuntimeBackendSummary: actual.getProviderRuntimeBackendSummary,
|
getProviderRuntimeBackendSummary: actual.getProviderRuntimeBackendSummary,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,16 @@ describe('ProviderModelBadges', () => {
|
||||||
expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1);
|
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', () => {
|
it('collapses long model lists and expands them inline without an internal scroll area', () => {
|
||||||
const models = Array.from(
|
const models = Array.from(
|
||||||
{ length: 18 },
|
{ length: 18 },
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,19 @@ vi.mock('@renderer/components/ui/tabs', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({
|
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: ({
|
ProviderRuntimeBackendSelector: ({
|
||||||
provider,
|
provider,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
getTeamModelBadgeLabel,
|
||||||
getVisibleTeamProviderModels,
|
getVisibleTeamProviderModels,
|
||||||
isAnthropicOneMillionContextTeamModel,
|
isAnthropicOneMillionContextTeamModel,
|
||||||
isAnthropicSonnetOneMillionContextTeamModel,
|
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', () => {
|
it('orders OpenCode free models before paid models', () => {
|
||||||
expect(
|
expect(
|
||||||
getVisibleTeamProviderModels(
|
getVisibleTeamProviderModels(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue