Parent directory

skill-locator.test.ts

4102 bytes
  1import { describe, it, beforeEach, afterEach } from "node:test";
  2import assert from "node:assert/strict";
  3import type { FileSystem } from "../ports/fs.ts";
  4import { DefaultSkillLocator } from "./skill-locator.ts";
  5
  6const ORIGINAL_HOME = process.env.HOME;
  7const ORIGINAL_PI_CODING_AGENT_DIR = process.env.PI_CODING_AGENT_DIR;
  8
  9describe("DefaultSkillLocator", () => {
 10  beforeEach(() => {
 11    process.env.HOME = "/home/tester";
 12    process.env.PI_CODING_AGENT_DIR = "/home/tester/.pi/agent";
 13  });
 14
 15  afterEach(() => {
 16    restoreEnv("HOME", ORIGINAL_HOME);
 17    restoreEnv("PI_CODING_AGENT_DIR", ORIGINAL_PI_CODING_AGENT_DIR);
 18  });
 19
 20  it("finds global, user, and project skills with Pi root markdown discovery rules", async () => {
 21    const fs = new MemoryTreeFileSystem([
 22      "/home/tester/.pi/agent/skills/user-root.md",
 23      "/home/tester/.agents/skills/ignored-global-root.md",
 24      "/home/tester/.agents/skills/global-skill/SKILL.md",
 25      "/repo/.pi/skills/project-root.md",
 26      "/repo/.agents/skills/ignored-project-legacy-root.md",
 27      "/repo/.agents/skills/project-legacy-skill/SKILL.md",
 28    ]);
 29    const locator = new DefaultSkillLocator(fs);
 30
 31    const files = await locator.findSkillFiles("/repo");
 32    const byPath = new Map(files.map((file) => [file.filePath, file]));
 33
 34    assert.equal(byPath.get("/home/tester/.pi/agent/skills/user-root.md")?.source.kind, "user");
 35    assert.equal(byPath.get("/home/tester/.agents/skills/global-skill/SKILL.md")?.source.kind, "global");
 36    assert.equal(byPath.get("/repo/.pi/skills/project-root.md")?.source.kind, "project");
 37    assert.equal(byPath.get("/repo/.agents/skills/project-legacy-skill/SKILL.md")?.source.kind, "project-legacy");
 38    assert.equal(byPath.has("/home/tester/.agents/skills/ignored-global-root.md"), false);
 39    assert.equal(byPath.has("/repo/.agents/skills/ignored-project-legacy-root.md"), false);
 40  });
 41});
 42
 43function restoreEnv(name: string, value: string | undefined): void {
 44  if (value === undefined) {
 45    delete process.env[name];
 46  } else {
 47    process.env[name] = value;
 48  }
 49}
 50
 51class MemoryTreeFileSystem implements FileSystem {
 52  private readonly files = new Set<string>();
 53  private readonly dirs = new Set<string>(["/"]);
 54
 55  constructor(paths: string[]) {
 56    for (const path of paths) this.addFile(path);
 57  }
 58
 59  async readFile(path: string): Promise<string> {
 60    if (!this.files.has(path)) throw new Error(`missing file: ${path}`);
 61    return "---\nname: test\ndescription: Test skill.\n---\n";
 62  }
 63
 64  async writeFileAtomic(): Promise<void> {}
 65
 66  async access(path: string): Promise<boolean> {
 67    return this.files.has(path) || this.dirs.has(path);
 68  }
 69
 70  async readdir(path: string): Promise<Array<{ name: string; isDirectory: boolean; isFile: boolean; isSymbolicLink: boolean }>> {
 71    if (!this.dirs.has(path)) throw new Error(`missing dir: ${path}`);
 72    const prefix = path === "/" ? "/" : `${path}/`;
 73    const names = new Set<string>();
 74    for (const dir of this.dirs) {
 75      if (dir === path || !dir.startsWith(prefix)) continue;
 76      const rest = dir.slice(prefix.length);
 77      const [name] = rest.split("/");
 78      if (name) names.add(name);
 79    }
 80    for (const file of this.files) {
 81      if (!file.startsWith(prefix)) continue;
 82      const rest = file.slice(prefix.length);
 83      const [name] = rest.split("/");
 84      if (name) names.add(name);
 85    }
 86    return [...names].sort().map((name) => {
 87      const fullPath = path === "/" ? `/${name}` : `${path}/${name}`;
 88      return {
 89        name,
 90        isDirectory: this.dirs.has(fullPath),
 91        isFile: this.files.has(fullPath),
 92        isSymbolicLink: false,
 93      };
 94    });
 95  }
 96
 97  async stat(path: string): Promise<{ isDirectory: boolean; isFile: boolean; mode: number }> {
 98    return { isDirectory: this.dirs.has(path), isFile: this.files.has(path), mode: 0o644 };
 99  }
100
101  private addFile(path: string): void {
102    this.files.add(path);
103    const parts = path.split("/").filter(Boolean);
104    let current = "";
105    for (const part of parts.slice(0, -1)) {
106      current += `/${part}`;
107      this.dirs.add(current);
108    }
109  }
110}