Files
2026-07-13 12:20:01 +08:00

118 lines
3.2 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import os from "os"
import path from "path"
const tempDirs: string[] = []
async function makeTempDir(prefix: string): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
tempDirs.push(dir)
return dir
}
afterEach(async () => {
await Promise.all(
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
)
})
const installScript = path.join(
import.meta.dir,
"..",
".cline",
"scripts",
"install-skills.sh",
)
const manualSkill = "lfg"
const invocableSkill = "ce-plan"
type RunResult = {
exitCode: number
stdout: string
stderr: string
}
async function runInstall(
env: Record<string, string | undefined>,
args: string[] = ["--global"],
): Promise<RunResult> {
const proc = Bun.spawn(["bash", installScript, ...args], {
cwd: path.join(import.meta.dir, ".."),
env: { ...process.env, ...env },
stderr: "pipe",
stdout: "pipe",
})
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
])
return { exitCode, stdout, stderr }
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
describe("cline install-skills.sh", () => {
test("does not remove unrelated manual-only skill symlinks", async () => {
const dest = await makeTempDir("cline-skills-dest-")
const userSkill = await makeTempDir("cline-user-lfg-")
await fs.writeFile(path.join(userSkill, "SKILL.md"), "# user lfg\n")
await fs.symlink(userSkill, path.join(dest, manualSkill))
const result = await runInstall({
CLINE_SKILLS_DIR: dest,
})
expect(result.exitCode).toBe(0)
expect(await pathExists(path.join(dest, manualSkill))).toBe(true)
expect(result.stderr).not.toContain("removed lfg")
})
test("does not overwrite an existing user-managed symlink for an invocable skill", async () => {
const dest = await makeTempDir("cline-skills-user-invocable-")
const userSkill = await makeTempDir("cline-user-ce-plan-")
await fs.writeFile(path.join(userSkill, "SKILL.md"), "# user ce-plan\n")
const link = path.join(dest, invocableSkill)
await fs.symlink(userSkill, link)
const result = await runInstall({
CLINE_SKILLS_DIR: dest,
})
expect(result.exitCode).toBe(0)
expect(await fs.realpath(link)).toBe(await fs.realpath(userSkill))
expect(result.stderr).toContain(
`skip ${invocableSkill}: ${link} is an existing user-managed symlink (not overwritten)`,
)
})
test("removes stale CE-owned manual-only symlinks on default install", async () => {
const dest = await makeTempDir("cline-skills-ce-")
const repoRoot = path.join(import.meta.dir, "..")
const ceManualSkill = path.join(repoRoot, "skills", manualSkill)
await fs.symlink(ceManualSkill, path.join(dest, manualSkill))
const result = await runInstall({
CLINE_SKILLS_DIR: dest,
})
expect(result.exitCode).toBe(0)
expect(await pathExists(path.join(dest, manualSkill))).toBe(false)
expect(result.stderr).toContain("removed lfg: stale CE manual-only symlink")
})
})