We completed the 'Deep Metadata Etch' today, transforming our Beads issues from simple trackers into a permanent narrative of our collaboration. Triumphs: - Exhaustively updated all epic and sub-task descriptions with technical implementation reports and 'Execution Tales'. - Finalized the 'bb' agent CLI skill (bb.ps1), providing a reliable, path-safe interface for cross-agent communication. - Published ADR-001 and RFC-001 to document our coordination protocols. - Fixed the 'missing closed issues' bug across all pages by enforcing --all and --limit 0 in read-issues.ts. Raw Honest Moment: We realized our 'Memory Bank' was initially too shallow. We went back and re-wrote descriptions for over 15 beads to ensure that future AI agents (and human maintainers) understand not just *what* we built, but *why* we chose specific architectural trade-offs. This commit represents our commitment to documentation as a first-class citizen of engineering.
141 lines
4.6 KiB
TypeScript
141 lines
4.6 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 { IssuesEventBus, ActivityEventBus } from '../../src/lib/realtime';
|
|
import { IssuesWatchManager } from '../../src/lib/watcher';
|
|
|
|
test('IssuesWatchManager startWatch is idempotent per project', async () => {
|
|
const bus = new IssuesEventBus();
|
|
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 20 });
|
|
|
|
await manager.startWatch('C:/Repo/One');
|
|
await manager.startWatch('c:\\repo\\one');
|
|
|
|
assert.equal(manager.getWatchedProjectCount(), 1);
|
|
await manager.stopAll();
|
|
});
|
|
|
|
test('IssuesWatchManager emits event after file change in watched .beads path', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-watch-'));
|
|
const beadsDir = path.join(root, '.beads');
|
|
const issuesPath = path.join(beadsDir, 'issues.jsonl');
|
|
await fs.mkdir(beadsDir, { recursive: true });
|
|
await fs.writeFile(issuesPath, '', 'utf8');
|
|
|
|
const bus = new IssuesEventBus();
|
|
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 40 });
|
|
|
|
const events: string[] = [];
|
|
const stop = bus.subscribe((event) => {
|
|
events.push(event.projectRoot);
|
|
});
|
|
|
|
await manager.startWatch(root);
|
|
|
|
await fs.writeFile(issuesPath, `${JSON.stringify({ id: 'bb-1', title: 'watch' })}\n`, 'utf8');
|
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
|
|
stop();
|
|
await manager.stopAll();
|
|
|
|
assert.equal(events.length >= 1, true);
|
|
});
|
|
|
|
test('IssuesWatchManager emits event after beads.db change', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-watch-db-'));
|
|
const beadsDir = path.join(root, '.beads');
|
|
const dbPath = path.join(beadsDir, 'beads.db');
|
|
await fs.mkdir(beadsDir, { recursive: true });
|
|
await fs.writeFile(dbPath, 'seed', 'utf8');
|
|
|
|
const bus = new IssuesEventBus();
|
|
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 40 });
|
|
|
|
const events: string[] = [];
|
|
const stop = bus.subscribe((event) => {
|
|
events.push(event.projectRoot);
|
|
});
|
|
|
|
await manager.startWatch(root);
|
|
|
|
await fs.writeFile(dbPath, `seed-${Date.now()}`, 'utf8');
|
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
|
|
stop();
|
|
await manager.stopAll();
|
|
|
|
assert.equal(events.length >= 1, true);
|
|
});
|
|
|
|
test('IssuesWatchManager emits event after beads.db-wal change', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-watch-wal-'));
|
|
const beadsDir = path.join(root, '.beads');
|
|
const walPath = path.join(beadsDir, 'beads.db-wal');
|
|
await fs.mkdir(beadsDir, { recursive: true });
|
|
await fs.writeFile(walPath, 'seed', 'utf8');
|
|
|
|
const bus = new IssuesEventBus();
|
|
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 40 });
|
|
|
|
const events: string[] = [];
|
|
const stop = bus.subscribe((event) => {
|
|
events.push(event.projectRoot);
|
|
});
|
|
|
|
await manager.startWatch(root);
|
|
|
|
await fs.writeFile(walPath, `seed-${Date.now()}`, 'utf8');
|
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
|
|
stop();
|
|
await manager.stopAll();
|
|
|
|
assert.equal(events.length >= 1, true);
|
|
});
|
|
|
|
test('IssuesWatchManager emits ActivityEvent on issue change', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-watch-activity-'));
|
|
const beadsDir = path.join(root, '.beads');
|
|
const issuesPath = path.join(beadsDir, 'issues.jsonl');
|
|
|
|
await fs.mkdir(beadsDir, { recursive: true });
|
|
|
|
// Initial state: 1 issue
|
|
const issuev1 = { id: 'bb-1', title: 'Task A', status: 'open' };
|
|
await fs.writeFile(issuesPath, JSON.stringify(issuev1) + '\n', 'utf8');
|
|
|
|
const issuesBus = new IssuesEventBus();
|
|
const activityBus = new ActivityEventBus();
|
|
const manager = new IssuesWatchManager({
|
|
eventBus: issuesBus,
|
|
activityBus,
|
|
debounceMs: 50
|
|
});
|
|
|
|
const activities: string[] = [];
|
|
const stop = activityBus.subscribe((e) => {
|
|
activities.push(`${e.event.kind}:${e.event.beadId}`);
|
|
});
|
|
|
|
// Start watching (should load initial snapshot silently)
|
|
await manager.startWatch(root);
|
|
|
|
// Wait for initial read to settle
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
// Modify issue: status change
|
|
const issuev2 = { ...issuev1, status: 'in_progress' };
|
|
await fs.writeFile(issuesPath, JSON.stringify(issuev2) + '\n', 'utf8');
|
|
|
|
// Wait for debounce + processing
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
|
|
stop();
|
|
await manager.stopAll();
|
|
|
|
// Expect status_changed for bb-1
|
|
assert.ok(activities.includes('status_changed:bb-1'), `Expected status_changed event. Got: ${activities.join(', ')}`);
|
|
});
|