FABRIC/src/tui/components/WorkerGrid.test.ts
jedarden 47c3396e0c
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
fix(bf-27e4): unify stuck detection metric with beadsCompleted
Fix discrepancy where /api/workers returned contradictory data:
- beadsCompleted: 285 (counts bead.released events including timed-out)
- stuck: true, stuckReason: 'Running for 2311m with only 1 completion(s)'

The stuck detection now correctly uses:
- beadsCompleted: all beads processed (including timed-out/deferred)
- beadsSucceeded: only successful completions (bead.completed events)
- beadsTimedOut: new counter for timed-out/deferred beads

Changes:
- Add beadsTimedOut counter to WorkerInfo type
- Increment beadsTimedOut on bead.released with TimedOut/Deferred outcome
- Update stuck detection to show clear reason text:
  - 'X processed but 0 successful completions (all timed out/deferred)'
  - 'X processed but only Y successful completion(s) (Z timed out/deferred)'
- Add beadsTimedOut to evidence array

Fix acceptance criteria:
- Worker processing 100 timed-out beads shows clearly in UI:
  - 100 beads completed
  - 0 beads succeeded
  - Stuck reason: '100 processed but 0 successful completions'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:11:50 -04:00

507 lines
14 KiB
TypeScript

/**
* Tests for WorkerGrid Component
*
* Tests the worker grid display with mocked blessed elements.
*/
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import blessed from 'blessed';
// Mock the blessed module before importing WorkerGrid
vi.mock('blessed', () => {
// Create the mock box inside the factory
const mockBoxInstance = {
setContent: vi.fn(),
focus: vi.fn(),
key: vi.fn(),
screen: {
render: vi.fn(),
},
};
const mockBox = vi.fn(() => mockBoxInstance);
return {
default: {
box: mockBox,
},
box: mockBox,
};
});
// Import after mocking
import { WorkerGrid } from './WorkerGrid.js';
import { WorkerInfo } from '../../types.js';
// Helper to create mock WorkerInfo
function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
return {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],
hasCollision: false,
activeDirectories: [],
collisionTypes: [],
eventCount: 10,
currentBead: null,
...overrides,
};
}
// Helper to create mock screen
function createMockScreen() {
return {
render: vi.fn(),
append: vi.fn(),
key: vi.fn(),
destroy: vi.fn(),
} as unknown as blessed.Widgets.Screen;
}
describe('WorkerGrid', () => {
let workerGrid: WorkerGrid;
let mockScreen: blessed.Widgets.Screen;
let mockBoxInstance: any;
beforeEach(() => {
vi.clearAllMocks();
mockScreen = createMockScreen();
// Get the mock box instance from the mock
const blessedMock = blessed as unknown as { box: Mock };
mockBoxInstance = blessedMock.box();
workerGrid = new WorkerGrid({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
bottom: 0,
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create a blessed box with correct options', () => {
const blessedMock = blessed as unknown as { box: Mock };
expect(blessedMock.box).toHaveBeenCalledWith(
expect.objectContaining({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
bottom: 0,
label: ' Workers ',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true,
mouse: true,
})
);
});
it('should bind key handlers on construction', () => {
// Key bindings should be registered
expect(mockBoxInstance.key).toHaveBeenCalled();
});
});
describe('updateWorkers', () => {
it('should update workers list and render', () => {
const workers = [
createMockWorker({ id: 'w-abc123', status: 'active' }),
createMockWorker({ id: 'w-def456', status: 'idle' }),
];
workerGrid.updateWorkers(workers);
expect(mockBoxInstance.setContent).toHaveBeenCalled();
// Component calls this.box.screen.render() which is the box's internal screen reference
expect(mockBoxInstance.screen.render).toHaveBeenCalled();
});
it('should show "No workers detected" when empty', () => {
workerGrid.updateWorkers([]);
expect(mockBoxInstance.setContent).toHaveBeenCalledWith(
expect.stringContaining('No workers detected')
);
});
it('should show worker count in header', () => {
const workers = [
createMockWorker({ id: 'w-abc123' }),
createMockWorker({ id: 'w-def456' }),
createMockWorker({ id: 'w-ghi789' }),
];
workerGrid.updateWorkers(workers);
expect(mockBoxInstance.setContent).toHaveBeenCalledWith(
expect.stringContaining('Total: 3 workers')
);
});
it('should reset selected index if out of bounds', () => {
// First set some workers
workerGrid.updateWorkers([
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
]);
// Update to fewer workers
workerGrid.updateWorkers([createMockWorker({ id: 'w-1' })]);
// Should not throw and selection should be valid
const selected = workerGrid.getSelected();
expect(selected).toBeDefined();
expect(selected?.id).toBe('w-1');
});
});
describe('selectNext', () => {
it('should move to next worker', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
createMockWorker({ id: 'w-3' }),
];
workerGrid.updateWorkers(workers);
// Initially selected is first worker
expect(workerGrid.getSelected()?.id).toBe('w-1');
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-2');
});
it('should wrap to first worker when at end', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
];
workerGrid.updateWorkers(workers);
// Move to last
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-2');
// Wrap to first
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-1');
});
it('should do nothing when no workers', () => {
workerGrid.updateWorkers([]);
// Should not throw
expect(() => workerGrid.selectNext()).not.toThrow();
});
});
describe('selectPrevious', () => {
it('should move to previous worker', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
createMockWorker({ id: 'w-3' }),
];
workerGrid.updateWorkers(workers);
// Move to second
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-2');
// Move back to first
workerGrid.selectPrevious();
expect(workerGrid.getSelected()?.id).toBe('w-1');
});
it('should wrap to last worker when at beginning', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
];
workerGrid.updateWorkers(workers);
// At first, wrap to last
workerGrid.selectPrevious();
expect(workerGrid.getSelected()?.id).toBe('w-2');
});
it('should do nothing when no workers', () => {
workerGrid.updateWorkers([]);
// Should not throw
expect(() => workerGrid.selectPrevious()).not.toThrow();
});
});
describe('getSelected', () => {
it('should return currently selected worker', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
];
workerGrid.updateWorkers(workers);
expect(workerGrid.getSelected()?.id).toBe('w-1');
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-2');
});
it('should return undefined when no workers', () => {
workerGrid.updateWorkers([]);
expect(workerGrid.getSelected()).toBeUndefined();
});
});
describe('focus', () => {
it('should focus the box element', () => {
workerGrid.focus();
expect(mockBoxInstance.focus).toHaveBeenCalled();
});
});
describe('getElement', () => {
it('should return the box element', () => {
const element = workerGrid.getElement();
expect(element).toBe(mockBoxInstance);
});
});
describe('render output', () => {
it('should include worker status icons', () => {
const workers = [
createMockWorker({ id: 'w-1', status: 'active' }),
createMockWorker({ id: 'w-2', status: 'idle' }),
createMockWorker({ id: 'w-3', status: 'error' }),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Check for status icons
expect(content).toContain('●'); // active
expect(content).toContain('○'); // idle
expect(content).toContain('✗'); // error
});
it('should render workers with correct status colors', () => {
const workers = [
createMockWorker({ id: 'w-1', status: 'active' }),
createMockWorker({ id: 'w-2', status: 'idle' }),
createMockWorker({ id: 'w-3', status: 'error' }),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Check for status color tags
expect(content).toContain('{light-green-fg}'); // active
expect(content).toContain('{light-yellow-fg}'); // idle
expect(content).toContain('{light-red-fg}'); // error
});
it('should show collision indicator when worker has collision', () => {
const workers = [
createMockWorker({ id: 'w-1', hasCollision: true }),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('⚠');
});
it('should not show collision indicator when no collision', () => {
const workers = [
createMockWorker({ id: 'w-1', hasCollision: false }),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).not.toContain('⚠');
});
it('should truncate worker ID to 12 characters', () => {
const workers = [
createMockWorker({ id: 'w-verylongworkerid123456' }),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('w-verylongwo');
});
it('should show selection marker on selected worker', () => {
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
];
workerGrid.updateWorkers(workers);
const firstContent = mockBoxInstance.setContent.mock.calls[0][0];
expect(firstContent).toContain('>'); // Selection marker
// Select second worker
workerGrid.selectNext();
const secondContent = mockBoxInstance.setContent.mock.calls[1][0];
expect(secondContent).toContain('>');
});
it('should include current task from lastEvent', () => {
const workers = [
createMockWorker({
id: 'w-1',
lastEvent: {
ts: Date.now(),
worker: 'w-1',
level: 'info',
msg: 'Processing bead',
bead: 'bd-abc123',
},
}),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('bd-abc123');
expect(content).toContain('Processing bead');
});
});
describe('key bindings', () => {
it('should bind up and k keys to selectPrevious', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['up', 'k'], expect.any(Function));
});
it('should bind down and j keys to selectNext', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['down', 'j'], expect.any(Function));
});
it('should bind g key to select first', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['g'], expect.any(Function));
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
createMockWorker({ id: 'w-3' }),
];
workerGrid.updateWorkers(workers);
// Move to last worker
workerGrid.selectNext();
workerGrid.selectNext();
expect(workerGrid.getSelected()?.id).toBe('w-3');
// Find the 'g' handler and call it
const gCall = mockBoxInstance.key.mock.calls.find((call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('g'));
const gHandler = gCall?.[1];
if (gHandler) {
gHandler();
}
expect(workerGrid.getSelected()?.id).toBe('w-1');
});
it('should bind G (shift+g) key to select last', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['G'], expect.any(Function));
const workers = [
createMockWorker({ id: 'w-1' }),
createMockWorker({ id: 'w-2' }),
createMockWorker({ id: 'w-3' }),
];
workerGrid.updateWorkers(workers);
// Initially at first
expect(workerGrid.getSelected()?.id).toBe('w-1');
// Find the 'G' handler and call it
const GCall = mockBoxInstance.key.mock.calls.find((call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('G'));
const GHandler = GCall?.[1];
if (GHandler) {
GHandler();
}
expect(workerGrid.getSelected()?.id).toBe('w-3');
});
});
describe('edge cases', () => {
it('should handle workers with no lastEvent', () => {
const workers = [
createMockWorker({
id: 'w-1',
lastEvent: undefined,
}),
];
// Should not throw
expect(() => workerGrid.updateWorkers(workers)).not.toThrow();
});
it('should handle empty message in lastEvent', () => {
const workers = [
createMockWorker({
id: 'w-1',
lastEvent: {
ts: Date.now(),
worker: 'w-1',
level: 'info',
msg: '',
},
}),
];
// Should not throw
expect(() => workerGrid.updateWorkers(workers)).not.toThrow();
});
it('should handle very long task descriptions', () => {
const workers = [
createMockWorker({
id: 'w-1',
lastEvent: {
ts: Date.now(),
worker: 'w-1',
level: 'info',
msg: 'This is a very long task description that should be truncated to 25 characters',
bead: 'bd-test',
},
}),
];
workerGrid.updateWorkers(workers);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Message should be truncated (25 chars)
expect(content).toContain('This is a very long ta');
});
});
});