52a03d7de4
Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.9 KiB
TypeScript
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);
|
|
});
|