diff --git a/src/main/services/infrastructure/teamWatchScope.ts b/src/main/services/infrastructure/teamWatchScope.ts index 0137882b..9e471781 100644 --- a/src/main/services/infrastructure/teamWatchScope.ts +++ b/src/main/services/infrastructure/teamWatchScope.ts @@ -29,9 +29,9 @@ export function notifyTeamWatchScopeChanged(): void { scopeChangeListener?.(); } -function collectAliveTeams(scope: Set): void { +function collectAliveTeams(scope: Set): boolean { if (!aliveTeamsProvider) { - return; + return true; } try { for (const teamName of aliveTeamsProvider()) { @@ -39,9 +39,11 @@ function collectAliveTeams(scope: Set): void { scope.add(teamName); } } + return true; } catch { - // A provider failure must never break watching. The watcher treats a thrown - // or empty scope conservatively (inboxes + root stay watched either way). + // A provider failure must never narrow watching. Returning null below is the + // safe fallback: watch every team, matching the original behavior. + return false; } } @@ -49,9 +51,11 @@ function collectAliveTeams(scope: Set): void { * Current set of teams whose team-root/task artifacts should be watched. Prunes * engaged entries past their TTL as a side effect of being called. */ -export function computeTeamWatchScope(nowMs: number = Date.now()): ReadonlySet { +export function computeTeamWatchScope(nowMs: number = Date.now()): ReadonlySet | null { const scope = new Set(); - collectAliveTeams(scope); + if (!collectAliveTeams(scope)) { + return null; + } for (const [teamName, engagedAt] of engagedAtByTeam) { if (nowMs - engagedAt <= ENGAGED_TTL_MS) { scope.add(teamName); @@ -71,7 +75,8 @@ export function markTeamEngaged(teamName: string, nowMs: number = Date.now()): v if (!teamName) { return; } - const wasInScope = computeTeamWatchScope(nowMs).has(teamName); + const currentScope = computeTeamWatchScope(nowMs); + const wasInScope = currentScope === null || currentScope.has(teamName); engagedAtByTeam.set(teamName, nowMs); if (!wasInScope) { scopeChangeListener?.(); diff --git a/test/main/ipc/teams.test.ts b/test/main/ipc/teams.test.ts index ca43152d..a43f261a 100644 --- a/test/main/ipc/teams.test.ts +++ b/test/main/ipc/teams.test.ts @@ -1323,7 +1323,7 @@ describe('ipc teams handlers', () => { it('marks created teams engaged before provisioning writes startup artifacts', async () => { const createTeam = 'created-watch-scope'; provisioningService.createTeam.mockImplementationOnce(async () => { - expect(computeTeamWatchScope().has(createTeam)).toBe(true); + expect(computeTeamWatchScope()?.has(createTeam)).toBe(true); return { runId: 'run-created-watch-scope' }; }); @@ -1334,7 +1334,7 @@ describe('ipc teams handlers', () => { })) as { success: boolean }; expect(result.success).toBe(true); - expect(computeTeamWatchScope().has(createTeam)).toBe(true); + expect(computeTeamWatchScope()?.has(createTeam)).toBe(true); }); it('returns cached TEAM_LIST data under active launch pressure without starting another scan', async () => { @@ -4401,7 +4401,7 @@ describe('ipc teams handlers', () => { })) as { success: boolean }; expect(result.success).toBe(true); - expect(computeTeamWatchScope().has('draft-team')).toBe(true); + expect(computeTeamWatchScope()?.has('draft-team')).toBe(true); expect(provisioningService.launchTeam).not.toHaveBeenCalled(); expect(provisioningService.createTeam).toHaveBeenCalledWith( { diff --git a/test/main/services/infrastructure/teamWatchScope.test.ts b/test/main/services/infrastructure/teamWatchScope.test.ts index 5901c34f..fff0f888 100644 --- a/test/main/services/infrastructure/teamWatchScope.test.ts +++ b/test/main/services/infrastructure/teamWatchScope.test.ts @@ -18,23 +18,23 @@ afterEach(() => { describe('teamWatchScope', () => { it('includes alive teams from the provider', () => { setAliveTeamsProvider(() => ['t-alive']); - expect([...computeTeamWatchScope(1000)]).toContain('t-alive'); + expect([...(computeTeamWatchScope(1000) ?? [])]).toContain('t-alive'); }); it('includes engaged teams within TTL and prunes after expiry', () => { markTeamEngaged('t-eng', 0); - expect(computeTeamWatchScope(FIVE_MIN).has('t-eng')).toBe(true); - expect(computeTeamWatchScope(FIVE_MIN + 1).has('t-eng')).toBe(false); + expect(computeTeamWatchScope(FIVE_MIN)?.has('t-eng')).toBe(true); + expect(computeTeamWatchScope(FIVE_MIN + 1)?.has('t-eng')).toBe(false); // pruning is sticky: it stays out without re-engaging - expect(computeTeamWatchScope(FIVE_MIN + 2).has('t-eng')).toBe(false); + expect(computeTeamWatchScope(FIVE_MIN + 2)?.has('t-eng')).toBe(false); }); it('unions alive and engaged teams', () => { setAliveTeamsProvider(() => ['a']); markTeamEngaged('b', 0); const scope = computeTeamWatchScope(1000); - expect(scope.has('a')).toBe(true); - expect(scope.has('b')).toBe(true); + expect(scope?.has('a')).toBe(true); + expect(scope?.has('b')).toBe(true); }); it('notifies the listener only when engagement newly adds to scope', () => { @@ -66,7 +66,7 @@ describe('teamWatchScope', () => { throw new Error('boom'); }); expect(() => computeTeamWatchScope(0)).not.toThrow(); - expect([...computeTeamWatchScope(0)]).toEqual([]); + expect(computeTeamWatchScope(0)).toBeNull(); }); it('ignores empty team names', () => { @@ -74,6 +74,6 @@ describe('teamWatchScope', () => { setTeamWatchScopeChangeListener(listener); markTeamEngaged('', 0); expect(listener).not.toHaveBeenCalled(); - expect(computeTeamWatchScope(0).size).toBe(0); + expect(computeTeamWatchScope(0)?.size).toBe(0); }); });