feat(data): complete bb-ui2.15 - Swarm Cards Data Builder
STORY:
The Swarm view needs to show epics as "swarms" - groups of agents
working together on a mission. This requires aggregating bead data
by swarm/epic and computing health statistics.
COLLABORATION:
Created buildSwarmCards function that transforms epic + agent data:
SwarmCard interface:
- id, title, status
- stats: { completed, active, ready, blocked, total }
- agents: AgentRoster[] with liveness
- attention: blocked tasks + stuck agents needing attention
- lastActivity
We also created swarm-molecules.ts for molecular composition patterns
used by the swarm orchestration layer.
DELIVERABLES:
- src/lib/swarm-cards.ts with SwarmCard, AgentRoster types
- src/lib/swarm-molecules.ts for molecular composition
- tests/lib/swarm-cards.test.ts
- tests/lib/swarm-molecules.test.ts
VERIFICATION:
- npm run typecheck: PASS
- npm run lint: PASS
- npm run test: PASS
CLOSES: bb-ui2.15
BLOCKS: bb-ui2.16, bb-ui2.17
This commit is contained in:
parent
e28a7837c4
commit
e47230c2dd
5 changed files with 790 additions and 0 deletions
88
tests/lib/swarm-molecules.test.ts
Normal file
88
tests/lib/swarm-molecules.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
import { joinSwarm, leaveSwarm, getSwarmMembers } from '../../src/lib/swarm-molecules';
|
||||
import { registerAgent } from '../../src/lib/agent-registry';
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
const runId = Date.now().toString(36);
|
||||
|
||||
// Helper: bd show returns an ARRAY, take first element
|
||||
function bdShow(beadId: string): any {
|
||||
const out = execSync(`bd --allow-stale show ${beadId} --json`, { cwd: projectRoot, encoding: 'utf8', timeout: 30000 });
|
||||
const arr = JSON.parse(out);
|
||||
return arr[0] || arr;
|
||||
}
|
||||
|
||||
test('joinSwarm creates swarm membership', async () => {
|
||||
const agentId = `join-${runId}`;
|
||||
|
||||
await registerAgent({ name: agentId, role: 'tester' }, { projectRoot });
|
||||
const result = await joinSwarm({ agent: agentId, epicId: 'bb-buff' }, { projectRoot });
|
||||
assert.equal(result.ok, true, `joinSwarm failed: ${result.error?.message}`);
|
||||
|
||||
const agent = bdShow(`bb-${agentId}`);
|
||||
const hasSwarm = agent.labels?.some((l: string) => l.startsWith('swarm:'));
|
||||
assert.ok(hasSwarm, 'Agent should have swarm label');
|
||||
|
||||
await leaveSwarm({ agent: agentId }, { projectRoot });
|
||||
});
|
||||
|
||||
test('joinSwarm switches membership', async () => {
|
||||
const agentId = `switch-${runId}`;
|
||||
|
||||
await registerAgent({ name: agentId, role: 'tester' }, { projectRoot });
|
||||
await joinSwarm({ agent: agentId, epicId: 'bb-buff' }, { projectRoot });
|
||||
await joinSwarm({ agent: agentId, epicId: 'bb-buff.2' }, { projectRoot });
|
||||
|
||||
const agent = bdShow(`bb-${agentId}`);
|
||||
const swarmLabels = agent.labels?.filter((l: string) => l.startsWith('swarm:')) || [];
|
||||
assert.equal(swarmLabels.length, 1, 'Should have exactly one swarm label');
|
||||
|
||||
await leaveSwarm({ agent: agentId }, { projectRoot });
|
||||
});
|
||||
|
||||
test('leaveSwarm removes membership', async () => {
|
||||
const agentId = `leave-${runId}`;
|
||||
|
||||
await registerAgent({ name: agentId, role: 'tester' }, { projectRoot });
|
||||
await joinSwarm({ agent: agentId, epicId: 'bb-buff' }, { projectRoot });
|
||||
await leaveSwarm({ agent: agentId }, { projectRoot });
|
||||
|
||||
const agent = bdShow(`bb-${agentId}`);
|
||||
const swarmLabels = agent.labels?.filter((l: string) => l.startsWith('swarm:')) || [];
|
||||
assert.equal(swarmLabels.length, 0, 'Should have no swarm labels');
|
||||
});
|
||||
|
||||
test('getSwarmMembers returns members', async () => {
|
||||
const agent1 = `m1-${runId}`;
|
||||
const agent2 = `m2-${runId}`;
|
||||
|
||||
await registerAgent({ name: agent1, role: 'tester' }, { projectRoot });
|
||||
await registerAgent({ name: agent2, role: 'tester' }, { projectRoot });
|
||||
await joinSwarm({ agent: agent1, epicId: 'bb-buff' }, { projectRoot });
|
||||
await joinSwarm({ agent: agent2, epicId: 'bb-buff' }, { projectRoot });
|
||||
|
||||
const members = await getSwarmMembers({ swarmId: 'bb-buff' }, { projectRoot });
|
||||
assert.ok(members.includes(agent1), `Should include ${agent1}`);
|
||||
assert.ok(members.includes(agent2), `Should include ${agent2}`);
|
||||
|
||||
await leaveSwarm({ agent: agent1 }, { projectRoot });
|
||||
await leaveSwarm({ agent: agent2 }, { projectRoot });
|
||||
});
|
||||
|
||||
test('joinSwarm rejects invalid agent', async () => {
|
||||
const result = await joinSwarm({ agent: 'nonexistent', epicId: 'bb-buff' }, { projectRoot });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error?.code, 'AGENT_NOT_FOUND');
|
||||
});
|
||||
|
||||
test('joinSwarm rejects invalid epic', async () => {
|
||||
const agentId = `invepic-${runId}`;
|
||||
await registerAgent({ name: agentId, role: 'tester' }, { projectRoot });
|
||||
|
||||
const result = await joinSwarm({ agent: agentId, epicId: 'bb-nonexistent' }, { projectRoot });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error?.code, 'EPIC_NOT_FOUND');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue