From 7fa822d3eaf74650d9f2b8560e7efb80b592d1a6 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 26 May 2026 21:39:00 -0400 Subject: [PATCH] fix(test): restructure DirectoryTailer re-activation test The test 'resumes from saved position when a file is re-activated after eviction' was incorrectly creating both files before starting the tailer. With maxActiveFiles: 1, only the newer file (fileB) was being activated initially, so fileA never emitted its 'initial' event. Restructured to: 1. Create fileA with content 2. Start tailer (fileA gets activated) 3. Wait for fileA to emit 'initial' 4. Create fileB (triggers eviction of fileA via dirWatcher) 5. Continue with re-activation test This properly tests the LRU eviction and position checkpointing behavior. --- src/directoryTailer.test.ts | 61 +++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/directoryTailer.test.ts b/src/directoryTailer.test.ts index 569ea6a..773fa8c 100644 --- a/src/directoryTailer.test.ts +++ b/src/directoryTailer.test.ts @@ -273,38 +273,59 @@ describe('DirectoryTailer', () => { const fileA = path.join(tempDir, 'a.jsonl'); const fileB = path.join(tempDir, 'b.jsonl'); - fs.writeFileSync(fileA, makeEvent('w-a', 'before-eviction', 1) + '\n'); - fs.writeFileSync(fileB, ''); + // Create fileA with initial content - this will be activated first. + fs.writeFileSync(fileA, makeEvent('w-a', 'initial', 1) + '\n'); - // maxActiveFiles=1 so opening fileB will evict fileA. const tailer = new DirectoryTailer({ directory: tempDir, maxActiveFiles: 1, recentMtimeMs: 86_400_000, - inactiveCheckIntervalMs: 200, + inactiveCheckIntervalMs: 100, }); - const received: string[] = []; - tailer.on('event', (event) => received.push(event.msg)); + const received: Array<{ msg: string; filePath: string }> = []; + tailer.on('event', (event, filePath) => { + received.push({ msg: event.msg, filePath }); + }); + // Start with only fileA present - it will be activated. tailer.start(); + + // Wait for fileA to be read and emit 'initial'. + await new Promise((r) => setTimeout(r, 100)); + + // Verify fileA is active and emitted 'initial'. + expect(tailer.activeFiles.length).toBe(1); + expect(tailer.activeFiles[0]).toBe(fileA); + expect(received.filter((r) => r.msg === 'initial').length).toBe(1); + + // Create fileB empty - when detected, it will evict fileA via activateWithEviction. + fs.writeFileSync(fileB, ''); + + // Wait for dirWatcher to detect fileB and evict fileA. + await new Promise((r) => setTimeout(r, 200)); + + // Exactly one file should be active (fileB, the newer one). + expect(tailer.activeFiles.length).toBe(1); + expect(tailer.activeFiles[0]).toBe(fileB); + + // fileA should have been read and emitted 'initial' before being evicted. + const initialCount = received.filter((r) => r.msg === 'initial').length; + expect(initialCount).toBe(1); + + // Write to fileA (the inactive/evicted file) to trigger re-activation. + // This new content should be read from the checkpointed position. + fs.appendFileSync(fileA, makeEvent('w-a', 'after-eviction', 2) + '\n'); + + // Wait for the poll cycle to detect mtime change and re-activate fileA. await new Promise((r) => setTimeout(r, 400)); - // Exactly one file is active; the other is inactive. - expect(tailer.activeFiles.length).toBe(1); - - // Write to the inactive file to trigger re-activation. - const inactive = tailer.activeFiles[0] === fileA ? fileB : fileA; - fs.appendFileSync(inactive, makeEvent('w-inactive', 'after-reactivation', 2) + '\n'); - - await new Promise((r) => setTimeout(r, 800)); tailer.stop(); - // The event written after re-activation must have been received. - expect(received).toContain('after-reactivation'); - // The event written before eviction (to fileA at start time) should NOT - // have been re-emitted when fileA was re-activated (position is checkpointed). - const beforeCount = received.filter((m) => m === 'before-eviction').length; - expect(beforeCount).toBe(0); + // The new event should be received. + expect(received.some((r) => r.msg === 'after-eviction')).toBe(true); + // 'initial' should only have been emitted once (during initial activation), + // not again when fileA was re-activated. + expect(received.filter((r) => r.msg === 'initial').length).toBe(1); }); });