moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
/**
* Skills 2.0 Tier 3b — fs.watch-based SKILL.md watcher.
*
* fs.watch semantics are OS-dependent (inotify/FSEvents/ReadDirectoryChangesW)
* so tests verify the public contract rather than exact event counts:
* - callback fires for .md changes
* - non-.md files are ignored
* - callback is debounced (single burst → one call)
* - close() is idempotent and silences further events
* - unsupported filesystems don't crash
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { watchSkillDirectory } from '../src/skill-watcher.js';
function sleep(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
describe('watchSkillDirectory — Tier 3b', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-skill-watch-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('creates the directory if it does not exist', () => {
const newDir = path.join(tmpDir, 'not-yet-created');
expect(fs.existsSync(newDir)).toBe(false);
const handle = watchSkillDirectory(newDir, { onChange: () => {} });
expect(fs.existsSync(newDir)).toBe(true);
handle.close();
});
it('fires onChange when a .md file is written', async () => {
const events: string[][] = [];
const handle = watchSkillDirectory(tmpDir, {
onChange: (changed) => events.push(changed),
debounceMs: 50,
});
fs.writeFileSync(path.join(tmpDir, 'fresh.md'), 'body');
await sleep(200); // let the debounced callback fire
expect(events.length).toBeGreaterThanOrEqual(1);
const allChanged = events.flat();
expect(allChanged).toContain('fresh.md');
handle.close();
});
it('ignores non-.md files', async () => {
const events: string[][] = [];
const handle = watchSkillDirectory(tmpDir, {
onChange: (changed) => events.push(changed),
debounceMs: 50,
});
fs.writeFileSync(path.join(tmpDir, 'notes.txt'), 'body');
fs.writeFileSync(path.join(tmpDir, 'config.json'), '{}');
await sleep(200);
const allChanged = events.flat();
expect(allChanged).not.toContain('notes.txt');
expect(allChanged).not.toContain('config.json');
handle.close();
});
it('debounces rapid .md writes into a single callback', async () => {
const events: string[][] = [];
const handle = watchSkillDirectory(tmpDir, {
onChange: (changed) => events.push(changed),
debounceMs: 100,
});
fs.writeFileSync(path.join(tmpDir, 'a.md'), 'x');
fs.writeFileSync(path.join(tmpDir, 'b.md'), 'x');
fs.writeFileSync(path.join(tmpDir, 'c.md'), 'x');
await sleep(250);
// One debounced callback with all changed files
expect(events.length).toBeLessThanOrEqual(2); // 1 debounce window + possible tail
const allChanged = new Set(events.flat());
expect(allChanged.has('a.md')).toBe(true);
expect(allChanged.has('b.md')).toBe(true);
expect(allChanged.has('c.md')).toBe(true);
handle.close();
});
it('close() is idempotent and silences further callbacks', async () => {
let calls = 0;
const handle = watchSkillDirectory(tmpDir, {
onChange: () => { calls++; },
debounceMs: 30,
});
handle.close();
handle.close(); // second close must not throw
fs.writeFileSync(path.join(tmpDir, 'post-close.md'), 'x');
await sleep(150);
expect(calls).toBe(0);
});
it('does not throw if onChange callback throws', async () => {
const handle = watchSkillDirectory(tmpDir, {
onChange: () => { throw new Error('callback explosion'); },
debounceMs: 30,
});
fs.writeFileSync(path.join(tmpDir, 'explode.md'), 'x');
// If this crashes the process, the test runner will fail globally.
await sleep(150);
handle.close();
expect(true).toBe(true); // reached without unhandled exception
});
});