test(bd-0nd): add directory-source integration test with NEEDLE fixtures

Add e2e test proving DirectoryTailer works with real NEEDLE per-worker
JSONL format: copies anonymized fixtures to temp dir, asserts events from
multiple workers, hot-adds a gamma worker mid-test, all under 1.4s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-22 16:32:11 -04:00
parent 632f35a4c3
commit 398f090a11
3 changed files with 93 additions and 1 deletions

View file

@ -5,9 +5,14 @@
* Uses real log samples from ~/.needle/logs/ to ensure compatibility.
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { parseLogLine, parseLogLines } from './parser.js';
import { LogEvent } from './types.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { fileURLToPath } from 'url';
import { DirectoryTailer } from './directoryTailer.js';
describe('NEEDLE-FABRIC Integration', () => {
describe('worker.started events', () => {
@ -600,3 +605,82 @@ describe('NEEDLE-FABRIC Integration', () => {
});
});
});
describe('FABRIC ↔ NEEDLE directory source integration', () => {
let tempDir: string;
let tailer: DirectoryTailer;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.resolve(__dirname, '../tests/fixtures/needle-logs');
function makeNeedleEvent(worker_id: string, session_id: string, sequence: number, event_type: string, data: Record<string, unknown> = {}) {
return JSON.stringify({
timestamp: new Date().toISOString(),
event_type,
worker_id,
session_id,
sequence,
...('bead_id' in data ? { bead_id: data.bead_id } : {}),
data,
});
}
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-needle-integ-'));
const entries = fs.readdirSync(fixturesDir);
for (const entry of entries) {
if (entry.endsWith('.jsonl')) {
fs.copyFileSync(path.join(fixturesDir, entry), path.join(tempDir, entry));
}
}
});
afterEach(() => {
tailer?.stop();
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('picks up events from multiple worker files and hot-adds a new file', async () => {
const received: Array<{ worker: string; msg: string }> = [];
tailer = new DirectoryTailer({ directory: tempDir });
tailer.on('event', (event: LogEvent) => {
received.push({ worker: event.worker, msg: event.msg });
});
tailer.start();
// Let initial scan settle — tailers position at end of existing files
await new Promise((r) => setTimeout(r, 150));
// Append new events to the pre-existing fixture files
const alphaPath = path.join(tempDir, 'alpha-d6288428.jsonl');
const bravoPath = path.join(tempDir, 'bravo-44c92b93.jsonl');
fs.appendFileSync(alphaPath, makeNeedleEvent('alpha-d6288428', 'session-alpha-001', 10, 'bead.claimed', { bead_id: 'bd-hot1', actor: 'fabric-test' }) + '\n');
fs.appendFileSync(bravoPath, makeNeedleEvent('bravo-44c92b93', 'session-bravo-002', 10, 'bead.claimed', { bead_id: 'bd-hot2', actor: 'fabric-test' }) + '\n');
// Wait for watchers to fire and events to propagate
await new Promise((r) => setTimeout(r, 500));
// Assert: at least 2 distinct worker_id values in received events
const workers = new Set(received.map((e) => e.worker));
expect(workers.size).toBeGreaterThanOrEqual(2);
expect(workers).toContain('alpha-d6288428');
expect(workers).toContain('bravo-44c92b93');
// Mid-test: hot-add a new gamma worker file
const gammaPath = path.join(tempDir, 'gamma-aabb1122.jsonl');
fs.writeFileSync(gammaPath, '');
await new Promise((r) => setTimeout(r, 200));
fs.appendFileSync(gammaPath, makeNeedleEvent('gamma-aabb1122', 'session-gamma-003', 1, 'worker.started', { pid: 30303, workspace: '/home/coder/NEEDLE', agent: 'claude-code-sonnet' }) + '\n');
await new Promise((r) => setTimeout(r, 500));
// Assert: gamma event showed up
const gammaEvents = received.filter((e) => e.worker === 'gamma-aabb1122');
expect(gammaEvents.length).toBeGreaterThanOrEqual(1);
expect(gammaEvents.some((e) => e.msg === 'worker.started')).toBe(true);
tailer.stop();
});
});

View file

@ -0,0 +1,4 @@
{"timestamp":"2026-04-22T10:00:00.000Z","event_type":"worker.started","worker_id":"alpha-d6288428","session_id":"session-alpha-001","sequence":1,"data":{"pid":10101,"workspace":"/home/coder/NEEDLE","agent":"claude-anthropic-sonnet"}}
{"timestamp":"2026-04-22T10:00:05.000Z","event_type":"bead.claimed","worker_id":"alpha-d6288428","session_id":"session-alpha-001","sequence":2,"bead_id":"bd-a1b2","data":{"actor":"fabric-test","attempt":1}}
{"timestamp":"2026-04-22T10:00:10.000Z","event_type":"bead.completed","worker_id":"alpha-d6288428","session_id":"session-alpha-001","sequence":3,"bead_id":"bd-a1b2","data":{"duration_ms":5000,"output_file":"/tmp/needle-bd-a1b2.log"}}
{"timestamp":"2026-04-22T10:00:11.000Z","event_type":"worker.idle","worker_id":"alpha-d6288428","session_id":"session-alpha-001","sequence":4,"data":{"consecutive_empty":1,"idle_seconds":0}}

View file

@ -0,0 +1,4 @@
{"timestamp":"2026-04-22T10:00:01.000Z","event_type":"worker.started","worker_id":"bravo-44c92b93","session_id":"session-bravo-002","sequence":1,"data":{"pid":20202,"workspace":"/home/coder/NEEDLE","agent":"claude-anthropic-opus"}}
{"timestamp":"2026-04-22T10:00:06.000Z","event_type":"bead.claimed","worker_id":"bravo-44c92b93","session_id":"session-bravo-002","sequence":2,"bead_id":"bd-c3d4","data":{"actor":"fabric-test","attempt":1}}
{"timestamp":"2026-04-22T10:00:12.000Z","event_type":"bead.completed","worker_id":"bravo-44c92b93","session_id":"session-bravo-002","sequence":3,"bead_id":"bd-c3d4","data":{"duration_ms":6000,"output_file":"/tmp/needle-bd-c3d4.log"}}
{"timestamp":"2026-04-22T10:00:13.000Z","event_type":"effort.recorded","worker_id":"bravo-44c92b93","session_id":"session-bravo-002","sequence":4,"bead_id":"bd-c3d4","data":{"duration_ms":6000}}