import { tap, expect } from '@git.zone/tstest/tapbundle'; import { RustWatcher } from '../ts/watchers/watcher.rust.js'; import type { IWatchEvent } from '../ts/watchers/interfaces.js'; import * as path from 'path'; import * as fs from 'fs'; const TEST_DIR = path.resolve('./test/assets'); const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); function waitForEvent( watcher: { events$: { subscribe: Function } }, filter: (e: IWatchEvent) => boolean, timeoutMs = 5000 ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { sub.unsubscribe(); reject(new Error(`Timeout waiting for event after ${timeoutMs}ms`)); }, timeoutMs); const sub = watcher.events$.subscribe((event: IWatchEvent) => { if (filter(event)) { clearTimeout(timeout); sub.unsubscribe(); resolve(event); } }); }); } function collectEvents( watcher: { events$: { subscribe: Function } }, filter: (e: IWatchEvent) => boolean, durationMs: number ): Promise { return new Promise((resolve) => { const events: IWatchEvent[] = []; const sub = watcher.events$.subscribe((event: IWatchEvent) => { if (filter(event)) events.push(event); }); setTimeout(() => { sub.unsubscribe(); resolve(events); }, durationMs); }); } let available = false; tap.test('RustWatcher (Node): check availability', async () => { available = await RustWatcher.isAvailable(); console.log(`[test] Rust binary available: ${available}`); if (!available) { console.log('[test] Skipping Rust watcher tests — binary not found'); } }); let watcher: RustWatcher; tap.test('RustWatcher (Node): should create and start', async () => { if (!available) return; watcher = new RustWatcher({ basePaths: [TEST_DIR], depth: 4, followSymlinks: false, debounceMs: 100, }); expect(watcher.isWatching).toBeFalse(); await watcher.start(); expect(watcher.isWatching).toBeTrue(); await delay(300); }); tap.test('RustWatcher (Node): should emit initial add events', async () => { if (!available) return; // The initial scan should have emitted add events for existing files. // We verify by creating a file and checking it gets an add event const file = path.join(TEST_DIR, 'rust-node-add.txt'); const eventPromise = waitForEvent(watcher, (e) => e.type === 'add' && e.path.includes('rust-node-add.txt')); await fs.promises.writeFile(file, 'rust node add test'); const event = await eventPromise; expect(event.type).toEqual('add'); expect(event.path).toInclude('rust-node-add.txt'); await fs.promises.unlink(file); await delay(200); }); tap.test('RustWatcher (Node): should detect file modification', async () => { if (!available) return; const file = path.join(TEST_DIR, 'rust-node-change.txt'); await fs.promises.writeFile(file, 'initial'); await delay(300); const eventPromise = waitForEvent(watcher, (e) => e.type === 'change' && e.path.includes('rust-node-change.txt')); await fs.promises.writeFile(file, 'modified'); const event = await eventPromise; expect(event.type).toEqual('change'); await fs.promises.unlink(file); await delay(200); }); tap.test('RustWatcher (Node): should detect file deletion', async () => { if (!available) return; const file = path.join(TEST_DIR, 'rust-node-unlink.txt'); await fs.promises.writeFile(file, 'to delete'); await delay(300); const eventPromise = waitForEvent(watcher, (e) => e.type === 'unlink' && e.path.includes('rust-node-unlink.txt')); await fs.promises.unlink(file); const event = await eventPromise; expect(event.type).toEqual('unlink'); }); tap.test('RustWatcher (Node): should detect directory creation', async () => { if (!available) return; const dir = path.join(TEST_DIR, 'rust-node-subdir'); const eventPromise = waitForEvent(watcher, (e) => e.type === 'addDir' && e.path.includes('rust-node-subdir')); await fs.promises.mkdir(dir, { recursive: true }); const event = await eventPromise; expect(event.type).toEqual('addDir'); await delay(200); await fs.promises.rmdir(dir); await delay(200); }); tap.test('RustWatcher (Node): should handle rapid modifications', async () => { if (!available) return; const file = path.join(TEST_DIR, 'rust-node-rapid.txt'); await fs.promises.writeFile(file, 'initial'); await delay(200); const collector = collectEvents(watcher, (e) => e.type === 'change' && e.path.includes('rust-node-rapid.txt'), 3000); for (let i = 0; i < 10; i++) { await fs.promises.writeFile(file, `content ${i}`); await delay(10); } const events = await collector; console.log(`[test] Rapid mods: 10 writes, ${events.length} events received`); expect(events.length).toBeGreaterThan(0); await fs.promises.unlink(file); await delay(200); }); tap.test('RustWatcher (Node): should not be watching after stop', async () => { if (!available) return; await watcher.stop(); expect(watcher.isWatching).toBeFalse(); }); tap.test('RustWatcher (Node): cleanup', async () => { for (const name of ['rust-node-add.txt', 'rust-node-change.txt', 'rust-node-unlink.txt', 'rust-node-rapid.txt']) { try { await fs.promises.unlink(path.join(TEST_DIR, name)); } catch {} } try { await fs.promises.rmdir(path.join(TEST_DIR, 'rust-node-subdir')); } catch {} }); export default tap.start();