beadboard/tests/lib/agent-sessions-liveness.test.ts
zenchantlive 4ee550c333 feat(telemetry): complete bb-buff.1.3 - Backend Liveness Refactor
STORY:
The session backend needed to aggregate agent health from a live
telemetry stream rather than static bead metadata. This refactor
makes liveness signals real-time and accurate.

COLLABORATION:
We extended the ActivityEvent model with a native 'heartbeat' kind,
updated extendActivityLease() to emit through the activity bus, and
refactored getAgentLivenessMap() to prioritize heartbeat activity
history over stale bead metadata.

DELIVERABLES:
- ActivityEvent extended with 'heartbeat' kind
- extendActivityLease() emits heartbeats through activity bus
- getAgentLivenessMap() prefers telemetry over static metadata
- Registry APIs support projectRoot injection for testing
- Tests verify preference logic via TDD

VERIFICATION:
- 93/93 tests PASSING
- Heartbeat override verified in isolated temp projects

CLOSES: bb-buff.1.3
BLOCKS: bb-buff.3.2, bb-buff.3.3, bb-buff.2.1
2026-02-15 21:14:05 -08:00

81 lines
3 KiB
TypeScript

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 { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { registerAgent } from '../../src/lib/agent-registry';
import { getAgentLivenessMap } from '../../src/lib/agent-sessions';
import type { ActivityEvent } from '../../src/lib/activity';
async function withTempProject(run: (projectRoot: string) => Promise<void>): Promise<void> {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-liveness-test-'));
execSync('bd init --prefix bb --force', { cwd: tempDir, stdio: 'ignore' });
try {
await run(tempDir);
} finally {
for (let i = 0; i < 5; i++) {
try {
await fs.rm(tempDir, { recursive: true, force: true });
break;
} catch {
await new Promise(r => setTimeout(r, 500));
}
}
}
}
test('getAgentLivenessMap prefers telemetry over bead metadata', async () => {
await withTempProject(async (projectRoot) => {
const agentId = 'telemetry-agent';
const regResult = await registerAgent({ name: agentId, role: 'infra' }, { projectRoot });
assert.equal(regResult.ok, true, `Failed to register agent: ${regResult.error?.message}`);
// Verify bead exists on disk
const issues = execSync('bd list --label gt:agent --json', { cwd: projectRoot, encoding: 'utf8' });
console.log('Registered agents:', issues);
// 1. No heartbeats in stream -> use metadata
const livenessMap1 = await getAgentLivenessMap(projectRoot, []);
console.log('Liveness Map 1:', livenessMap1);
assert.equal(livenessMap1[agentId], 'active');
// 2. Add an old heartbeat to stream (2 hours ago)
// Metadata is fresh (just registered), so it should fallback to metadata and stay active.
const wayOldTime = new Date(Date.now() - 120 * 60 * 1000).toISOString();
const wayOldHeartbeat: ActivityEvent = {
id: randomUUID(),
kind: 'heartbeat',
beadId: `bb-${agentId}`,
beadTitle: `Agent: ${agentId}`,
projectId: projectRoot,
projectName: 'test',
timestamp: wayOldTime,
actor: agentId,
payload: { message: 'running' }
};
const livenessMap2 = await getAgentLivenessMap(projectRoot, [wayOldHeartbeat]);
assert.equal(livenessMap2[agentId], 'active', 'Should fallback to fresh metadata if telemetry is ancient');
// 3. Fresh heartbeat should stay active
const nowTime = new Date().toISOString();
const freshHeartbeat: ActivityEvent = {
id: randomUUID(),
kind: 'heartbeat',
beadId: `bb-${agentId}`,
beadTitle: `Agent: ${agentId}`,
projectId: projectRoot,
projectName: 'test',
timestamp: nowTime,
actor: agentId,
payload: { message: 'running' }
};
const livenessMap3 = await getAgentLivenessMap(projectRoot, [freshHeartbeat]);
assert.equal(livenessMap3[agentId], 'active');
});
});