The main process watched every team directory under ~/.claude/teams (one shallow chokidar target per team root, per team inboxes, and per task dir). On macOS this falls back to kqueue, which needs one fd per watched file, so a workspace with many teams kept ~1600 descriptors open and made startup and reconcile work scale with the number of teams on disk. Scope the team-root and task watching to teams that are running or currently engaged in the UI. The teams root and every team's inboxes are still watched for all teams, so cross-team message delivery, the lead inbox->stdin relay, and notifications are unchanged. Idle teams are static, so dropping their team-root/ task watches is safe; opening a team (getData) or launching it re-adds it via an immediate watch-scope refresh. The provider falls back to watching every team when unset, and the EMFILE polling fallback is intentionally left unscoped so a scope change can never look like a deletion. Measured on a 162-team workspace: open team fds 1600 -> 730, with team-root watching restored the moment a team is opened or goes live.
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
computeTeamWatchScope,
|
|
markTeamEngaged,
|
|
resetTeamWatchScopeForTests,
|
|
setAliveTeamsProvider,
|
|
setTeamWatchScopeChangeListener,
|
|
} from '../../../../src/main/services/infrastructure/teamWatchScope';
|
|
|
|
const FIVE_MIN = 5 * 60_000;
|
|
|
|
afterEach(() => {
|
|
resetTeamWatchScopeForTests();
|
|
});
|
|
|
|
describe('teamWatchScope', () => {
|
|
it('includes alive teams from the provider', () => {
|
|
setAliveTeamsProvider(() => ['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);
|
|
// pruning is sticky: it stays out without re-engaging
|
|
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);
|
|
});
|
|
|
|
it('notifies the listener only when engagement newly adds to scope', () => {
|
|
const listener = vi.fn();
|
|
setTeamWatchScopeChangeListener(listener);
|
|
markTeamEngaged('x', 0);
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
markTeamEngaged('x', 1000); // already in scope -> no extra churn
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not notify when engaging an already-alive (in-scope) team', () => {
|
|
setAliveTeamsProvider(() => ['y']);
|
|
const listener = vi.fn();
|
|
setTeamWatchScopeChangeListener(listener);
|
|
markTeamEngaged('y', 0);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('survives a throwing alive provider (watcher falls back safely)', () => {
|
|
setAliveTeamsProvider(() => {
|
|
throw new Error('boom');
|
|
});
|
|
expect(() => computeTeamWatchScope(0)).not.toThrow();
|
|
expect([...computeTeamWatchScope(0)]).toEqual([]);
|
|
});
|
|
|
|
it('ignores empty team names', () => {
|
|
const listener = vi.fn();
|
|
setTeamWatchScopeChangeListener(listener);
|
|
markTeamEngaged('', 0);
|
|
expect(listener).not.toHaveBeenCalled();
|
|
expect(computeTeamWatchScope(0).size).toBe(0);
|
|
});
|
|
});
|