fix(skills): avoid skills watcher FD exhaustion

Watch SKILL.md only (and one-level SKILL.md in skill roots) to prevent chokidar from tracking huge unrelated trees.

Co-authored-by: household-bard <shakespeare@hessianinformatics.com>
This commit is contained in:
Peter Steinberger
2026-02-14 19:25:19 +01:00
parent 01b3226ecb
commit 0e046f61ab
3 changed files with 82 additions and 4 deletions

View File

@@ -0,0 +1,64 @@
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
const watchMock = vi.fn(() => ({
on: vi.fn(),
close: vi.fn(async () => undefined),
}));
vi.mock("chokidar", () => {
return {
default: { watch: watchMock },
};
});
describe("ensureSkillsWatcher", () => {
it("ignores node_modules, dist, .git, and Python venvs by default", async () => {
const mod = await import("./refresh.js");
mod.ensureSkillsWatcher({ workspaceDir: "/tmp/workspace" });
expect(watchMock).toHaveBeenCalledTimes(1);
const targets = watchMock.mock.calls[0]?.[0] as string[];
const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown };
expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED);
expect(targets).toEqual(
expect.arrayContaining([
path.join("/tmp/workspace", "skills", "SKILL.md"),
path.join("/tmp/workspace", "skills", "*", "SKILL.md"),
]),
);
expect(targets.every((target) => target.includes("SKILL.md"))).toBe(true);
const ignored = mod.DEFAULT_SKILLS_WATCH_IGNORED;
// Node/JS paths
expect(ignored.some((re) => re.test("/tmp/workspace/skills/node_modules/pkg/index.js"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/dist/index.js"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.git/config"))).toBe(true);
// Python virtual environments and caches
expect(ignored.some((re) => re.test("/tmp/workspace/skills/scripts/.venv/bin/python"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/venv/lib/python3.10/site.py"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/__pycache__/module.pyc"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.mypy_cache/3.10/foo.json"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.pytest_cache/v/cache"))).toBe(true);
// Build artifacts and caches
expect(ignored.some((re) => re.test("/tmp/workspace/skills/build/output.js"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.cache/data.json"))).toBe(true);
// Should NOT ignore normal skill files
expect(ignored.some((re) => re.test("/tmp/.hidden/skills/index.md"))).toBe(false);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/my-skill/SKILL.md"))).toBe(false);
});
});