Use fs.realpath() in config-source-discovery to resolve symlinks before loading skills, preventing duplicate/mismatched paths on systems where tmpdir() returns a symlink (e.g., macOS /var → /private/var). Also adds agents-config-dir utility for ~/.agents path resolution.
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from "bun:test"
|
|
import { mkdirSync, writeFileSync, symlinkSync, rmSync, realpathSync } from "fs"
|
|
import { join } from "path"
|
|
import { tmpdir } from "os"
|
|
import { resolveSymlink, resolveSymlinkAsync, isSymbolicLink } from "./file-utils"
|
|
|
|
const testDir = join(realpathSync(tmpdir()), "file-utils-test-" + Date.now())
|
|
|
|
// Create a directory structure that mimics the real-world scenario:
|
|
//
|
|
// testDir/
|
|
// ├── repo/
|
|
// │ ├── skills/
|
|
// │ │ └── category/
|
|
// │ │ └── my-skill/
|
|
// │ │ └── SKILL.md
|
|
// │ └── .opencode/
|
|
// │ └── skills/
|
|
// │ └── my-skill -> ../../skills/category/my-skill (relative symlink)
|
|
// └── config/
|
|
// └── skills -> ../repo/.opencode/skills (absolute symlink)
|
|
|
|
const realSkillDir = join(testDir, "repo", "skills", "category", "my-skill")
|
|
const repoOpencodeSkills = join(testDir, "repo", ".opencode", "skills")
|
|
const configSkills = join(testDir, "config", "skills")
|
|
|
|
beforeAll(() => {
|
|
// Create real skill directory with a file
|
|
mkdirSync(realSkillDir, { recursive: true })
|
|
writeFileSync(join(realSkillDir, "SKILL.md"), "# My Skill")
|
|
|
|
// Create .opencode/skills/ with a relative symlink to the real skill
|
|
mkdirSync(repoOpencodeSkills, { recursive: true })
|
|
symlinkSync("../../skills/category/my-skill", join(repoOpencodeSkills, "my-skill"))
|
|
|
|
// Create config/skills as an absolute symlink to .opencode/skills
|
|
mkdirSync(join(testDir, "config"), { recursive: true })
|
|
symlinkSync(repoOpencodeSkills, configSkills)
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
})
|
|
|
|
describe("resolveSymlink", () => {
|
|
it("resolves a regular file path to itself", () => {
|
|
const filePath = join(realSkillDir, "SKILL.md")
|
|
expect(resolveSymlink(filePath)).toBe(filePath)
|
|
})
|
|
|
|
it("resolves a relative symlink to its real path", () => {
|
|
const symlinkPath = join(repoOpencodeSkills, "my-skill")
|
|
expect(resolveSymlink(symlinkPath)).toBe(realSkillDir)
|
|
})
|
|
|
|
it("resolves a chained symlink (symlink-to-dir-containing-symlinks) to the real path", () => {
|
|
// This is the real-world scenario:
|
|
// config/skills/my-skill -> (follows config/skills) -> repo/.opencode/skills/my-skill -> repo/skills/category/my-skill
|
|
const chainedPath = join(configSkills, "my-skill")
|
|
expect(resolveSymlink(chainedPath)).toBe(realSkillDir)
|
|
})
|
|
|
|
it("returns the original path for non-existent paths", () => {
|
|
const fakePath = join(testDir, "does-not-exist")
|
|
expect(resolveSymlink(fakePath)).toBe(fakePath)
|
|
})
|
|
})
|
|
|
|
describe("resolveSymlinkAsync", () => {
|
|
it("resolves a regular file path to itself", async () => {
|
|
const filePath = join(realSkillDir, "SKILL.md")
|
|
expect(await resolveSymlinkAsync(filePath)).toBe(filePath)
|
|
})
|
|
|
|
it("resolves a relative symlink to its real path", async () => {
|
|
const symlinkPath = join(repoOpencodeSkills, "my-skill")
|
|
expect(await resolveSymlinkAsync(symlinkPath)).toBe(realSkillDir)
|
|
})
|
|
|
|
it("resolves a chained symlink (symlink-to-dir-containing-symlinks) to the real path", async () => {
|
|
const chainedPath = join(configSkills, "my-skill")
|
|
expect(await resolveSymlinkAsync(chainedPath)).toBe(realSkillDir)
|
|
})
|
|
|
|
it("returns the original path for non-existent paths", async () => {
|
|
const fakePath = join(testDir, "does-not-exist")
|
|
expect(await resolveSymlinkAsync(fakePath)).toBe(fakePath)
|
|
})
|
|
})
|
|
|
|
describe("isSymbolicLink", () => {
|
|
it("returns true for a symlink", () => {
|
|
expect(isSymbolicLink(join(repoOpencodeSkills, "my-skill"))).toBe(true)
|
|
})
|
|
|
|
it("returns false for a regular directory", () => {
|
|
expect(isSymbolicLink(realSkillDir)).toBe(false)
|
|
})
|
|
|
|
it("returns false for a non-existent path", () => {
|
|
expect(isSymbolicLink(join(testDir, "does-not-exist"))).toBe(false)
|
|
})
|
|
})
|