agent-ecosystem/test/main/services/team/stallMonitor/TeamTaskStallMonitor.test.ts

87 lines
2.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { TeamTaskStallMonitor } from '../../../../../src/main/services/team/stallMonitor/TeamTaskStallMonitor';
describe('TeamTaskStallMonitor', () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllEnvs();
});
it('runs end-to-end and notifies only after a second confirmed scan', async () => {
vi.useFakeTimers();
vi.stubEnv('CLAUDE_TEAM_TASK_STALL_MONITOR_ENABLED', 'true');
vi.stubEnv('CLAUDE_TEAM_TASK_STALL_ALERTS_ENABLED', 'true');
vi.stubEnv('CLAUDE_TEAM_TASK_STALL_SCAN_INTERVAL_MS', '1000');
vi.stubEnv('CLAUDE_TEAM_TASK_STALL_STARTUP_GRACE_MS', '1');
vi.stubEnv('CLAUDE_TEAM_TASK_STALL_ACTIVATION_GRACE_MS', '1');
const registry = {
start: vi.fn(),
stop: vi.fn(async () => undefined),
noteTeamChange: vi.fn(),
listActiveTeams: vi.fn(async () => ['demo']),
};
const snapshot = {
teamName: 'demo',
inProgressTasks: [{ id: 'task-a', displayId: 'abcd1234', subject: 'Task A' }],
reviewOpenTasks: [],
allTasksById: new Map([
['task-a', { id: 'task-a', displayId: 'abcd1234', subject: 'Task A' }],
]),
};
const snapshotSource = {
getSnapshot: vi.fn(async () => snapshot),
};
const policy = {
evaluateWork: vi.fn(() => ({
status: 'alert',
taskId: 'task-a',
branch: 'work',
signal: 'turn_ended_after_touch',
epochKey: 'task-a:epoch',
reason: 'Potential work stall.',
})),
evaluateReview: vi.fn(),
};
const journal = {
reconcileScan: vi
.fn()
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
status: 'alert',
taskId: 'task-a',
branch: 'work',
signal: 'turn_ended_after_touch',
epochKey: 'task-a:epoch',
reason: 'Potential work stall.',
},
]),
markAlerted: vi.fn(async () => undefined),
};
const notifier = {
notifyLead: vi.fn(async () => undefined),
};
const monitor = new TeamTaskStallMonitor(
registry as never,
snapshotSource as never,
policy as never,
journal as never,
notifier as never
);
monitor.start();
await vi.advanceTimersByTimeAsync(2_100);
await vi.advanceTimersByTimeAsync(2_100);
expect(snapshotSource.getSnapshot).toHaveBeenCalledTimes(2);
expect(notifier.notifyLead).toHaveBeenCalledTimes(1);
expect(journal.markAlerted).toHaveBeenCalledWith(
'demo',
'task-a:epoch',
expect.any(String)
);
});
});