feat(ux): consolidate Launch Swarm + telemetry UX with minimized strip
- Removed broken LaunchSwarmDialog (formula-based) from TopBar/LeftPanel - All Rocket buttons (TopBar, LeftPanel, DAG nodes, social cards) now open AssignmentPanel (archetype-based) which actually works - Every Rocket clears taskId first so assignMode && !taskId condition passes - Conversation button priority: taskId always shows conversation, not assign panel - Added TelemetryStrip: minimized right sidebar with status dots when non-telemetry panel (conversation/assignment) is active - Live feed has minimize button → restores last taskId or assignMode - DAG nodes: Signal icon → restores telemetry feed - Social button on DAG nodes: single router.push to avoid race (setView + setTaskId) - Fixed social card message button: opens right panel with drawer:closed (no popup) Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
parent
65d69ecbbc
commit
c246ceaf21
165 changed files with 13730 additions and 1132 deletions
33
tests/api/bd-health-route.test.ts
Normal file
33
tests/api/bd-health-route.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { GET as healthGet } from '../../src/app/api/bd/health/route';
|
||||
|
||||
test('bd health route returns setup guidance when bd is missing from PATH', async () => {
|
||||
const previousPath = process.env.PATH;
|
||||
const previousPathAlt = process.env.Path;
|
||||
process.env.PATH = '';
|
||||
process.env.Path = '';
|
||||
|
||||
try {
|
||||
const response = await healthGet(new Request('http://localhost/api/bd/health?projectRoot=C:/repo/test'));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(body.ok, false);
|
||||
assert.equal(body.error.classification, 'not_found');
|
||||
assert.equal(typeof body.error.message, 'string');
|
||||
assert.equal(String(body.error.message).includes('bd command not found in PATH'), true);
|
||||
} finally {
|
||||
if (previousPath === undefined) {
|
||||
delete process.env.PATH;
|
||||
} else {
|
||||
process.env.PATH = previousPath;
|
||||
}
|
||||
if (previousPathAlt === undefined) {
|
||||
delete process.env.Path;
|
||||
} else {
|
||||
process.env.Path = previousPathAlt;
|
||||
}
|
||||
}
|
||||
});
|
||||
67
tests/api/coord-events-route.test.ts
Normal file
67
tests/api/coord-events-route.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { handleCoordEventsPost } from '../../src/app/api/coord/events/route';
|
||||
|
||||
test('handleCoordEventsPost returns 400 for invalid body', async () => {
|
||||
const request = new Request('http://localhost/api/coord/events', {
|
||||
method: 'POST',
|
||||
body: 'not-json',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
const response = await handleCoordEventsPost(request);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(body.ok, false);
|
||||
});
|
||||
|
||||
test('handleCoordEventsPost writes event and returns success', async () => {
|
||||
const request = new Request('http://localhost/api/coord/events', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
projectRoot: process.cwd(),
|
||||
event: {
|
||||
version: 'coord.v1',
|
||||
kind: 'coord_event',
|
||||
issue_id: 'bb-123',
|
||||
actor: 'amber-otter',
|
||||
timestamp: '2026-02-28T18:00:00.000Z',
|
||||
data: {
|
||||
event_type: 'SEND',
|
||||
event_id: 'evt_01',
|
||||
project_root: process.cwd(),
|
||||
to_agent: 'cobalt-harbor',
|
||||
state: 'unread',
|
||||
payload: { subject: 's', body: 'b' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
const response = await handleCoordEventsPost(request, {
|
||||
writeCoordEvent: async () => ({
|
||||
ok: true,
|
||||
eventId: 'evt_01',
|
||||
commandResult: {
|
||||
success: true,
|
||||
classification: null,
|
||||
command: 'bd',
|
||||
args: [],
|
||||
cwd: process.cwd(),
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
durationMs: 1,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.ok, true);
|
||||
assert.equal(body.eventId, 'evt_01');
|
||||
});
|
||||
72
tests/components/sessions/conversation-drawer-coord.test.tsx
Normal file
72
tests/components/sessions/conversation-drawer-coord.test.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { buildCommentMutationBody, buildCoordMessageActionEvent } from '../../../src/components/sessions/conversation-drawer';
|
||||
|
||||
describe('ConversationDrawer coord action payloads', () => {
|
||||
const message = {
|
||||
message_id: 'evt_send_1',
|
||||
thread_id: 'bead:bb-123',
|
||||
bead_id: 'bb-123',
|
||||
from_agent: 'amber-otter',
|
||||
to_agent: 'cobalt-harbor',
|
||||
category: 'HANDOFF',
|
||||
subject: 'subject',
|
||||
body: 'body',
|
||||
state: 'unread',
|
||||
requires_ack: true,
|
||||
created_at: '2026-02-28T10:00:00.000Z',
|
||||
read_at: null,
|
||||
acked_at: null,
|
||||
} as const;
|
||||
|
||||
it('builds READ event with event_ref to message id', () => {
|
||||
const payload = buildCoordMessageActionEvent({
|
||||
action: 'read',
|
||||
message: message as any,
|
||||
beadId: 'bb-123',
|
||||
projectRoot: '/tmp/repo',
|
||||
nowIso: '2026-02-28T11:00:00.000Z',
|
||||
}) as any;
|
||||
|
||||
assert.equal(payload.kind, 'coord_event');
|
||||
assert.equal(payload.data.event_type, 'READ');
|
||||
assert.equal(payload.data.event_ref, 'evt_send_1');
|
||||
});
|
||||
|
||||
it('builds ACK event with recipient as actor', () => {
|
||||
const payload = buildCoordMessageActionEvent({
|
||||
action: 'ack',
|
||||
message: message as any,
|
||||
beadId: 'bb-123',
|
||||
projectRoot: '/tmp/repo',
|
||||
nowIso: '2026-02-28T11:00:00.000Z',
|
||||
}) as any;
|
||||
|
||||
assert.equal(payload.actor, 'cobalt-harbor');
|
||||
assert.equal(payload.data.event_type, 'ACK');
|
||||
assert.equal(payload.issue_id, 'bb-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConversationDrawer comment payload', () => {
|
||||
it('includes actor when provided', () => {
|
||||
const payload = buildCommentMutationBody({
|
||||
projectRoot: '/tmp/repo',
|
||||
text: 'hello',
|
||||
actor: 'zenchant',
|
||||
}) as any;
|
||||
|
||||
assert.equal(payload.actor, 'zenchant');
|
||||
});
|
||||
|
||||
it('omits actor when blank', () => {
|
||||
const payload = buildCommentMutationBody({
|
||||
projectRoot: '/tmp/repo',
|
||||
text: 'hello',
|
||||
actor: ' ',
|
||||
}) as any;
|
||||
|
||||
assert.equal('actor' in payload, false);
|
||||
});
|
||||
});
|
||||
|
|
@ -40,6 +40,12 @@ test('UnifiedShell - imports AssignmentPanel', async () => {
|
|||
assert.ok(fileContent.includes('AssignmentPanel'), 'Should import AssignmentPanel');
|
||||
});
|
||||
|
||||
test('UnifiedShell - checks bd health and renders setup warning', async () => {
|
||||
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/unified-shell.tsx'), 'utf-8');
|
||||
assert.ok(fileContent.includes('useBdHealth'), 'Should use bd health hook');
|
||||
assert.ok(fileContent.includes('BD setup issue:'), 'Should show bd setup warning text');
|
||||
});
|
||||
|
||||
// Test that AssignmentPanel is rendered conditionally based on view and assignMode
|
||||
test('UnifiedShell - renders AssignmentPanel conditionally', async () => {
|
||||
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/unified-shell.tsx'), 'utf-8');
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { BdExecutableNotFoundError, resolveBdExecutable } from '../../src/lib/bd-path';
|
||||
|
||||
test('resolveBdExecutable prefers explicit configured path when provided', async () => {
|
||||
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-'));
|
||||
const explicit = path.join(temp, 'tools', 'bd.exe');
|
||||
await fs.mkdir(path.dirname(explicit), { recursive: true });
|
||||
await fs.writeFile(explicit, '');
|
||||
|
||||
const resolved = await resolveBdExecutable({ explicitPath: explicit, env: { Path: '', NODE_ENV: 'test' } });
|
||||
|
||||
assert.equal(resolved.executable, explicit);
|
||||
assert.equal(resolved.source, 'config');
|
||||
});
|
||||
|
||||
test('resolveBdExecutable finds bd.exe on PATH when explicit path is not set', async () => {
|
||||
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-bd-path-env-'));
|
||||
const candidate = path.join(temp, 'bd.exe');
|
||||
await fs.writeFile(candidate, '');
|
||||
|
||||
const resolved = await resolveBdExecutable({ env: { Path: temp, NODE_ENV: 'test' } });
|
||||
|
||||
assert.equal(resolved.executable, candidate);
|
||||
assert.equal(resolved.source, 'path');
|
||||
});
|
||||
|
||||
test('resolveBdExecutable throws actionable setup guidance when executable is missing', async () => {
|
||||
await assert.rejects(
|
||||
() => resolveBdExecutable({ env: { Path: '', NODE_ENV: 'test' } }),
|
||||
(error: unknown) => {
|
||||
assert.equal(error instanceof BdExecutableNotFoundError, true);
|
||||
const message = String((error as Error).message).toLowerCase();
|
||||
assert.equal(message.includes('npm install -g @beads/bd'), true);
|
||||
assert.equal(message.includes('bd.exe'), true);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import test from 'node:test';
|
|||
import assert from 'node:assert/strict';
|
||||
|
||||
import { runBdCommand } from '../../src/lib/bridge';
|
||||
import { normalizeProjectRootForRuntime } from '../../src/lib/project-root';
|
||||
|
||||
test('runBdCommand returns structured success payload from exec output', async () => {
|
||||
const result = await runBdCommand(
|
||||
|
|
@ -12,12 +13,11 @@ test('runBdCommand returns structured success payload from exec output', async (
|
|||
explicitBdPath: 'C:/tools/bd.exe',
|
||||
},
|
||||
{
|
||||
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
||||
exec: async (command: string, options: any) => {
|
||||
assert.ok(command.includes('bd'));
|
||||
assert.ok(command.startsWith('bd '));
|
||||
assert.ok(command.includes('list'));
|
||||
assert.ok(command.includes('--json'));
|
||||
assert.equal(options.cwd, 'C:/repo/project');
|
||||
assert.equal(options.cwd, normalizeProjectRootForRuntime('C:/repo/project'));
|
||||
return { stdout: '[{"id":"bb-1"}]\r\n', stderr: '' };
|
||||
},
|
||||
},
|
||||
|
|
@ -32,7 +32,6 @@ test('runBdCommand classifies missing executable as not_found', async () => {
|
|||
const result = await runBdCommand(
|
||||
{ projectRoot: 'C:/repo/project', args: ['list'] },
|
||||
{
|
||||
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
||||
exec: async () => {
|
||||
const error = new Error('spawn ENOENT') as NodeJS.ErrnoException;
|
||||
error.code = 'ENOENT';
|
||||
|
|
@ -49,7 +48,6 @@ test('runBdCommand classifies timeout failures', async () => {
|
|||
const result = await runBdCommand(
|
||||
{ projectRoot: 'C:/repo/project', args: ['list'], timeoutMs: 5 },
|
||||
{
|
||||
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
||||
exec: async () => {
|
||||
const error = new Error('timed out') as NodeJS.ErrnoException & { killed?: boolean; signal?: string };
|
||||
error.code = 'ETIMEDOUT';
|
||||
|
|
@ -68,7 +66,6 @@ test('runBdCommand classifies non-zero bad-argument exits', async () => {
|
|||
const result = await runBdCommand(
|
||||
{ projectRoot: 'C:/repo/project', args: ['update', '--bad-flag'] },
|
||||
{
|
||||
resolveBdExecutable: async () => ({ executable: 'C:/tools/bd.exe', source: 'config' }),
|
||||
exec: async () => {
|
||||
const error = new Error('exit code 1') as NodeJS.ErrnoException & {
|
||||
stdout?: string;
|
||||
|
|
@ -85,3 +82,26 @@ test('runBdCommand classifies non-zero bad-argument exits', async () => {
|
|||
assert.equal(result.success, false);
|
||||
assert.equal(result.classification, 'bad_args');
|
||||
});
|
||||
|
||||
test('runBdCommand treats shell "not recognized" stderr as not_found', async () => {
|
||||
const result = await runBdCommand(
|
||||
{ projectRoot: 'C:/repo/project', args: ['list'] },
|
||||
{
|
||||
exec: async () => {
|
||||
const error = new Error('exit code 1') as NodeJS.ErrnoException & {
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
exitCode?: number;
|
||||
};
|
||||
error.code = 'BD_EXIT';
|
||||
error.stderr = `'bd' is not recognized as an internal or external command`;
|
||||
error.exitCode = 1;
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.classification, 'not_found');
|
||||
assert.equal(result.error?.includes('bd command not found in PATH'), true);
|
||||
});
|
||||
|
|
|
|||
68
tests/lib/coord-events.test.ts
Normal file
68
tests/lib/coord-events.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { writeCoordEvent } from '../../src/lib/coord-events';
|
||||
|
||||
function validEnvelope() {
|
||||
return {
|
||||
version: 'coord.v1',
|
||||
kind: 'coord_event',
|
||||
issue_id: 'bb-123',
|
||||
actor: 'amber-otter',
|
||||
timestamp: '2026-02-28T18:00:00.000Z',
|
||||
data: {
|
||||
event_type: 'SEND',
|
||||
event_id: 'evt_01',
|
||||
project_root: '/tmp/repo',
|
||||
to_agent: 'cobalt-harbor',
|
||||
state: 'unread',
|
||||
payload: {
|
||||
subject: 'Need review',
|
||||
body: 'Please check API changes',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('writeCoordEvent rejects invalid payload', async () => {
|
||||
const result = await writeCoordEvent(
|
||||
{ version: 'coord.v1' },
|
||||
{ projectRoot: '/tmp/repo' },
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error?.classification, 'bad_args');
|
||||
});
|
||||
|
||||
test('writeCoordEvent invokes bd audit record with --stdin payload', async () => {
|
||||
let capturedArgs: string[] | null = null;
|
||||
let capturedStdin = '';
|
||||
|
||||
const result = await writeCoordEvent(
|
||||
validEnvelope(),
|
||||
{ projectRoot: '/tmp/repo' },
|
||||
{
|
||||
runBdCommand: async (options) => {
|
||||
capturedArgs = options.args;
|
||||
capturedStdin = options.stdinText ?? '';
|
||||
return {
|
||||
success: true,
|
||||
classification: null,
|
||||
command: 'bd',
|
||||
args: options.args,
|
||||
cwd: options.projectRoot,
|
||||
stdout: '{"ok":true}',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
durationMs: 1,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(capturedArgs, ['audit', 'record', '--stdin', '--json']);
|
||||
assert.match(capturedStdin, /"coord_event"/);
|
||||
assert.match(capturedStdin, /"SEND"/);
|
||||
});
|
||||
90
tests/lib/coord-projections-inbox.test.ts
Normal file
90
tests/lib/coord-projections-inbox.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { projectInbox, projectMessageState, type CoordProtocolEvent } from '../../src/lib/coord-projections';
|
||||
|
||||
const baseSend: CoordProtocolEvent = {
|
||||
version: 'coord.v1',
|
||||
kind: 'coord_event',
|
||||
issue_id: 'bb-123',
|
||||
actor: 'amber-otter',
|
||||
timestamp: '2026-02-28T10:00:00.000Z',
|
||||
data: {
|
||||
event_type: 'SEND',
|
||||
event_id: 'evt_send_1',
|
||||
project_root: '/tmp/repo',
|
||||
to_agent: 'cobalt-harbor',
|
||||
state: 'unread',
|
||||
payload: {
|
||||
subject: 'handoff',
|
||||
body: 'please review',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('projectMessageState derives unread -> read -> acked', () => {
|
||||
const events: CoordProtocolEvent[] = [
|
||||
{
|
||||
...baseSend,
|
||||
timestamp: '2026-02-28T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
...baseSend,
|
||||
timestamp: '2026-02-28T10:01:00.000Z',
|
||||
data: {
|
||||
event_type: 'READ',
|
||||
event_id: 'evt_read_1',
|
||||
event_ref: 'evt_send_1',
|
||||
project_root: '/tmp/repo',
|
||||
payload: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseSend,
|
||||
timestamp: '2026-02-28T10:02:00.000Z',
|
||||
data: {
|
||||
event_type: 'ACK',
|
||||
event_id: 'evt_ack_1',
|
||||
event_ref: 'evt_send_1',
|
||||
project_root: '/tmp/repo',
|
||||
payload: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const state = projectMessageState(events);
|
||||
assert.equal(state.get('evt_send_1'), 'acked');
|
||||
});
|
||||
|
||||
test('projectInbox tolerates out-of-order and unknown refs', () => {
|
||||
const events: CoordProtocolEvent[] = [
|
||||
{
|
||||
...baseSend,
|
||||
timestamp: '2026-02-28T10:02:00.000Z',
|
||||
data: {
|
||||
event_type: 'ACK',
|
||||
event_id: 'evt_ack_unknown',
|
||||
event_ref: 'evt_missing',
|
||||
project_root: '/tmp/repo',
|
||||
payload: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseSend,
|
||||
timestamp: '2026-02-28T10:01:00.000Z',
|
||||
data: {
|
||||
event_type: 'READ',
|
||||
event_id: 'evt_read_1',
|
||||
event_ref: 'evt_send_1',
|
||||
project_root: '/tmp/repo',
|
||||
payload: {},
|
||||
},
|
||||
},
|
||||
baseSend,
|
||||
];
|
||||
|
||||
const inbox = projectInbox(events, 'bb-123', 'cobalt-harbor');
|
||||
assert.equal(inbox.length, 1);
|
||||
assert.equal(inbox[0].state, 'read');
|
||||
assert.equal(inbox[0].to_agent, 'cobalt-harbor');
|
||||
});
|
||||
89
tests/lib/coord-projections-reservations.test.ts
Normal file
89
tests/lib/coord-projections-reservations.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
calculateReservationIncursions,
|
||||
isTakeoverAllowed,
|
||||
projectReservations,
|
||||
type CoordProtocolEvent,
|
||||
} from '../../src/lib/coord-projections';
|
||||
|
||||
function reserveEvent(input: {
|
||||
actor: string;
|
||||
scope: string;
|
||||
bead: string;
|
||||
at: string;
|
||||
type?: 'RESERVE' | 'RELEASE' | 'TAKEOVER';
|
||||
takeoverMode?: 'stale' | 'evicted';
|
||||
}): CoordProtocolEvent {
|
||||
return {
|
||||
version: 'coord.v1',
|
||||
kind: 'coord_event',
|
||||
issue_id: input.bead,
|
||||
actor: input.actor,
|
||||
timestamp: input.at,
|
||||
data: {
|
||||
event_type: input.type ?? 'RESERVE',
|
||||
event_id: `${input.type ?? 'RESERVE'}-${input.actor}-${Date.parse(input.at)}`,
|
||||
project_root: '/tmp/repo',
|
||||
scope: input.scope,
|
||||
takeover_mode: input.takeoverMode,
|
||||
payload: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('isTakeoverAllowed enforces active/stale/evicted policy', () => {
|
||||
assert.equal(isTakeoverAllowed('active', 'stale'), false);
|
||||
assert.equal(isTakeoverAllowed('stale', 'stale'), true);
|
||||
assert.equal(isTakeoverAllowed('stale', 'evicted'), false);
|
||||
assert.equal(isTakeoverAllowed('evicted', 'stale'), true);
|
||||
assert.equal(isTakeoverAllowed('evicted', 'evicted'), true);
|
||||
});
|
||||
|
||||
test('projectReservations applies reserve/release transitions', () => {
|
||||
const events: CoordProtocolEvent[] = [
|
||||
reserveEvent({ actor: 'agent-a', scope: 'src/lib/*', bead: 'bb-1', at: '2026-02-28T10:00:00.000Z' }),
|
||||
reserveEvent({ actor: 'agent-a', scope: 'src/lib/*', bead: 'bb-1', at: '2026-02-28T10:01:00.000Z', type: 'RELEASE' }),
|
||||
];
|
||||
|
||||
const reservations = projectReservations(events, { 'agent-a': 'active' });
|
||||
assert.equal(reservations.length, 0);
|
||||
});
|
||||
|
||||
test('projectReservations rejects stale takeover when owner active', () => {
|
||||
const events: CoordProtocolEvent[] = [
|
||||
reserveEvent({ actor: 'agent-a', scope: 'src/lib/*', bead: 'bb-1', at: '2026-02-28T10:00:00.000Z' }),
|
||||
reserveEvent({ actor: 'agent-b', scope: 'src/lib/*', bead: 'bb-2', at: '2026-02-28T10:01:00.000Z', type: 'TAKEOVER', takeoverMode: 'stale' }),
|
||||
];
|
||||
|
||||
const reservations = projectReservations(events, { 'agent-a': 'active' });
|
||||
assert.equal(reservations.length, 1);
|
||||
assert.equal(reservations[0].agent_id, 'agent-a');
|
||||
});
|
||||
|
||||
test('projectReservations allows stale takeover when owner stale', () => {
|
||||
const events: CoordProtocolEvent[] = [
|
||||
reserveEvent({ actor: 'agent-a', scope: 'src/lib/*', bead: 'bb-1', at: '2026-02-28T10:00:00.000Z' }),
|
||||
reserveEvent({ actor: 'agent-b', scope: 'src/lib/*', bead: 'bb-2', at: '2026-02-28T10:01:00.000Z', type: 'TAKEOVER', takeoverMode: 'stale' }),
|
||||
];
|
||||
|
||||
const reservations = projectReservations(events, { 'agent-a': 'stale' });
|
||||
assert.equal(reservations.length, 1);
|
||||
assert.equal(reservations[0].agent_id, 'agent-b');
|
||||
assert.equal(reservations[0].takeover_mode, 'stale');
|
||||
});
|
||||
|
||||
test('calculateReservationIncursions finds partial overlap', () => {
|
||||
const reservations = projectReservations(
|
||||
[
|
||||
reserveEvent({ actor: 'agent-a', scope: 'src/lib/*', bead: 'bb-1', at: '2026-02-28T10:00:00.000Z' }),
|
||||
reserveEvent({ actor: 'agent-b', scope: 'src/lib/parser.ts', bead: 'bb-2', at: '2026-02-28T10:01:00.000Z' }),
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
const incursions = calculateReservationIncursions(reservations);
|
||||
assert.equal(incursions.length, 1);
|
||||
assert.equal(incursions[0].severity, 'partial');
|
||||
});
|
||||
63
tests/lib/coord-schema.test.ts
Normal file
63
tests/lib/coord-schema.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { validateCoordEventEnvelope } from '../../src/lib/coord-schema';
|
||||
|
||||
function baseEnvelope(eventType: string): any {
|
||||
return {
|
||||
version: 'coord.v1',
|
||||
kind: 'coord_event',
|
||||
issue_id: 'bb-123',
|
||||
actor: 'amber-otter',
|
||||
timestamp: '2026-02-28T18:00:00.000Z',
|
||||
data: {
|
||||
event_type: eventType,
|
||||
event_id: 'evt_01JN6Y1Q7R80E8P6K1Q5',
|
||||
project_root: '/tmp/repo',
|
||||
payload: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('validateCoordEventEnvelope accepts valid SEND', () => {
|
||||
const input = baseEnvelope('SEND');
|
||||
input.data.to_agent = 'cobalt-harbor';
|
||||
input.data.state = 'unread';
|
||||
input.data.payload = { subject: 's', body: 'b' };
|
||||
|
||||
const result = validateCoordEventEnvelope(input);
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
|
||||
test('validateCoordEventEnvelope rejects READ without event_ref', () => {
|
||||
const input = baseEnvelope('READ');
|
||||
|
||||
const result = validateCoordEventEnvelope(input);
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) {
|
||||
assert.match(result.error, /event_ref/i);
|
||||
}
|
||||
});
|
||||
|
||||
test('validateCoordEventEnvelope accepts TAKEOVER with stale mode', () => {
|
||||
const input = baseEnvelope('TAKEOVER');
|
||||
input.data.scope = 'src/lib/*';
|
||||
input.data.takeover_mode = 'stale';
|
||||
input.data.reason = 'owner stale';
|
||||
|
||||
const result = validateCoordEventEnvelope(input);
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
|
||||
test('validateCoordEventEnvelope rejects TAKEOVER with invalid mode', () => {
|
||||
const input = baseEnvelope('TAKEOVER');
|
||||
input.data.scope = 'src/lib/*';
|
||||
input.data.takeover_mode = 'none';
|
||||
input.data.reason = 'owner stale';
|
||||
|
||||
const result = validateCoordEventEnvelope(input);
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) {
|
||||
assert.match(result.error, /takeover_mode/i);
|
||||
}
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ test('executeMutation surfaces bridge failures in normalized response', async ()
|
|||
return {
|
||||
success: false,
|
||||
classification: 'non_zero_exit',
|
||||
command: 'bd.exe',
|
||||
command: 'bd',
|
||||
args,
|
||||
cwd: root,
|
||||
stdout: '',
|
||||
|
|
@ -93,7 +93,7 @@ test('executeMutation returns successful normalized response', async () => {
|
|||
return {
|
||||
success: true,
|
||||
classification: null,
|
||||
command: 'bd.exe',
|
||||
command: 'bd',
|
||||
args,
|
||||
cwd: root,
|
||||
stdout: '{"id":"bb-123"}',
|
||||
|
|
@ -109,3 +109,62 @@ test('executeMutation returns successful normalized response', async () => {
|
|||
assert.equal(result.operation, 'update');
|
||||
assert.equal(result.command.success, true);
|
||||
});
|
||||
|
||||
test('executeMutation includes --actor when provided in payload', async () => {
|
||||
const payload = validateMutationPayload('comment', {
|
||||
projectRoot: root,
|
||||
id: 'bb-123',
|
||||
text: 'Operator note',
|
||||
actor: 'zenchant',
|
||||
});
|
||||
|
||||
const result = await executeMutation('comment', payload, {
|
||||
runBdCommand: async ({ args }) => {
|
||||
assert.deepEqual(args, ['--actor', 'zenchant', 'comments', 'add', 'bb-123', 'Operator note', '--json']);
|
||||
return {
|
||||
success: true,
|
||||
classification: null,
|
||||
command: 'bd',
|
||||
args,
|
||||
cwd: root,
|
||||
stdout: '{"ok":true}',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
durationMs: 2,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
|
||||
test('executeMutation ignores bdPath and uses default runner contract', async () => {
|
||||
const payload = validateMutationPayload('update', {
|
||||
projectRoot: root,
|
||||
id: 'bb-123',
|
||||
status: 'in_progress',
|
||||
bdPath: 'C:/Tools/beads/bd.exe',
|
||||
});
|
||||
|
||||
const result = await executeMutation('update', payload, {
|
||||
runBdCommand: async (options) => {
|
||||
assert.equal(options.explicitBdPath, undefined);
|
||||
assert.deepEqual(options.args, ['update', 'bb-123', '-s', 'in_progress', '--json']);
|
||||
return {
|
||||
success: true,
|
||||
classification: null,
|
||||
command: 'bd',
|
||||
args: options.args,
|
||||
cwd: root,
|
||||
stdout: '{"ok":true}',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
durationMs: 2,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue