import { describe, expect, it } from 'vitest'; import { MemberWorkSyncToolActivityBusySignal } from '@features/member-work-sync/main/infrastructure/MemberWorkSyncToolActivityBusySignal'; import type { TeamChangeEvent, ToolActivityEventPayload } from '@shared/types'; function toolEvent(teamName: string, payload: ToolActivityEventPayload): TeamChangeEvent { return { type: 'tool-activity', teamName, detail: JSON.stringify(payload), }; } describe('MemberWorkSyncToolActivityBusySignal', () => { it('treats active tools as busy and recent finishes as a bounded quiet window', async () => { const signal = new MemberWorkSyncToolActivityBusySignal({ busyGraceMs: 90_000 }); signal.noteTeamChange( toolEvent('team-a', { action: 'start', activity: { memberName: 'bob', toolUseId: 'tool-1', toolName: 'bash', startedAt: '2026-04-29T00:00:00.000Z', source: 'runtime', }, }) ); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'bob', nowIso: '2026-04-29T00:00:15.000Z', }) ).resolves.toMatchObject({ busy: true, reason: 'active_tool_activity', retryAfterIso: '2026-04-29T00:01:45.000Z', }); signal.noteTeamChange( toolEvent('team-a', { action: 'finish', memberName: 'bob', toolUseId: 'tool-1', finishedAt: '2026-04-29T00:01:00.000Z', }) ); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'bob', nowIso: '2026-04-29T00:01:30.000Z', }) ).resolves.toMatchObject({ busy: true, reason: 'recent_tool_activity', retryAfterIso: '2026-04-29T00:02:30.000Z', }); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'bob', nowIso: '2026-04-29T00:02:31.000Z', }) ).resolves.toEqual({ busy: false }); }); it('does not leak activity across members and clears targeted reset events', async () => { const signal = new MemberWorkSyncToolActivityBusySignal({ busyGraceMs: 90_000 }); signal.noteTeamChange( toolEvent('team-a', { action: 'start', activity: { memberName: 'bob', toolUseId: 'tool-1', toolName: 'read', startedAt: '2026-04-29T00:00:00.000Z', source: 'member_log', }, }) ); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'alice', nowIso: '2026-04-29T00:00:15.000Z', }) ).resolves.toEqual({ busy: false }); signal.noteTeamChange( toolEvent('team-a', { action: 'reset', memberName: 'bob', toolUseIds: ['tool-1'], }) ); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'bob', nowIso: '2026-04-29T00:00:15.000Z', }) ).resolves.toEqual({ busy: false }); }); it('drops all tracked activity for a team when it goes offline', async () => { const signal = new MemberWorkSyncToolActivityBusySignal({ busyGraceMs: 90_000 }); signal.noteTeamChange( toolEvent('team-a', { action: 'start', activity: { memberName: 'bob', toolUseId: 'tool-1', toolName: 'write', startedAt: '2026-04-29T00:00:00.000Z', source: 'runtime', }, }) ); signal.noteTeamChange({ type: 'lead-activity', teamName: 'team-a', detail: 'offline', }); await expect( signal.isBusy({ teamName: 'team-a', memberName: 'bob', nowIso: '2026-04-29T00:00:15.000Z', }) ).resolves.toEqual({ busy: false }); }); });