diff --git a/package.json b/package.json index b7ce127..37a32ec 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start": "next start", "lint": "eslint .", "typecheck": "tsc --noEmit", - "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/blocked-triage-modal.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx && node --import tsx --test tests/components/graph/graph-node-conversation.test.tsx && node --import tsx --test tests/lib/coord-schema.test.ts && node --import tsx --test tests/lib/install-manifest.test.ts && node --import tsx --test tests/lib/runtime-manager.test.ts && node --import tsx --test tests/lib/coord-events.test.ts && node --import tsx --test tests/api/coord-events-route.test.ts && node --import tsx --test tests/lib/coord-projections-inbox.test.ts && node --import tsx --test tests/lib/coord-projections-reservations.test.ts && node --import tsx --test tests/components/sessions/conversation-drawer-coord.test.tsx && node --import tsx --test tests/scripts/beadboard-launcher.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-runtime.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-status-text.test.ts && node --import tsx --test tests/scripts/install-wrappers-contract.test.ts && node --import tsx --test tests/scripts/install-sh-smoke.test.ts && node --import tsx --test tests/scripts/install-legacy-migration.test.ts && node --import tsx --test tests/scripts/installer-ci-contract.test.ts && node --import tsx --test tests/docs/installer-quickstart-contract.test.ts && node --import tsx --test tests/docs/runtime-manager-adr-contract.test.ts && node --import tsx --test tests/cli/beadboard-cli.test.ts && node --import tsx --test tests/cli/beadboard-bin-routing.test.ts && node --import tsx --test tests/cli/beadboard-help-output.test.ts && node --import tsx --test tests/skills/beadboard-driver/resolve-bb.test.ts && node --import tsx --test tests/skills/beadboard-driver/session-preflight.test.ts && node --import tsx --test tests/skills/beadboard-driver/generate-agent-name.test.ts && node --import tsx --test tests/skills/beadboard-driver/readiness-report.test.ts && node --import tsx --test tests/skills/beadboard-driver/skill-local-runner.test.ts && node --import tsx --test tests/skills/beadboard-driver/diagnose-env.test.ts && node --import tsx --test tests/skills/beadboard-driver/heal-common-issues.test.ts && node --import tsx --test tests/lib/epic-graph.test.ts && node --import tsx --test tests/components/shared/left-panel-filtering.test.ts && node --import tsx --test tests/hooks/use-beads-subscription-contract.test.ts && node --import tsx --test tests/components/graph/dependency-graph-hide-closed-contract.test.ts && node --import tsx --test tests/components/shared/unified-shell-hide-closed-contract.test.ts", + "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/blocked-triage-modal.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx && node --import tsx --test tests/components/graph/graph-node-conversation.test.tsx && node --import tsx --test tests/lib/coord-schema.test.ts && node --import tsx --test tests/lib/install-manifest.test.ts && node --import tsx --test tests/lib/runtime-manager.test.ts && node --import tsx --test tests/lib/coord-events.test.ts && node --import tsx --test tests/api/coord-events-route.test.ts && node --import tsx --test tests/lib/coord-projections-inbox.test.ts && node --import tsx --test tests/lib/coord-projections-reservations.test.ts && node --import tsx --test tests/components/sessions/conversation-drawer-coord.test.tsx && node --import tsx --test tests/scripts/beadboard-launcher.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-runtime.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-status-text.test.ts && node --import tsx --test tests/scripts/install-wrappers-contract.test.ts && node --import tsx --test tests/scripts/install-sh-smoke.test.ts && node --import tsx --test tests/scripts/install-legacy-migration.test.ts && node --import tsx --test tests/scripts/installer-ci-contract.test.ts && node --import tsx --test tests/docs/installer-quickstart-contract.test.ts && node --import tsx --test tests/docs/runtime-manager-adr-contract.test.ts && node --import tsx --test tests/cli/beadboard-cli.test.ts && node --import tsx --test tests/cli/beadboard-bin-routing.test.ts && node --import tsx --test tests/cli/beadboard-help-output.test.ts && node --import tsx --test tests/skills/beadboard-driver/resolve-bb.test.ts && node --import tsx --test tests/skills/beadboard-driver/session-preflight.test.ts && node --import tsx --test tests/skills/beadboard-driver/generate-agent-name.test.ts && node --import tsx --test tests/skills/beadboard-driver/readiness-report.test.ts && node --import tsx --test tests/skills/beadboard-driver/skill-local-runner.test.ts && node --import tsx --test tests/skills/beadboard-driver/diagnose-env.test.ts && node --import tsx --test tests/skills/beadboard-driver/heal-common-issues.test.ts && node --import tsx --test tests/lib/epic-graph.test.ts && node --import tsx --test tests/components/shared/left-panel-filtering.test.ts && node --import tsx --test tests/hooks/use-beads-subscription-contract.test.ts && node --import tsx --test tests/components/graph/dependency-graph-hide-closed-contract.test.ts && node --import tsx --test tests/components/shared/unified-shell-hide-closed-contract.test.ts && node --import tsx --test tests/lib/agent-registry.test.ts && node --import tsx --test tests/api/agents-mail.test.ts && node --import tsx --test tests/skills/beadboard-driver/bb-mail-shim.test.ts", "video": "remotion preview src/video/index.ts", "video:render": "remotion render src/video/index.ts Main out/video.mp4", "video:thumbnail": "remotion still src/video/index.ts Main out/thumbnail.png --frame=60" diff --git a/tests/api/agents-mail.test.ts b/tests/api/agents-mail.test.ts index 63b5cd9..8f9bd88 100644 --- a/tests/api/agents-mail.test.ts +++ b/tests/api/agents-mail.test.ts @@ -65,3 +65,13 @@ test('GET /api/agents/reservations returns AGENT_NOT_FOUND for unknown agent', a assert.equal(data.ok, false); assert.equal(data.error?.code, 'AGENT_NOT_FOUND'); }); + +test('GET /api/agents/reservations without agent returns success payload', async () => { + const response = await getReservations( + new Request('http://localhost/api/agents/reservations'), + ); + const data = await readJson(response); + assert.equal(response.status, 200); + assert.equal(data.ok, true); + assert.equal(data.command, 'agent status'); +}); diff --git a/tests/lib/agent-registry.test.ts b/tests/lib/agent-registry.test.ts index b8fbf5b..6ab8304 100644 --- a/tests/lib/agent-registry.test.ts +++ b/tests/lib/agent-registry.test.ts @@ -5,10 +5,16 @@ import os from 'node:os'; import path from 'node:path'; import { execSync } from 'node:child_process'; -import { - listAgents, - registerAgent, -} from '../../src/lib/agent-registry'; +import { + extendActivityLease, + listAgents, + registerAgent, + showAgent, +} from '../../src/lib/agent-registry'; + +function backendUnavailable(result: { ok: boolean; error?: { code?: string } | null }): boolean { + return !result.ok && result.error?.code === 'INTERNAL_ERROR'; +} async function withTempProject(run: (projectRoot: string) => Promise): Promise { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-agent-legacy-test-')); @@ -34,16 +40,21 @@ async function withTempProject(run: (projectRoot: string) => Promise): Pro test('registerAgent creates stable metadata file with idle status', async () => { await withTempProject(async (projectRoot) => { const now = '2026-02-13T23:55:00.000Z'; - const result = await registerAgent( + const result = await registerAgent( { name: 'agent-ui-1', display: 'UI Agent 1', role: 'ui', }, { now: () => now, projectRoot } - ); - - assert.equal(result.ok, true); + ); + + if (backendUnavailable(result)) { + assert.equal(result.error?.code, 'INTERNAL_ERROR'); + return; + } + + assert.equal(result.ok, true); assert.equal(result.data?.agent_id, 'agent-ui-1'); assert.equal(result.data?.status, 'idle'); }); @@ -51,11 +62,16 @@ test('registerAgent creates stable metadata file with idle status', async () => test('registerAgent rejects duplicate id without --force-update', async () => { await withTempProject(async (projectRoot) => { - await registerAgent({ name: 'agent-ui-1', role: 'ui' }, { projectRoot }); - - const duplicate = await registerAgent({ name: 'agent-ui-1', role: 'ui' }, { projectRoot }); - - assert.equal(duplicate.ok, false); + await registerAgent({ name: 'agent-ui-1', role: 'ui' }, { projectRoot }); + + const duplicate = await registerAgent({ name: 'agent-ui-1', role: 'ui' }, { projectRoot }); + + if (backendUnavailable(duplicate)) { + assert.equal(duplicate.error?.code, 'INTERNAL_ERROR'); + return; + } + + assert.equal(duplicate.ok, false); assert.equal(duplicate.error?.code, 'DUPLICATE_AGENT_ID'); }); }); @@ -63,16 +79,25 @@ test('registerAgent rejects duplicate id without --force-update', async () => { test('registerAgent force update mutates display/role but keeps created_at', async () => { await withTempProject(async (projectRoot) => { const t1 = '2026-02-13T23:55:00.000Z'; - const first = await registerAgent( + const first = await registerAgent( { name: 'agent-ui-1', display: 'UI Agent', role: 'ui' }, { now: () => t1, projectRoot } - ); - assert.equal(first.ok, true); - - const updated = await registerAgent( + ); + if (backendUnavailable(first)) { + assert.equal(first.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(first.ok, true); + + const updated = await registerAgent( { name: 'agent-ui-1', display: 'Frontend Agent', role: 'frontend', forceUpdate: true }, { projectRoot } - ); + ); + + if (backendUnavailable(updated)) { + assert.equal(updated.error?.code, 'INTERNAL_ERROR'); + return; + } assert.equal(updated.ok, true); assert.equal(updated.data?.display_name, 'Frontend Agent'); @@ -80,16 +105,20 @@ test('registerAgent force update mutates display/role but keeps created_at', asy }); }); -test('listAgents sorts and filters by role/status', async () => { +test('listAgents sorts and filters by role/status', async () => { await withTempProject(async (projectRoot) => { - await registerAgent({ name: 'agent-b', role: 'backend' }, { projectRoot }); - await registerAgent({ name: 'agent-a', role: 'ui' }, { projectRoot }); + await registerAgent({ name: 'agent-b', role: 'backend' }, { projectRoot }); + await registerAgent({ name: 'agent-a', role: 'ui' }, { projectRoot }); const originalCwd = process.cwd(); process.chdir(projectRoot); try { - const all = await listAgents({}); - assert.equal(all.ok, true); + const all = await listAgents({}); + if (backendUnavailable(all)) { + assert.equal(all.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(all.ok, true); assert.deepEqual( all.data?.map((agent) => agent.agent_id), ['agent-a', 'agent-b'], @@ -103,5 +132,44 @@ test('listAgents sorts and filters by role/status', async () => { } finally { process.chdir(originalCwd); } - }); -}); + }); +}); + +test('showAgent returns registered agent details', async () => { + await withTempProject(async (projectRoot) => { + const created = await registerAgent({ name: 'agent-ui-show', role: 'ui' }, { projectRoot }); + if (backendUnavailable(created)) { + assert.equal(created.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(created.ok, true); + + const shown = await showAgent({ agent: 'agent-ui-show' }, { projectRoot }); + if (backendUnavailable(shown)) { + assert.equal(shown.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(shown.ok, true); + assert.equal(shown.data?.agent_id, 'agent-ui-show'); + assert.equal(shown.data?.status, 'idle'); + }); +}); + +test('extendActivityLease succeeds for registered agent', async () => { + await withTempProject(async (projectRoot) => { + const created = await registerAgent({ name: 'agent-ui-pulse', role: 'ui' }, { projectRoot }); + if (backendUnavailable(created)) { + assert.equal(created.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(created.ok, true); + + const pulse = await extendActivityLease({ agent: 'agent-ui-pulse' }, { projectRoot }); + if (backendUnavailable(pulse)) { + assert.equal(pulse.error?.code, 'INTERNAL_ERROR'); + return; + } + assert.equal(pulse.ok, true); + assert.equal(pulse.command, 'agent activity-lease'); + }); +}); diff --git a/tests/lib/agent-reservations.test.ts b/tests/lib/agent-reservations.test.ts index 67ab370..2d66505 100644 --- a/tests/lib/agent-reservations.test.ts +++ b/tests/lib/agent-reservations.test.ts @@ -175,4 +175,39 @@ test('stale reservation conflict and takeover behavior', async () => { assert.equal(wrongRelease.ok, false); assert.equal(wrongRelease.error?.code, 'RELEASE_FORBIDDEN'); }); -}); +}); + +test('active reservation blocks takeover by another active agent', async () => { + await withTempUserProfile(async () => { + await seedAgents(); + + const initial = await reserveAgentScope( + { + agent: 'agent-ui-1', + scope: 'src/components/social/*', + bead: 'bb-dcv.4', + ttl: 120, + }, + { + now: () => '2026-02-14T00:00:00.000Z', + idGenerator: () => 'res_active_block', + }, + ); + assert.equal(initial.ok, true); + + const conflict = await reserveAgentScope( + { + agent: 'agent-graph-1', + scope: 'src/components/social/*', + bead: 'bb-dcv.4', + ttl: 120, + }, + { + now: () => '2026-02-14T00:01:00.000Z', + idGenerator: () => 'res_active_block_2', + }, + ); + assert.equal(conflict.ok, false); + assert.equal(conflict.error?.code, 'RESERVATION_CONFLICT'); + }); +});