test(bf-27e4): add test coverage for beadsCompleted vs stuck detection metric
Some checks are pending
CI / test (18.x) (push) Waiting to run
CI / test (20.x) (push) Waiting to run
CI / test (22.x) (push) Waiting to run

- Add test case for worker processing 100 beads with 0 successful completions
- Fix incorrect test expecting beadsCompleted to increment on bead.released
- beadsCompleted only increments on bead.completed events
- beadsReleased increments on bead.released with release_success
- Stuck detection now uses unified beadsCompleted metric with clear messaging

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-06-07 10:48:51 -04:00
parent 04904ce032
commit c047131e09
2 changed files with 53 additions and 2 deletions

View file

@ -227,12 +227,13 @@ describe('InMemoryEventStore', () => {
expect(worker?.status).toBe('idle');
});
it('should increment beadsCompleted on bead.released with release_success', () => {
it('should increment beadsReleased on bead.released with release_success', () => {
store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-1' }));
store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-2' }));
const worker = store.getWorker('w-test');
expect(worker?.beadsCompleted).toBe(2);
expect(worker?.beadsReleased).toBe(2);
expect(worker?.beadsCompleted).toBe(0); // beadsCompleted only increments on bead.completed
});
it('should clear activeBead and activeFiles on bead.released with release_success', () => {

View file

@ -10,6 +10,7 @@ const makeWorker = (overrides: Partial<WorkerInfo> = {}): WorkerInfo => ({
id: 'w-test',
status: 'active',
beadsCompleted: 3,
beadsReleased: 0,
firstSeen: Date.now() - 5 * 60 * 1000,
lastActivity: Date.now(),
activeFiles: [],
@ -203,6 +204,55 @@ describe('Stuck Detection', () => {
});
});
describe('long-running detection', () => {
it('detects worker with many beads released but zero successful completions', () => {
// Worker that has processed 100 beads (all timed out/deferred)
const worker = makeWorker({
firstSeen: Date.now() - 40 * 60 * 1000, // 40 minutes ago
lastActivity: Date.now(), // Recent activity to avoid no_progress detection
beadsCompleted: 0, // No successful completions
beadsReleased: 100, // All beads timed out/deferred
});
const events: LogEvent[] = [];
// Create only 5 events to avoid triggering "events but no completions" in no_progress
for (let i = 0; i < 5; i++) {
events.push(makeEvent({ ts: Date.now() - i * 30000, msg: 'working on task' }));
}
const pattern = isWorkerStuck(worker, events);
expect(pattern).not.toBeNull();
expect(pattern!.type).toBe('long_running');
expect(pattern!.reason).toContain('40m'); // Running for 40 minutes
expect(pattern!.reason).toContain('100 processed'); // 100 beads released
expect(pattern!.reason).toContain('0 successful completions'); // No successful completions
expect(pattern!.reason).toContain('timed out/deferred'); // Clarifies why
expect(pattern!.evidence).toContain('Beads successfully completed: 0');
expect(pattern!.evidence).toContain('Beads released (including timed-out): 100');
});
it('detects worker with only 1 successful completion after long runtime', () => {
const worker = makeWorker({
firstSeen: Date.now() - 30 * 60 * 1000, // 30 minutes ago
beadsCompleted: 1, // Only 1 successful completion
beadsReleased: 50, // 50 beads released (49 timed out)
});
const events: LogEvent[] = [];
for (let i = 0; i < 30; i++) {
events.push(makeEvent({ ts: Date.now() - i * 40000 }));
}
const pattern = isWorkerStuck(worker, events);
expect(pattern).not.toBeNull();
expect(pattern!.type).toBe('long_running');
expect(pattern!.reason).toContain('30m');
expect(pattern!.reason).toContain('only 1 successful completion');
expect(pattern!.evidence).toContain('Beads successfully completed: 1');
expect(pattern!.evidence).toContain('Beads released (including timed-out): 50');
});
});
describe('legacy detection (non-state-transition)', () => {
it('still detects repeated tool calls', () => {
const worker = makeWorker();