chore: add utility scripts and additional test coverage

STORY:
Development requires supporting scripts and comprehensive test coverage.
This commit adds utility scripts and extends test suites.

COLLABORATION:
- scripts/capture-sessions.mjs: Screenshot capture for sessions hub
- scripts/capture-timeline.mjs: Timeline view capture utility
- sessions-header-logic.ts: Session header business logic
- Additional test files for sessions, hooks, and snapshot diffing
This commit is contained in:
zenchantlive 2026-02-15 21:19:31 -08:00
parent 812b6cf8c5
commit 173937c1f3
7 changed files with 729 additions and 0 deletions

View file

@ -0,0 +1,68 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getSwarmHealth } from '../../../src/components/sessions/sessions-header-logic';
import type { AgentRecord, AgentLiveness } from '../../../src/lib/agent-registry';
// Mock AgentRecord helper
function mockAgent(id: string, liveness: AgentLiveness): { agent: AgentRecord, liveness: AgentLiveness } {
return {
agent: {
agent_id: id,
display_name: id,
role: 'agent',
status: 'idle',
created_at: new Date().toISOString(),
last_seen_at: new Date().toISOString(),
version: 1
},
liveness
};
}
test('getSwarmHealth returns green/active when all agents are active', () => {
const members = [
mockAgent('a1', 'active'),
mockAgent('a2', 'active')
];
const livenessMap = { a1: 'active', a2: 'active' };
const health = getSwarmHealth(members.map(m => m.agent), livenessMap);
assert.equal(health.status, 'active');
assert.equal(health.color, 'text-emerald-400');
});
test('getSwarmHealth returns yellow/warning when any agent is stale', () => {
const members = [
mockAgent('a1', 'active'),
mockAgent('a2', 'stale')
];
const livenessMap = { a1: 'active', a2: 'stale' };
const health = getSwarmHealth(members.map(m => m.agent), livenessMap);
assert.equal(health.status, 'warning');
assert.equal(health.color, 'text-amber-400');
});
test('getSwarmHealth returns red/critical when any agent is evicted/dead', () => {
const members = [
mockAgent('a1', 'active'),
mockAgent('a2', 'evicted')
];
const livenessMap = { a1: 'active', a2: 'evicted' };
const health = getSwarmHealth(members.map(m => m.agent), livenessMap);
assert.equal(health.status, 'critical');
assert.equal(health.color, 'text-rose-400');
});
test('getSwarmHealth returns gray/offline when all agents are idle', () => {
const members = [
mockAgent('a1', 'idle'),
mockAgent('a2', 'idle')
];
const livenessMap = { a1: 'idle', a2: 'idle' };
const health = getSwarmHealth(members.map(m => m.agent), livenessMap);
assert.equal(health.status, 'offline');
assert.equal(health.color, 'text-zinc-500');
});

View file

@ -0,0 +1,85 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import type { AgentRecord } from '../../../src/lib/agent-registry';
interface SwarmGroup {
swarmId: string;
swarmLabel: string;
members: AgentRecord[];
}
function groupAgentsBySwarm(
agents: AgentRecord[],
swarmGroups: SwarmGroup[]
): { swarmGroups: SwarmGroup[]; unassigned: AgentRecord[] } {
const nonEmptySwarmGroups = swarmGroups.filter(g => g.members.length > 0);
const assignedIds = new Set(nonEmptySwarmGroups.flatMap(g => g.members.map(m => m.agent_id)));
const unassigned = agents.filter(a => !assignedIds.has(a.agent_id));
return { swarmGroups: nonEmptySwarmGroups, unassigned };
}
describe('SessionsHeader: Agent Grouping', () => {
const mockAgent = (id: string): AgentRecord => ({
agent_id: id,
display_name: id,
role: 'agent',
status: 'idle',
created_at: new Date().toISOString(),
last_seen_at: new Date().toISOString(),
version: 1,
});
it('groups agents by swarm', () => {
const agents = [
mockAgent('agent-1'),
mockAgent('agent-2'),
mockAgent('agent-3'),
];
const swarmGroups: SwarmGroup[] = [
{
swarmId: 'bb-buff',
swarmLabel: 'bb-buff',
members: [agents[0], agents[1]],
},
];
const result = groupAgentsBySwarm(agents, swarmGroups);
assert.equal(result.swarmGroups.length, 1);
assert.equal(result.swarmGroups[0].members.length, 2);
assert.equal(result.unassigned.length, 1);
assert.equal(result.unassigned[0].agent_id, 'agent-3');
});
it('shows fallback bucket for unassigned agents', () => {
const agents = [
mockAgent('agent-1'),
mockAgent('agent-2'),
];
const swarmGroups: SwarmGroup[] = [];
const result = groupAgentsBySwarm(agents, swarmGroups);
assert.equal(result.swarmGroups.length, 0);
assert.equal(result.unassigned.length, 2);
});
it('handles empty swarm groups', () => {
const agents = [mockAgent('agent-1')];
const swarmGroups: SwarmGroup[] = [
{
swarmId: 'bb-empty',
swarmLabel: 'bb-empty',
members: [],
},
];
const result = groupAgentsBySwarm(agents, swarmGroups);
assert.equal(result.swarmGroups.length, 0, 'Empty swarm groups should not render');
assert.equal(result.unassigned.length, 1);
});
});

View file

@ -0,0 +1,31 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
// Test for shallow comparison of initialIssues
// This test verifies that the hook doesn't sync state when the array contents are the same
// but the reference has changed (common in Next.js server component re-renders)
describe('useBeadsSubscription shallow comparison', () => {
it('should NOT sync state when initialIssues reference changes but contents are same', async () => {
// This is a placeholder test - the actual fix requires React Testing Library
// to properly test React hooks in a browser environment.
//
// The bug: When the parent re-renders, initialIssues gets a new array reference.
// The useEffect with [initialIssues] dependency fires, calling setIssues(initialIssues),
// which overwrites local state including form inputs in child components.
//
// The fix: Add shallow comparison before syncing.
// Expected behavior after fix:
// - When initialIssues changes reference but contents are equal → NO sync
// - When initialIssues actually changes → sync
const issues1 = [{ id: '1', title: 'Task 1' }];
const issues2 = [{ id: '1', title: 'Task 1' }]; // Same content, new reference
// These should be considered equal after JSON.stringify comparison
const isEqual = JSON.stringify(issues1) === JSON.stringify(issues2);
assert.ok(isEqual, 'Issues with same content should be equal when stringified');
});
});

View file

@ -0,0 +1,435 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import type { BeadIssueWithProject, BeadDependency } from '../../src/lib/types';
import { diffSnapshots } from '../../src/lib/snapshot-differ';
const MOCK_PROJECT = {
key: 'proj-1',
root: 'C:\\test',
displayPath: 'test',
name: 'Test Project',
source: 'local' as const,
addedAt: null,
};
function createMockIssue(id: string, overrides: Partial<BeadIssueWithProject> = {}): BeadIssueWithProject {
return {
id,
title: `Title ${id}`,
description: null,
status: 'open',
priority: 2,
issue_type: 'task',
assignee: null,
owner: 'owner',
labels: [],
dependencies: [],
created_at: '2026-02-13T00:00:00Z',
updated_at: '2026-02-13T00:00:00Z',
closed_at: null,
close_reason: null,
closed_by_session: null,
created_by: 'creator',
due_at: null,
estimated_minutes: null,
external_ref: null,
metadata: {},
project: MOCK_PROJECT,
...overrides,
};
}
describe('Snapshot Differ Stress Tests', () => {
describe('HIGH-FREQUENCY BURSTS', () => {
it('should capture only final diff when status changes multiple times (simulated burst)', () => {
const prev = [createMockIssue('bb-1', { status: 'open' })];
const curr = [createMockIssue('bb-1', { status: 'closed' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'closed');
assert.strictEqual(events[0].payload.from, 'open');
assert.strictEqual(events[0].payload.to, 'closed');
});
it('should handle rapid status oscillation (open -> in_progress -> blocked -> in_progress)', () => {
const prev = [createMockIssue('bb-1', { status: 'open' })];
const curr = [createMockIssue('bb-1', { status: 'in_progress' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'status_changed');
});
});
describe('BATCH MUTATIONS', () => {
it('should handle 50+ beads all changing status simultaneously', () => {
const prev: BeadIssueWithProject[] = [];
const curr: BeadIssueWithProject[] = [];
for (let i = 1; i <= 50; i++) {
curr.push(createMockIssue(`bb-${i}`, { status: 'open' }));
}
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 50);
events.forEach(e => {
assert.strictEqual(e.kind, 'created');
});
});
it('should handle 100 beads with mixed status transitions', () => {
const prev: BeadIssueWithProject[] = [];
const curr: BeadIssueWithProject[] = [];
for (let i = 1; i <= 100; i++) {
if (i <= 50) {
prev.push(createMockIssue(`bb-${i}`, { status: 'open' }));
curr.push(createMockIssue(`bb-${i}`, { status: 'in_progress' }));
} else {
curr.push(createMockIssue(`bb-${i}`, { status: 'open' }));
}
}
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 100);
const statusChanged = events.filter(e => e.kind === 'status_changed');
const created = events.filter(e => e.kind === 'created');
assert.strictEqual(statusChanged.length, 50);
assert.strictEqual(created.length, 50);
});
});
describe('COMPLEX PERMUTATIONS', () => {
it('should emit events for status + assignee + priority + labels all changing on same bead', () => {
const prev = [createMockIssue('bb-1', {
status: 'open',
assignee: null,
priority: 2,
labels: ['bug'],
})];
const curr = [createMockIssue('bb-1', {
status: 'in_progress',
assignee: 'alice',
priority: 1,
labels: ['bug', 'urgent'],
})];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 4);
const kinds = events.map(e => e.kind);
assert.ok(kinds.includes('status_changed'));
assert.ok(kinds.includes('assignee_changed'));
assert.ok(kinds.includes('priority_changed'));
assert.ok(kinds.includes('labels_changed'));
});
it('should handle all trackable fields changing simultaneously', () => {
const prev = [createMockIssue('bb-1', {
status: 'open',
title: 'Old Title',
description: null,
priority: 3,
issue_type: 'task',
assignee: null,
labels: [],
dependencies: [],
due_at: null,
estimated_minutes: null,
})];
const curr = [createMockIssue('bb-1', {
status: 'in_progress',
title: 'New Title',
description: 'New description',
priority: 1,
issue_type: 'feature',
assignee: 'bob',
labels: ['enhancement'],
dependencies: [{ type: 'blocks', target: 'bb-2' }],
due_at: '2026-02-20T00:00:00Z',
estimated_minutes: 120,
})];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 10);
const kinds = events.map(e => e.kind);
assert.ok(kinds.includes('status_changed'));
assert.ok(kinds.includes('title_changed'));
assert.ok(kinds.includes('description_changed'));
assert.ok(kinds.includes('priority_changed'));
assert.ok(kinds.includes('type_changed'));
assert.ok(kinds.includes('assignee_changed'));
assert.ok(kinds.includes('labels_changed'));
assert.ok(kinds.includes('dependency_added'));
assert.ok(kinds.includes('due_date_changed'));
assert.ok(kinds.includes('estimate_changed'));
});
});
describe('NULL SAFETY', () => {
it('should NOT crash when labels is null in prev (DOCUMENTS BUG - will throw TypeError)', () => {
const prev = [createMockIssue('bb-1', { labels: null as unknown as string[] })];
const curr = [createMockIssue('bb-1', { labels: ['bug'] })];
assert.throws(() => {
diffSnapshots(prev, curr);
}, /TypeError|Cannot read properties of null/);
});
it('should NOT crash when labels is null in curr (DOCUMENTS BUG - will throw TypeError)', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
const curr = [createMockIssue('bb-1', { labels: null as unknown as string[] })];
assert.throws(() => {
diffSnapshots(prev, curr);
}, /TypeError|Cannot read properties of null/);
});
it('should NOT crash when dependencies is null (DOCUMENTS BUG - will throw TypeError)', () => {
const prev = [createMockIssue('bb-1', { dependencies: null as unknown as BeadDependency[] })];
const curr = [createMockIssue('bb-1', { dependencies: [] })];
assert.throws(() => {
diffSnapshots(prev, curr);
}, /TypeError|Cannot read properties of null/);
});
it('should NOT crash when dependencies is undefined in curr (DOCUMENTS BUG - will throw TypeError)', () => {
const prev = [createMockIssue('bb-1', { dependencies: [] })];
const curr = [createMockIssue('bb-1', { dependencies: undefined as unknown as BeadDependency[] })];
assert.throws(() => {
diffSnapshots(prev, curr);
}, /TypeError|Cannot read properties of undefined/);
});
});
describe('EMPTY ARRAYS', () => {
it('should not emit event when labels goes from empty to empty', () => {
const prev = [createMockIssue('bb-1', { labels: [] })];
const curr = [createMockIssue('bb-1', { labels: [] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should emit labels_changed when labels goes from empty to non-empty', () => {
const prev = [createMockIssue('bb-1', { labels: [] })];
const curr = [createMockIssue('bb-1', { labels: ['bug'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'labels_changed');
});
it('should emit labels_changed when labels goes from non-empty to empty', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
const curr = [createMockIssue('bb-1', { labels: [] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'labels_changed');
});
it('should not emit event when dependencies goes from empty to empty', () => {
const prev = [createMockIssue('bb-1', { dependencies: [] })];
const curr = [createMockIssue('bb-1', { dependencies: [] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should emit dependency_added when dependencies goes from empty to non-empty', () => {
const prev = [createMockIssue('bb-1', { dependencies: [] })];
const curr = [createMockIssue('bb-1', { dependencies: [{ type: 'blocks', target: 'bb-2' }] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'dependency_added');
});
it('should emit dependency_removed when dependencies goes from non-empty to empty', () => {
const prev = [createMockIssue('bb-1', { dependencies: [{ type: 'blocks', target: 'bb-2' }] })];
const curr = [createMockIssue('bb-1', { dependencies: [] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'dependency_removed');
});
});
describe('DELETION DETECTION', () => {
it('should NOT emit any event when issue is deleted (DOCUMENTS CURRENT BEHAVIOR - no deletion event)', () => {
const prev = [createMockIssue('bb-1')];
const curr: BeadIssueWithProject[] = [];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should NOT emit deletion event when multiple issues are removed', () => {
const prev = [
createMockIssue('bb-1'),
createMockIssue('bb-2'),
createMockIssue('bb-3'),
];
const curr = [createMockIssue('bb-2')];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should still emit created events for new issues even when others are deleted', () => {
const prev = [createMockIssue('bb-1')];
const curr = [createMockIssue('bb-2')];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'created');
assert.strictEqual(events[0].beadId, 'bb-2');
});
});
describe('LABELS ORDER INDEPENDENCE', () => {
it('should NOT emit labels_changed when same labels are in different order', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug', 'ui', 'urgent'] })];
const curr = [createMockIssue('bb-1', { labels: ['urgent', 'bug', 'ui'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should NOT emit labels_changed for single label (trivially same order)', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
const curr = [createMockIssue('bb-1', { labels: ['bug'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should handle large label sets with shuffled order', () => {
const labels = Array.from({ length: 20 }, (_, i) => `label-${i}`);
const shuffled = [...labels].reverse();
const prev = [createMockIssue('bb-1', { labels })];
const curr = [createMockIssue('bb-1', { labels: shuffled })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
});
describe('DUPLICATE LABELS', () => {
it('should NOT emit event when labels have same duplicates in same order', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug', 'bug', 'ui'] })];
const curr = [createMockIssue('bb-1', { labels: ['bug', 'bug', 'ui'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should NOT emit event when labels have same duplicates in different order', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug', 'bug', 'ui'] })];
const curr = [createMockIssue('bb-1', { labels: ['ui', 'bug', 'bug'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should emit event when duplicate count differs', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug', 'bug'] })];
const curr = [createMockIssue('bb-1', { labels: ['bug'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'labels_changed');
});
it('should emit event when new duplicate is added', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
const curr = [createMockIssue('bb-1', { labels: ['bug', 'bug'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'labels_changed');
});
});
describe('EDGE CASES', () => {
it('should handle null previous snapshot (first load)', () => {
const curr = [createMockIssue('bb-1'), createMockIssue('bb-2')];
const events = diffSnapshots(null, curr);
assert.strictEqual(events.length, 2);
events.forEach(e => {
assert.strictEqual(e.kind, 'created');
});
});
it('should handle empty previous and current snapshots', () => {
const events = diffSnapshots([], []);
assert.strictEqual(events.length, 0);
});
it('should handle same issue in both snapshots with no changes', () => {
const issue = createMockIssue('bb-1');
const events = diffSnapshots([issue], [issue]);
assert.strictEqual(events.length, 0);
});
it('should handle multiple dependency changes on same bead', () => {
const prev = [createMockIssue('bb-1', {
dependencies: [
{ type: 'blocks', target: 'bb-2' },
{ type: 'relates_to', target: 'bb-3' },
],
})];
const curr = [createMockIssue('bb-1', {
dependencies: [
{ type: 'blocks', target: 'bb-4' },
],
})];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 3);
const kinds = events.map(e => e.kind);
assert.ok(kinds.includes('dependency_added'));
assert.ok(kinds.filter(k => k === 'dependency_removed').length === 2);
});
it('should generate valid UUIDs for all events', () => {
const prev: BeadIssueWithProject[] = [];
const curr = [createMockIssue('bb-1')];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
assert.match(events[0].id, uuidRegex);
});
it('should include correct project info in events', () => {
const prev: BeadIssueWithProject[] = [];
const curr = [createMockIssue('bb-1')];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events[0].projectId, 'proj-1');
assert.strictEqual(events[0].projectName, 'Test Project');
});
it('should compute actor from assignee or owner or created_by', () => {
const events1 = diffSnapshots([], [createMockIssue('bb-1', { assignee: 'alice', owner: 'bob', created_by: 'charlie' })]);
assert.strictEqual(events1[0].actor, 'alice');
const events2 = diffSnapshots([], [createMockIssue('bb-1', { assignee: null, owner: 'bob', created_by: 'charlie' })]);
assert.strictEqual(events2[0].actor, 'bob');
const events3 = diffSnapshots([], [createMockIssue('bb-1', { assignee: null, owner: null, created_by: 'charlie' })]);
assert.strictEqual(events3[0].actor, 'charlie');
});
});
});