diff --git a/src/store.test.ts b/src/store.test.ts index a3850a3..669911c 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -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', () => { diff --git a/src/tui/utils/stuckDetection.test.ts b/src/tui/utils/stuckDetection.test.ts index 0c54596..8fbe7bf 100644 --- a/src/tui/utils/stuckDetection.test.ts +++ b/src/tui/utils/stuckDetection.test.ts @@ -10,6 +10,7 @@ const makeWorker = (overrides: Partial = {}): 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();