Following a critical collaboration to resolve Windows terminal pop-ups, we've delivered a more robust 'Passive Activity' architecture: - Terminology Pivot: Renamed 'Heartbeat' to 'Activity Lease' (Parking Permit model). - Side-Effect Extension: tools/bb.ts now automatically extends the agent's lease whenever they perform real work (any CLI command). - Passive Handshake: bb-init.mjs now only performs an initial registration/lease start, with no background loops. - 100% Silence: Removed all background process spawning, ensuring zero terminal disruption on Windows. - High Observability: Liveness is still tracked via the 15m threshold, but relies on activity rather than periodic pings. OPERATIVE: silver-castle SESSION: 2026-02-14-1330
99 lines
2.7 KiB
TypeScript
99 lines
2.7 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 {
|
|
registerAgent,
|
|
extendActivityLease,
|
|
deriveLiveness,
|
|
agentFilePath,
|
|
} from '../../src/lib/agent-registry';
|
|
|
|
async function withTempUserProfile(run: () => Promise<void>): Promise<void> {
|
|
const previous = process.env.USERPROFILE;
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-agent-liveness-'));
|
|
process.env.USERPROFILE = tempDir;
|
|
|
|
try {
|
|
await run();
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.USERPROFILE;
|
|
} else {
|
|
process.env.USERPROFILE = previous;
|
|
}
|
|
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test('extendActivityLease updates last_seen_at and increments version', async () => {
|
|
await withTempUserProfile(async () => {
|
|
const start = '2026-02-14T10:00:00.000Z';
|
|
const next = '2026-02-14T10:05:00.000Z';
|
|
|
|
await registerAgent(
|
|
{ name: 'active-agent', role: 'infra' },
|
|
{ now: () => start }
|
|
);
|
|
|
|
const result = await extendActivityLease(
|
|
{ agent: 'active-agent' },
|
|
{ now: () => next }
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.data?.last_seen_at, next);
|
|
assert.equal(result.data?.version, 2);
|
|
|
|
const raw = await fs.readFile(agentFilePath('active-agent'), 'utf8');
|
|
const parsed = JSON.parse(raw);
|
|
assert.equal(parsed.last_seen_at, next);
|
|
});
|
|
});
|
|
|
|
test('deriveLiveness follows threshold rules (15m/30m default)', () => {
|
|
const now = new Date('2026-02-14T12:00:00Z');
|
|
|
|
// Active: 14 mins ago
|
|
assert.equal(
|
|
deriveLiveness('2026-02-14T11:46:00Z', now),
|
|
'active'
|
|
);
|
|
|
|
// Stale: Exactly 15 mins ago
|
|
assert.equal(
|
|
deriveLiveness('2026-02-14T11:45:00Z', now),
|
|
'stale'
|
|
);
|
|
|
|
// Stale: 29 mins ago
|
|
assert.equal(
|
|
deriveLiveness('2026-02-14T11:31:00Z', now),
|
|
'stale'
|
|
);
|
|
|
|
// Evicted: Exactly 30 mins ago
|
|
assert.equal(
|
|
deriveLiveness('2026-02-14T11:30:00Z', now),
|
|
'evicted'
|
|
);
|
|
|
|
// Evicted: 1 hour ago
|
|
assert.equal(
|
|
deriveLiveness('2026-02-14T11:00:00Z', now),
|
|
'evicted'
|
|
);
|
|
});
|
|
|
|
test('deriveLiveness respects custom staleMinutes', () => {
|
|
const now = new Date('2026-02-14T12:00:00Z');
|
|
const customThreshold = 5; // 5m stale, 10m evicted
|
|
|
|
assert.equal(deriveLiveness('2026-02-14T11:56:00Z', now, customThreshold), 'active');
|
|
assert.equal(deriveLiveness('2026-02-14T11:55:00Z', now, customThreshold), 'stale');
|
|
assert.equal(deriveLiveness('2026-02-14T11:51:00Z', now, customThreshold), 'stale');
|
|
assert.equal(deriveLiveness('2026-02-14T11:50:00Z', now, customThreshold), 'evicted');
|
|
});
|