This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
nerve/packages/daemon/src/__tests__/file-watcher.test.ts
T

131 lines
3.9 KiB
TypeScript

/**
* Unit tests for file-watcher.ts
*/
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createFileWatcher } from "../file-watcher.js";
import type { FileChange, FileWatcher } from "../file-watcher.js";
function makeTempNerveRoot(): string {
const dir = mkdtempSync(join(tmpdir(), "nerve-fw-test-"));
mkdirSync(join(dir, "senses", "cpu-usage"), { recursive: true });
writeFileSync(join(dir, "nerve.yaml"), "senses: {}\n");
writeFileSync(
join(dir, "senses", "cpu-usage", "index.js"),
"export async function compute() { return null; }",
);
return dir;
}
async function waitFor(
predicate: () => boolean,
timeoutMs: number,
intervalMs = 50,
): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`waitFor timed out after ${timeoutMs}ms`)),
timeoutMs,
);
const check = setInterval(() => {
if (predicate()) {
clearTimeout(timer);
clearInterval(check);
resolve();
}
}, intervalMs);
});
}
describe("createFileWatcher", () => {
let watcher: FileWatcher | null = null;
afterEach(() => {
if (watcher !== null) {
watcher.close();
watcher = null;
}
});
it("detects nerve.yaml changes", async () => {
const root = makeTempNerveRoot();
const changes: FileChange[] = [];
watcher = createFileWatcher(root, (change) => changes.push(change), 50);
// Wait for watcher to settle — macOS fs.watch may coalesce setup events
await new Promise((r) => setTimeout(r, 200));
// Clear any events from the setup phase
changes.length = 0;
writeFileSync(join(root, "nerve.yaml"), "senses: {}\n# changed\n");
await waitFor(() => changes.length > 0, 3000);
expect(changes.length).toBeGreaterThanOrEqual(1);
expect(changes.some((c) => c.kind === "config")).toBe(true);
}, 10_000);
it("detects sense .js file changes", async () => {
const root = makeTempNerveRoot();
const changes: FileChange[] = [];
watcher = createFileWatcher(root, (change) => changes.push(change), 50);
await new Promise((r) => setTimeout(r, 100));
writeFileSync(
join(root, "senses", "cpu-usage", "index.js"),
"export const initialState = {}; export async function compute(state) { return { state, trigger: null }; }",
);
await waitFor(() => changes.length > 0, 3000);
expect(changes.length).toBeGreaterThanOrEqual(1);
const senseChanges = changes.filter((c) => c.kind === "sense");
expect(senseChanges.length).toBeGreaterThanOrEqual(1);
expect((senseChanges[0] as { senseName: string }).senseName).toBe("cpu-usage");
}, 10_000);
it("close() stops the watcher", async () => {
const root = makeTempNerveRoot();
const changes: FileChange[] = [];
watcher = createFileWatcher(root, (change) => changes.push(change), 50);
await new Promise((r) => setTimeout(r, 100));
// Ignore fs.watch attachment noise on some platforms before asserting post-close silence
changes.length = 0;
watcher.close();
watcher = null;
writeFileSync(join(root, "nerve.yaml"), "senses: {}\n# after close\n");
// Wait and verify no changes were captured
await new Promise((r) => setTimeout(r, 500));
expect(changes.length).toBe(0);
}, 5_000);
it("debounces rapid changes", async () => {
const root = makeTempNerveRoot();
const changes: FileChange[] = [];
watcher = createFileWatcher(root, (change) => changes.push(change), 200);
await new Promise((r) => setTimeout(r, 100));
// Write rapidly
for (let i = 0; i < 5; i++) {
writeFileSync(join(root, "nerve.yaml"), `senses: {}\n# v${i}\n`);
}
await waitFor(() => changes.length > 0, 3000);
// With debounce, should see only 1 change (not 5)
expect(changes.length).toBe(1);
}, 10_000);
});