feat(bd-20w): Add unit tests for CollisionAlert component

Comprehensive test suite covering:
- Component initialization and blessed box/list creation
- Alert rendering with different severities (critical, error, warning, info)
- Navigation (selectNext, selectPrevious with wrapping)
- Acknowledgement behavior (single and all alerts)
- Severity level display with icons and colors
- Collision type display (file, bead, task)
- Visibility controls (show/hide)
- Unacknowledged alert counting
- Worker display (name list vs count)
- Selected alert details with suggestions
- Key bindings (up/down, enter, a, escape)
- Edge cases (long titles, empty lists, rapid updates)

All 57 tests passing.

Co-Authored-By: Claude Worker <noreply@anthropic.com>
This commit is contained in:
jeda 2026-03-04 03:37:09 +00:00
parent f9fc00b526
commit 0f075d1029

View file

@ -0,0 +1,857 @@
/**
* Tests for CollisionAlert Component
*
* Tests collision alert rendering, acknowledgement, navigation, and severity display.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as blessed from 'blessed';
// Mock the blessed module before importing CollisionAlert
vi.mock('blessed', () => {
// Create mock instances
const mockBoxInstance = {
setContent: vi.fn(),
focus: vi.fn(),
key: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
visible: true,
screen: {
render: vi.fn(),
},
};
const mockListInstance = {
key: vi.fn(),
focus: vi.fn(),
};
const mockBox = vi.fn(() => mockBoxInstance);
const mockList = vi.fn(() => mockListInstance);
return {
default: {
box: mockBox,
list: mockList,
},
box: mockBox,
list: mockList,
};
});
// Import after mocking
import { CollisionAlert } from './CollisionAlert.js';
import { CollisionAlert as CollisionAlertData, FileCollision, BeadCollision } from '../../types.js';
// Helper to create mock CollisionAlert data
function createMockAlert(overrides: Partial<CollisionAlertData> = {}): CollisionAlertData {
return {
id: 'alert-123',
type: 'file',
severity: 'warning',
title: 'File collision detected',
description: 'Multiple workers editing src/example.ts',
workers: ['w-alice', 'w-bob'],
timestamp: Date.now(),
acknowledged: false,
collision: {
path: 'src/example.ts',
workers: ['w-alice', 'w-bob'],
detectedAt: Date.now(),
events: [],
isActive: true,
} as FileCollision,
...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('CollisionAlert', () => {
let collisionAlert: CollisionAlert;
let mockScreen: blessed.Widgets.Screen;
let mockBoxInstance: any;
let mockListInstance: any;
beforeEach(() => {
vi.clearAllMocks();
mockScreen = createMockScreen();
// Get the mock instances from the mocks
const blessedMock = blessed as unknown as { box: vi.Mock; list: vi.Mock };
mockBoxInstance = blessedMock.box();
mockListInstance = blessedMock.list();
collisionAlert = new CollisionAlert({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create a blessed box with correct options', () => {
const blessedMock = blessed as unknown as { box: vi.Mock };
expect(blessedMock.box).toHaveBeenCalledWith(
expect.objectContaining({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
label: ' Collision Alerts ',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true,
mouse: true,
})
);
});
it('should create a list inside the box', () => {
const blessedMock = blessed as unknown as { list: vi.Mock };
expect(blessedMock.list).toHaveBeenCalledWith(
expect.objectContaining({
parent: mockBoxInstance,
top: 0,
left: 0,
right: 0,
bottom: 0,
keys: true,
vi: true,
mouse: true,
})
);
});
it('should bind key handlers on construction', () => {
// List key bindings should be registered
expect(mockListInstance.key).toHaveBeenCalled();
});
it('should register onAcknowledge callback', () => {
const onAcknowledge = vi.fn();
const alert = new CollisionAlert({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
onAcknowledge,
});
const alerts = [createMockAlert()];
alert.updateAlerts(alerts);
alert.acknowledgeSelected();
expect(onAcknowledge).toHaveBeenCalledWith('alert-123');
});
});
describe('updateAlerts', () => {
it('should update alerts list and render', () => {
const alerts = [
createMockAlert({ id: 'alert-1', severity: 'warning' }),
createMockAlert({ id: 'alert-2', severity: 'critical' }),
];
collisionAlert.updateAlerts(alerts);
expect(mockBoxInstance.setContent).toHaveBeenCalled();
expect(mockBoxInstance.screen.render).toHaveBeenCalled();
});
it('should show "No active collisions" when empty', () => {
collisionAlert.updateAlerts([]);
expect(mockBoxInstance.setContent).toHaveBeenCalledWith(
expect.stringContaining('No active collisions detected')
);
});
it('should show alert count in header', () => {
const alerts = [
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
createMockAlert({ id: 'alert-3' }),
];
collisionAlert.updateAlerts(alerts);
expect(mockBoxInstance.setContent).toHaveBeenCalledWith(
expect.stringContaining('Alerts: 3')
);
});
it('should reset selected index if out of bounds', () => {
// First set some alerts
collisionAlert.updateAlerts([
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
]);
// Update to fewer alerts
collisionAlert.updateAlerts([createMockAlert({ id: 'alert-1' })]);
// Should not throw and selection should be valid
const selected = collisionAlert.getSelected();
expect(selected).toBeDefined();
expect(selected?.id).toBe('alert-1');
});
});
describe('selectNext', () => {
it('should move to next alert', () => {
const alerts = [
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
createMockAlert({ id: 'alert-3' }),
];
collisionAlert.updateAlerts(alerts);
// Initially selected is first alert
expect(collisionAlert.getSelected()?.id).toBe('alert-1');
collisionAlert.selectNext();
expect(collisionAlert.getSelected()?.id).toBe('alert-2');
});
it('should wrap to first alert when at end', () => {
const alerts = [
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
];
collisionAlert.updateAlerts(alerts);
// Move to last
collisionAlert.selectNext();
expect(collisionAlert.getSelected()?.id).toBe('alert-2');
// Wrap to first
collisionAlert.selectNext();
expect(collisionAlert.getSelected()?.id).toBe('alert-1');
});
it('should do nothing when no alerts', () => {
collisionAlert.updateAlerts([]);
// Should not throw
expect(() => collisionAlert.selectNext()).not.toThrow();
});
});
describe('selectPrevious', () => {
it('should move to previous alert', () => {
const alerts = [
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
createMockAlert({ id: 'alert-3' }),
];
collisionAlert.updateAlerts(alerts);
// Move to second
collisionAlert.selectNext();
expect(collisionAlert.getSelected()?.id).toBe('alert-2');
// Move back to first
collisionAlert.selectPrevious();
expect(collisionAlert.getSelected()?.id).toBe('alert-1');
});
it('should wrap to last alert when at beginning', () => {
const alerts = [
createMockAlert({ id: 'alert-1' }),
createMockAlert({ id: 'alert-2' }),
];
collisionAlert.updateAlerts(alerts);
// At first, wrap to last
collisionAlert.selectPrevious();
expect(collisionAlert.getSelected()?.id).toBe('alert-2');
});
it('should do nothing when no alerts', () => {
collisionAlert.updateAlerts([]);
// Should not throw
expect(() => collisionAlert.selectPrevious()).not.toThrow();
});
});
describe('acknowledgeSelected', () => {
it('should acknowledge the selected alert', () => {
const alerts = [
createMockAlert({ id: 'alert-1', acknowledged: false }),
];
collisionAlert.updateAlerts(alerts);
expect(collisionAlert.getSelected()?.acknowledged).toBe(false);
collisionAlert.acknowledgeSelected();
expect(collisionAlert.getSelected()?.acknowledged).toBe(true);
});
it('should call onAcknowledge callback', () => {
const onAcknowledge = vi.fn();
const alert = new CollisionAlert({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
onAcknowledge,
});
const alerts = [createMockAlert({ id: 'alert-xyz' })];
alert.updateAlerts(alerts);
alert.acknowledgeSelected();
expect(onAcknowledge).toHaveBeenCalledWith('alert-xyz');
});
it('should not call callback if already acknowledged', () => {
const onAcknowledge = vi.fn();
const alert = new CollisionAlert({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
onAcknowledge,
});
const alerts = [createMockAlert({ acknowledged: true })];
alert.updateAlerts(alerts);
alert.acknowledgeSelected();
expect(onAcknowledge).not.toHaveBeenCalled();
});
it('should do nothing when no alerts', () => {
collisionAlert.updateAlerts([]);
// Should not throw
expect(() => collisionAlert.acknowledgeSelected()).not.toThrow();
});
});
describe('acknowledgeAll', () => {
it('should acknowledge all unacknowledged alerts', () => {
const alerts = [
createMockAlert({ id: 'alert-1', acknowledged: false }),
createMockAlert({ id: 'alert-2', acknowledged: false }),
createMockAlert({ id: 'alert-3', acknowledged: true }),
];
collisionAlert.updateAlerts(alerts);
collisionAlert.acknowledgeAll();
const updatedAlerts = [
collisionAlert.getSelected(),
...alerts.slice(1),
];
expect(alerts[0].acknowledged).toBe(true);
expect(alerts[1].acknowledged).toBe(true);
expect(alerts[2].acknowledged).toBe(true);
});
it('should call onAcknowledge for each unacknowledged alert', () => {
const onAcknowledge = vi.fn();
const alert = new CollisionAlert({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
height: 10,
onAcknowledge,
});
const alerts = [
createMockAlert({ id: 'alert-1', acknowledged: false }),
createMockAlert({ id: 'alert-2', acknowledged: false }),
createMockAlert({ id: 'alert-3', acknowledged: true }),
];
alert.updateAlerts(alerts);
alert.acknowledgeAll();
expect(onAcknowledge).toHaveBeenCalledTimes(2);
expect(onAcknowledge).toHaveBeenCalledWith('alert-1');
expect(onAcknowledge).toHaveBeenCalledWith('alert-2');
});
});
describe('getUnacknowledgedCount', () => {
it('should return count of unacknowledged alerts', () => {
const alerts = [
createMockAlert({ acknowledged: false }),
createMockAlert({ acknowledged: false }),
createMockAlert({ acknowledged: true }),
createMockAlert({ acknowledged: false }),
];
collisionAlert.updateAlerts(alerts);
expect(collisionAlert.getUnacknowledgedCount()).toBe(3);
});
it('should return 0 when all acknowledged', () => {
const alerts = [
createMockAlert({ acknowledged: true }),
createMockAlert({ acknowledged: true }),
];
collisionAlert.updateAlerts(alerts);
expect(collisionAlert.getUnacknowledgedCount()).toBe(0);
});
it('should return 0 when no alerts', () => {
collisionAlert.updateAlerts([]);
expect(collisionAlert.getUnacknowledgedCount()).toBe(0);
});
});
describe('show and hide', () => {
it('should show the panel', () => {
collisionAlert.show();
expect(mockBoxInstance.show).toHaveBeenCalled();
expect(mockListInstance.focus).toHaveBeenCalled();
});
it('should hide the panel', () => {
collisionAlert.hide();
expect(mockBoxInstance.hide).toHaveBeenCalled();
expect(mockBoxInstance.screen.render).toHaveBeenCalled();
});
it('should report visibility correctly', () => {
mockBoxInstance.visible = true;
expect(collisionAlert.isVisible()).toBe(true);
mockBoxInstance.visible = false;
expect(collisionAlert.isVisible()).toBe(false);
});
});
describe('focus', () => {
it('should focus the list element', () => {
collisionAlert.focus();
expect(mockListInstance.focus).toHaveBeenCalled();
});
});
describe('getElement', () => {
it('should return the box element', () => {
const element = collisionAlert.getElement();
expect(element).toBe(mockBoxInstance);
});
});
describe('severity display', () => {
it('should display critical severity with correct icon and color', () => {
const alerts = [
createMockAlert({ severity: 'critical', title: 'Critical alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('!!!'); // Critical icon
expect(content).toContain('red'); // Critical color
expect(content).toContain('Critical alert');
});
it('should display error severity with correct icon and color', () => {
const alerts = [
createMockAlert({ severity: 'error', title: 'Error alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('!!'); // Error icon
expect(content).toContain('red'); // Error color
});
it('should display warning severity with correct icon and color', () => {
const alerts = [
createMockAlert({ severity: 'warning', title: 'Warning alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('!'); // Warning icon
expect(content).toContain('yellow'); // Warning color
});
it('should display info severity with correct icon and color', () => {
const alerts = [
createMockAlert({ severity: 'info', title: 'Info alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('i'); // Info icon
expect(content).toContain('blue'); // Info color
});
});
describe('collision type display', () => {
it('should display file collision type icon', () => {
const alerts = [
createMockAlert({ type: 'file', title: 'File collision' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('[F]'); // File type icon
});
it('should display bead collision type icon', () => {
const alerts = [
createMockAlert({
type: 'bead',
title: 'Bead collision',
collision: {
beadId: 'bd-123',
workers: ['w-alice', 'w-bob'],
detectedAt: Date.now(),
events: [],
isActive: true,
severity: 'warning',
} as BeadCollision,
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('[B]'); // Bead type icon
});
it('should display task collision type icon', () => {
const alerts = [
createMockAlert({
type: 'task',
title: 'Task collision',
collision: {
type: 'directory',
description: 'Directory conflict',
workers: ['w-alice', 'w-bob'],
affectedResources: ['src/'],
detectedAt: Date.now(),
isActive: true,
riskLevel: 'medium',
},
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('[T]'); // Task type icon
});
});
describe('alert grouping by severity', () => {
it('should group critical and error alerts together', () => {
const alerts = [
createMockAlert({ id: 'critical-1', severity: 'critical', title: 'Critical 1' }),
createMockAlert({ id: 'error-1', severity: 'error', title: 'Error 1' }),
createMockAlert({ id: 'warning-1', severity: 'warning', title: 'Warning 1' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('CRITICAL/ERROR (2)');
expect(content).toContain('WARNINGS (1)');
});
it('should show warnings in separate section', () => {
const alerts = [
createMockAlert({ severity: 'warning', title: 'Warning 1' }),
createMockAlert({ severity: 'warning', title: 'Warning 2' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('WARNINGS (2)');
});
it('should show info alerts in separate section', () => {
const alerts = [
createMockAlert({ severity: 'info', title: 'Info 1' }),
createMockAlert({ severity: 'info', title: 'Info 2' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('INFO (2)');
});
});
describe('acknowledged alerts display', () => {
it('should show [ACK] marker for acknowledged alerts', () => {
const alerts = [
createMockAlert({ acknowledged: true, title: 'Acked alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('[ACK]');
});
it('should not show [ACK] marker for unacknowledged alerts', () => {
const alerts = [
createMockAlert({ acknowledged: false, title: 'Unacked alert' }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Content should not have ACK marker before the alert
const lines = content.split('\n');
const alertLine = lines.find((line: string) => line.includes('Unacked alert'));
expect(alertLine).not.toContain('[ACK]');
});
it('should show unacknowledged count in header', () => {
const alerts = [
createMockAlert({ acknowledged: false }),
createMockAlert({ acknowledged: true }),
createMockAlert({ acknowledged: false }),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('(2 unacknowledged)');
});
});
describe('worker display', () => {
it('should show worker names when 2 or fewer workers', () => {
const alerts = [
createMockAlert({
workers: ['w-alice', 'w-bob'],
title: 'Two workers',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('w-alice');
expect(content).toContain('w-bob');
});
it('should show worker count when more than 2 workers', () => {
const alerts = [
createMockAlert({
workers: ['w-alice', 'w-bob', 'w-charlie'],
title: 'Three workers',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('3 workers');
});
it('should truncate worker list to 15 characters', () => {
const alerts = [
createMockAlert({
workers: ['very-long-worker-name', 'another-long-name'],
title: 'Long names',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should be truncated to 15 chars
const lines = content.split('\n');
const alertLine = lines.find((line: string) => line.includes('Long names'));
// Check that there's a 15-char limit somewhere in the worker display
expect(alertLine).toBeDefined();
});
});
describe('selected alert details', () => {
it('should show details of selected alert', () => {
const alerts = [
createMockAlert({
title: 'Test Alert',
description: 'This is a test description',
workers: ['w-alice', 'w-bob'],
suggestion: 'Coordinate with other workers',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('Selected Alert Details');
expect(content).toContain('Test Alert');
expect(content).toContain('This is a test description');
expect(content).toContain('w-alice, w-bob');
expect(content).toContain('Coordinate with other workers');
});
it('should show suggestion when provided', () => {
const alerts = [
createMockAlert({
suggestion: 'Please review and coordinate',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('Suggestion');
expect(content).toContain('Please review and coordinate');
});
it('should not show suggestion section when not provided', () => {
const alerts = [
createMockAlert({
suggestion: undefined,
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Check that "Suggestion:" label doesn't appear
const lines = content.split('\n');
const suggestionLine = lines.find((line: string) => line.includes('Suggestion:'));
expect(suggestionLine).toBeUndefined();
});
it('should show keyboard shortcuts in details', () => {
const alerts = [createMockAlert()];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('[Enter] Acknowledge');
expect(content).toContain('[a] Acknowledge All');
expect(content).toContain('[Esc] Close');
});
});
describe('key bindings', () => {
it('should bind up and k keys to selectPrevious', () => {
expect(mockListInstance.key).toHaveBeenCalledWith(['up', 'k'], expect.any(Function));
});
it('should bind down and j keys to selectNext', () => {
expect(mockListInstance.key).toHaveBeenCalledWith(['down', 'j'], expect.any(Function));
});
it('should bind enter and space to acknowledgeSelected', () => {
expect(mockListInstance.key).toHaveBeenCalledWith(['enter', 'space'], expect.any(Function));
});
it('should bind a key to acknowledgeAll', () => {
expect(mockListInstance.key).toHaveBeenCalledWith(['a'], expect.any(Function));
});
it('should bind escape key to hide', () => {
expect(mockListInstance.key).toHaveBeenCalledWith(['escape'], expect.any(Function));
});
});
describe('edge cases', () => {
it('should handle alerts with very long titles', () => {
const alerts = [
createMockAlert({
title: 'This is a very long alert title that should be truncated to 40 characters maximum',
}),
];
collisionAlert.updateAlerts(alerts);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Title should be truncated (40 chars)
expect(content).toContain('This is a very long alert title that sh');
});
it('should handle empty worker list', () => {
const alerts = [
createMockAlert({
workers: [],
}),
];
// Should not throw
expect(() => collisionAlert.updateAlerts(alerts)).not.toThrow();
});
it('should handle mixed severity levels in one update', () => {
const alerts = [
createMockAlert({ severity: 'critical' }),
createMockAlert({ severity: 'error' }),
createMockAlert({ severity: 'warning' }),
createMockAlert({ severity: 'info' }),
];
// Should not throw
expect(() => collisionAlert.updateAlerts(alerts)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('CRITICAL/ERROR (2)');
expect(content).toContain('WARNINGS (1)');
expect(content).toContain('INFO (1)');
});
it('should handle rapid alert updates', () => {
const alerts1 = [createMockAlert({ id: 'alert-1' })];
const alerts2 = [createMockAlert({ id: 'alert-2' })];
const alerts3 = [createMockAlert({ id: 'alert-3' })];
// Should not throw
expect(() => {
collisionAlert.updateAlerts(alerts1);
collisionAlert.updateAlerts(alerts2);
collisionAlert.updateAlerts(alerts3);
}).not.toThrow();
expect(collisionAlert.getSelected()?.id).toBe('alert-3');
});
});
});