import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as smartwatch from '../ts/index.js'; import * as smartfile from '@push.rocks/smartfile'; import * as smartrx from '@push.rocks/smartrx'; import * as fs from 'fs'; import * as path from 'path'; // Skip in CI if (process.env.CI) { process.exit(0); } const TEST_DIR = './test/assets'; // Helper to delay const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // Helper to collect events function collectEvents( observable: smartrx.rxjs.Observable, durationMs: number ): Promise { return new Promise((resolve) => { const events: T[] = []; const subscription = observable.subscribe((value) => { events.push(value); }); setTimeout(() => { subscription.unsubscribe(); resolve(events); }, durationMs); }); } let testSmartwatch: smartwatch.Smartwatch; // =========================================== // STRESS TESTS // =========================================== tap.test('setup: start watcher', async () => { testSmartwatch = new smartwatch.Smartwatch([`${TEST_DIR}/**/*.txt`]); await testSmartwatch.start(); expect(testSmartwatch.status).toEqual('watching'); // Wait for chokidar to be ready await delay(500); }); tap.test('STRESS: rapid file modifications', async () => { const testFile = path.join(TEST_DIR, 'stress-rapid.txt'); // Create initial file await smartfile.memory.toFs('initial', testFile); await delay(200); const changeObservable = await testSmartwatch.getObservableFor('change'); // Rapidly modify the file 20 times const RAPID_CHANGES = 20; const eventCollector = collectEvents(changeObservable, 3000); for (let i = 0; i < RAPID_CHANGES; i++) { await smartfile.memory.toFs(`content ${i}`, testFile); await delay(10); // 10ms between writes } const events = await eventCollector; console.log(`[test] Rapid changes: sent ${RAPID_CHANGES}, received ${events.length} events`); // Due to debouncing, we won't get all events, but we should get at least some expect(events.length).toBeGreaterThan(0); // Cleanup await fs.promises.unlink(testFile); }); tap.test('STRESS: many files created rapidly', async () => { const FILE_COUNT = 20; const files: string[] = []; const addObservable = await testSmartwatch.getObservableFor('add'); const eventCollector = collectEvents(addObservable, 5000); // Create many files rapidly for (let i = 0; i < FILE_COUNT; i++) { const file = path.join(TEST_DIR, `stress-many-${i}.txt`); files.push(file); await smartfile.memory.toFs(`content ${i}`, file); await delay(20); // 20ms between creates } const events = await eventCollector; console.log(`[test] Many files: created ${FILE_COUNT}, detected ${events.length} events`); // Should detect most or all files expect(events.length).toBeGreaterThanOrEqual(FILE_COUNT * 0.8); // Allow 20% tolerance // Cleanup all files for (const file of files) { try { await fs.promises.unlink(file); } catch { // Ignore if already deleted } } }); tap.test('STRESS: interleaved add/change/delete operations', async () => { const testFiles = [ path.join(TEST_DIR, 'stress-interleave-1.txt'), path.join(TEST_DIR, 'stress-interleave-2.txt'), path.join(TEST_DIR, 'stress-interleave-3.txt'), ]; // Create initial files for (const file of testFiles) { await smartfile.memory.toFs('initial', file); } await delay(300); const addObservable = await testSmartwatch.getObservableFor('add'); const changeObservable = await testSmartwatch.getObservableFor('change'); const unlinkObservable = await testSmartwatch.getObservableFor('unlink'); const addEvents = collectEvents(addObservable, 3000); const changeEvents = collectEvents(changeObservable, 3000); const unlinkEvents = collectEvents(unlinkObservable, 3000); // Interleaved operations await smartfile.memory.toFs('changed 1', testFiles[0]); // change await delay(50); await fs.promises.unlink(testFiles[1]); // delete await delay(50); await smartfile.memory.toFs('recreated 1', testFiles[1]); // add (recreate) await delay(50); await smartfile.memory.toFs('changed 2', testFiles[2]); // change await delay(50); const [adds, changes, unlinks] = await Promise.all([addEvents, changeEvents, unlinkEvents]); console.log(`[test] Interleaved: adds=${adds.length}, changes=${changes.length}, unlinks=${unlinks.length}`); // Should have detected some events of each type expect(changes.length).toBeGreaterThan(0); // Cleanup for (const file of testFiles) { try { await fs.promises.unlink(file); } catch { // Ignore } } }); tap.test('teardown: stop watcher', async () => { await testSmartwatch.stop(); expect(testSmartwatch.status).toEqual('idle'); }); tap.test('cleanup: remove stress test files', async () => { const files = await fs.promises.readdir(TEST_DIR); for (const file of files) { if (file.startsWith('stress-')) { try { await fs.promises.unlink(path.join(TEST_DIR, file)); } catch { // Ignore } } } }); export default tap.start();