agent-ecosystem/test/main/services/infrastructure/TeamTaskWatchRegistry.test.ts
777genius 5d63ecfe32 perf: scope team file watching to active and engaged teams
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.
2026-05-30 00:25:55 +03:00

159 lines
5.3 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
type MockChokidarWatcher = {
targets: string[];
options: unknown;
on: (event: string, handler: (...args: unknown[]) => void) => MockChokidarWatcher;
close: ReturnType<typeof vi.fn>;
};
const chokidarMock = vi.hoisted(() => {
const instances: MockChokidarWatcher[] = [];
const make = () => (targets: string | string[], options: unknown) => {
const watcher = {
targets: (Array.isArray(targets) ? targets : [targets]).map((t) => String(t)),
options,
close: vi.fn().mockResolvedValue(undefined),
} as MockChokidarWatcher;
watcher.on = vi.fn(() => watcher);
instances.push(watcher);
return watcher;
};
const watch = vi.fn(make());
return {
instances,
watch,
reset() {
instances.length = 0;
watch.mockReset();
watch.mockImplementation(make());
},
};
});
vi.mock('chokidar', () => ({ watch: chokidarMock.watch }));
import { TeamTaskWatchRegistry } from '../../../../src/main/services/infrastructure/TeamTaskWatchRegistry';
function latestTargets(): string[] {
const last = chokidarMock.instances.at(-1);
return (last?.targets ?? []).map((t) => path.normalize(t));
}
describe('TeamTaskWatchRegistry scoping', () => {
let root: string;
beforeEach(() => {
chokidarMock.reset();
root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttwr-scope-'));
for (const team of ['alpha', 'beta', 'gamma']) {
fs.mkdirSync(path.join(root, team, 'inboxes'), { recursive: true });
fs.writeFileSync(path.join(root, team, 'config.json'), '{}');
fs.writeFileSync(path.join(root, team, 'inboxes', 'team-lead.json'), '[]');
}
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
it('watches only scoped team dirs but every team inbox (teams kind)', async () => {
const registry = new TeamTaskWatchRegistry({
kind: 'teams',
rootPath: root,
onChange: () => {},
onError: () => {},
getScopedTeamNames: () => new Set(['alpha']),
});
await registry.start();
const targets = latestTargets();
await registry.close();
expect(targets).toContain(path.normalize(root));
// scoped team root watched, unscoped team roots not watched
expect(targets).toContain(path.normalize(path.join(root, 'alpha')));
expect(targets).not.toContain(path.normalize(path.join(root, 'beta')));
expect(targets).not.toContain(path.normalize(path.join(root, 'gamma')));
// ALL inboxes watched regardless of scope (cross-team delivery)
expect(targets).toContain(path.normalize(path.join(root, 'alpha', 'inboxes')));
expect(targets).toContain(path.normalize(path.join(root, 'beta', 'inboxes')));
expect(targets).toContain(path.normalize(path.join(root, 'gamma', 'inboxes')));
});
it('falls back to watching every team when no scope provider is given', async () => {
const registry = new TeamTaskWatchRegistry({
kind: 'teams',
rootPath: root,
onChange: () => {},
onError: () => {},
});
await registry.start();
const targets = latestTargets();
await registry.close();
for (const team of ['alpha', 'beta', 'gamma']) {
expect(targets).toContain(path.normalize(path.join(root, team)));
expect(targets).toContain(path.normalize(path.join(root, team, 'inboxes')));
}
});
it('falls back to watching every team when the scope provider returns null', async () => {
const registry = new TeamTaskWatchRegistry({
kind: 'teams',
rootPath: root,
onChange: () => {},
onError: () => {},
getScopedTeamNames: () => null,
});
await registry.start();
const targets = latestTargets();
await registry.close();
for (const team of ['alpha', 'beta', 'gamma']) {
expect(targets).toContain(path.normalize(path.join(root, team)));
}
});
it('scopes task dirs and never adds inboxes (tasks kind)', async () => {
const registry = new TeamTaskWatchRegistry({
kind: 'tasks',
rootPath: root,
onChange: () => {},
onError: () => {},
getScopedTeamNames: () => new Set(['beta']),
});
await registry.start();
const targets = latestTargets();
await registry.close();
expect(targets).toContain(path.normalize(root));
expect(targets).toContain(path.normalize(path.join(root, 'beta')));
expect(targets).not.toContain(path.normalize(path.join(root, 'alpha')));
expect(targets).not.toContain(path.normalize(path.join(root, 'gamma')));
// tasks kind never watches inboxes
expect(targets).not.toContain(path.normalize(path.join(root, 'beta', 'inboxes')));
});
it('re-resolves scope on requestReconcile (newly scoped team gets watched)', async () => {
const scoped = new Set<string>(['alpha']);
const registry = new TeamTaskWatchRegistry({
kind: 'teams',
rootPath: root,
onChange: () => {},
onError: () => {},
getScopedTeamNames: () => scoped,
});
await registry.start();
expect(latestTargets()).not.toContain(path.normalize(path.join(root, 'beta')));
scoped.add('beta');
await registry.requestReconcile();
const targets = latestTargets();
await registry.close();
expect(targets).toContain(path.normalize(path.join(root, 'beta')));
});
});