diff --git a/src/needleFabric.integration.test.ts b/src/needleFabric.integration.test.ts index 1796f9c..3e5183c 100644 --- a/src/needleFabric.integration.test.ts +++ b/src/needleFabric.integration.test.ts @@ -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 = {}) { + 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(); + }); +}); diff --git a/tests/fixtures/needle-logs/alpha-d6288428.jsonl b/tests/fixtures/needle-logs/alpha-d6288428.jsonl new file mode 100644 index 0000000..a37f1fe --- /dev/null +++ b/tests/fixtures/needle-logs/alpha-d6288428.jsonl @@ -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}} diff --git a/tests/fixtures/needle-logs/bravo-44c92b93.jsonl b/tests/fixtures/needle-logs/bravo-44c92b93.jsonl new file mode 100644 index 0000000..d6e2ff2 --- /dev/null +++ b/tests/fixtures/needle-logs/bravo-44c92b93.jsonl @@ -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}}