checkpoint: pre-split branch cleanup
This commit is contained in:
parent
4c2ae2e5b7
commit
b5db7a7753
276 changed files with 35912 additions and 60119 deletions
|
|
@ -1,205 +1,205 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Create issue via bd to trigger a valid mutation
|
||||
execSync('bd create "Task watch" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
assert.equal(found, true, 'Expected event from file change');
|
||||
});
|
||||
|
||||
test('IssuesWatchManager emits telemetry event after beads.db change (not issues)', 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 });
|
||||
|
||||
// Initialize bd to create valid db
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
const bus = new IssuesEventBus();
|
||||
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 40 });
|
||||
|
||||
const events: Array<{ kind: string; changedPath?: string }> = [];
|
||||
const stop = bus.subscribe((event) => {
|
||||
events.push({ kind: event.kind, changedPath: event.changedPath });
|
||||
});
|
||||
|
||||
await manager.startWatch(root);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Touch beads.db directly without mutating issues to simulate a connection write/telemetry pulse
|
||||
await fs.appendFile(dbPath, ' ', 'utf8');
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
// REGRESSION: beads.db should emit 'telemetry', not 'issues'
|
||||
// This prevents the "typing interrupt" refresh loop during agent heartbeats
|
||||
assert.equal(events.length >= 1, true, 'Expected at least one event');
|
||||
const dbEvents = events.filter(e => e.changedPath?.includes('beads.db'));
|
||||
assert.ok(dbEvents.length > 0, 'Expected beads.db change event');
|
||||
for (const event of dbEvents) {
|
||||
assert.equal(event.kind, 'telemetry', `beads.db change should emit 'telemetry', got '${event.kind}'. This prevents refresh loops during agent heartbeats.`);
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Initial state: 1 issue via bd
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Modify issue via bd: status change. This updates beads.db-wal
|
||||
execSync('bd update bb-1 --status in_progress', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
assert.equal(found, true, 'Expected event from db-wal change');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Initial state: 1 issue via bd
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
execSync('bd update bb-1 --status open', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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 via bd: status change
|
||||
execSync('bd update bb-1 --status in_progress', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Wait for debounce + processing with retry loop
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (activities.includes('status_changed:bb-1')) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
// Expect status_changed for bb-1
|
||||
if (!found) {
|
||||
console.error('WATCHER FAIL. Activities found:', JSON.stringify(activities, null, 2));
|
||||
}
|
||||
assert.ok(found, `Expected status_changed event. Got: ${activities.join(', ')}`);
|
||||
});
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Create issue via bd to trigger a valid mutation
|
||||
execSync('bd create "Task watch" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
assert.equal(found, true, 'Expected event from file change');
|
||||
});
|
||||
|
||||
test('IssuesWatchManager emits telemetry event after beads.db change (not issues)', 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 });
|
||||
|
||||
// Initialize bd to create valid db
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
const bus = new IssuesEventBus();
|
||||
const manager = new IssuesWatchManager({ eventBus: bus, debounceMs: 40 });
|
||||
|
||||
const events: Array<{ kind: string; changedPath?: string }> = [];
|
||||
const stop = bus.subscribe((event) => {
|
||||
events.push({ kind: event.kind, changedPath: event.changedPath });
|
||||
});
|
||||
|
||||
await manager.startWatch(root);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Touch beads.db directly without mutating issues to simulate a connection write/telemetry pulse
|
||||
await fs.appendFile(dbPath, ' ', 'utf8');
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
// REGRESSION: beads.db should emit 'telemetry', not 'issues'
|
||||
// This prevents the "typing interrupt" refresh loop during agent heartbeats
|
||||
assert.equal(events.length >= 1, true, 'Expected at least one event');
|
||||
const dbEvents = events.filter(e => e.changedPath?.includes('beads.db'));
|
||||
assert.ok(dbEvents.length > 0, 'Expected beads.db change event');
|
||||
for (const event of dbEvents) {
|
||||
assert.equal(event.kind, 'telemetry', `beads.db change should emit 'telemetry', got '${event.kind}'. This prevents refresh loops during agent heartbeats.`);
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Initial state: 1 issue via bd
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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);
|
||||
|
||||
// Wait for initial read to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Modify issue via bd: status change. This updates beads.db-wal
|
||||
execSync('bd update bb-1 --status in_progress', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (events.length >= 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
assert.equal(found, true, 'Expected event from db-wal change');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
|
||||
// Initialize bd in temp dir
|
||||
execSync('bd init --prefix bb --force', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Initial state: 1 issue via bd
|
||||
execSync('bd create "Task A" --id bb-1', { cwd: root, stdio: 'ignore' });
|
||||
execSync('bd update bb-1 --status open', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
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 via bd: status change
|
||||
execSync('bd update bb-1 --status in_progress', { cwd: root, stdio: 'ignore' });
|
||||
|
||||
// Wait for debounce + processing with retry loop
|
||||
let found = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (activities.includes('status_changed:bb-1')) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop();
|
||||
await manager.stopAll();
|
||||
|
||||
// Expect status_changed for bb-1
|
||||
if (!found) {
|
||||
console.error('WATCHER FAIL. Activities found:', JSON.stringify(activities, null, 2));
|
||||
}
|
||||
assert.ok(found, `Expected status_changed event. Got: ${activities.join(', ')}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue