diff --git a/.agents/skills/nix-build/SKILL.md b/.agents/skills/nix-build/SKILL.md index 0646c0474..b965e9301 100644 --- a/.agents/skills/nix-build/SKILL.md +++ b/.agents/skills/nix-build/SKILL.md @@ -3,11 +3,11 @@ name: nix-build description: Build any @singularity-forge/* package (or the full stack) via nix develop. Pass a package name like "pi-coding-agent", "native", "mcp-server", or "all" for a full core build. --- -All build commands in this repo must run inside `nix develop`. Never use bare cargo/bun/rustc. +All build commands in this repo must run inside `nix develop`. Never use bare cargo/rustc. For a single package: ``` -nix develop --command bash -c "bun run --filter @singularity-forge/ build" +nix develop --command bash -c "npm run --workspace=@singularity-forge/ build" ``` For the full core build (native + all TS packages): diff --git a/packages/native/src/__tests__/watch.test.mjs b/packages/native/src/__tests__/watch.test.mjs new file mode 100644 index 000000000..d917fea43 --- /dev/null +++ b/packages/native/src/__tests__/watch.test.mjs @@ -0,0 +1,88 @@ +import { describe, test } from "vitest"; +import assert from "node:assert/strict"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import * as fs from "node:fs"; +import * as os from "node:os"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const { watchTree } = await import(path.resolve(__dirname, "../../dist/edit/index.js")); + +function makeTmpDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), "sf-watch-test-")); +} + +function waitForEvents(root, options, predicate, timeoutMs = 3000) { + return new Promise((resolve, reject) => { + const watch = watchTree(root, options ?? null); + const timer = setTimeout(() => { + watch.stop(); + reject(new Error(`Timed out after ${timeoutMs}ms waiting for events in ${root}`)); + }, timeoutMs); + + watch.on("events", (events) => { + if (predicate(events)) { + clearTimeout(timer); + watch.stop(); + resolve(events); + } + }); + }); +} + +describe("native edit: watchTree()", () => { + test("detects file creation", async ({ onTestFinished }) => { + const dir = makeTmpDir(); + onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true })); + + const pending = waitForEvents( + dir, + { debounceMs: 50 }, + (events) => events.some((event) => event.path.endsWith("hello.txt")), + ); + + await new Promise((resolve) => setTimeout(resolve, 100)); + fs.writeFileSync(path.join(dir, "hello.txt"), "hi"); + + const events = await pending; + assert.ok(events.some((event) => event.path.endsWith("hello.txt"))); + }); + + test("respects ignore patterns", async ({ onTestFinished }) => { + const dir = makeTmpDir(); + onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true })); + + const collected = []; + const watch = watchTree(dir, { ignore: ["*.log"], debounceMs: 50 }); + watch.on("events", (events) => collected.push(...events)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + fs.writeFileSync(path.join(dir, "ignored.log"), "log data"); + fs.writeFileSync(path.join(dir, "kept.txt"), "text data"); + await new Promise((resolve) => setTimeout(resolve, 600)); + watch.stop(); + + assert.equal(collected.filter((event) => event.path.endsWith("ignored.log")).length, 0); + assert.ok(collected.some((event) => event.path.endsWith("kept.txt"))); + }); + + test("stop ends the watch", async ({ onTestFinished }) => { + const dir = makeTmpDir(); + onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true })); + + const received = []; + const watch = watchTree(dir, { debounceMs: 50 }); + watch.on("events", (events) => received.push(...events)); + + await new Promise((resolve) => setTimeout(resolve, 100)); + fs.writeFileSync(path.join(dir, "before.txt"), "a"); + await new Promise((resolve) => setTimeout(resolve, 300)); + watch.stop(); + + const countAfterStop = received.length; + fs.writeFileSync(path.join(dir, "after.txt"), "b"); + await new Promise((resolve) => setTimeout(resolve, 300)); + + assert.equal(received.length, countAfterStop); + }); +}); diff --git a/scripts/base64-scan.sh b/scripts/base64-scan.sh index 5732ca36a..74f4f7610 100755 --- a/scripts/base64-scan.sh +++ b/scripts/base64-scan.sh @@ -102,7 +102,7 @@ should_scan() { *.zip|*.tar|*.gz|*.tgz|*.bz2|*.7z|*.rar|\ *.exe|*.dll|*.so|*.dylib|*.o|*.a|\ *.pdf|*.doc|*.docx|*.xls|*.xlsx|\ - *.lock|package-lock.json|pnpm-lock.yaml|bun.lock|\ + *.lock|package-lock.json|pnpm-lock.yaml|\ *.min.js|*.min.css|*.map|\ *.node|*.wasm) return 1 ;; diff --git a/scripts/secret-scan.sh b/scripts/secret-scan.sh index 741890e9f..1fb53d4a2 100755 --- a/scripts/secret-scan.sh +++ b/scripts/secret-scan.sh @@ -116,7 +116,7 @@ should_scan() { *.zip|*.tar|*.gz|*.tgz|*.bz2|*.7z|*.rar|\ *.exe|*.dll|*.so|*.dylib|*.o|*.a|\ *.pdf|*.doc|*.docx|*.xls|*.xlsx|\ - *.lock|package-lock.json|pnpm-lock.yaml|bun.lock|\ + *.lock|package-lock.json|pnpm-lock.yaml|\ *.min.js|*.min.css|*.map|\ *.node|*.wasm) return 1 ;;