import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; import { mkdir, rm, readFile, readdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { injectOpenCode, updateOpenCode } from "../../src/adapters/opencode.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_opencode_test__"); beforeEach(async () => { await mkdir(TMP_DIR, { recursive: true }); }); afterEach(async () => { await rm(TMP_DIR, { recursive: true, force: true }); }); describe("injectOpenCode", () => { it("生成 discuss、create、plan、build、archive 的 command 和 skill 文件", async () => { await injectOpenCode(TMP_DIR); const commands = await readdir(join(TMP_DIR, ".opencode", "commands")); const skills = await readdir(join(TMP_DIR, ".opencode", "skills")); for (const stage of ["discuss", "create", "plan", "build", "archive"]) { expect(commands).toContain(`rune-${stage}.md`); expect(skills).toContain(`rune-${stage}`); expect(existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"))).toBe( true, ); } }); it("不生成 rune-status command 和 skill", async () => { await injectOpenCode(TMP_DIR); const commands = await readdir(join(TMP_DIR, ".opencode", "commands")); const skills = await readdir(join(TMP_DIR, ".opencode", "skills")); expect(commands).not.toContain("rune-status.md"); expect(skills).not.toContain("rune-status"); }); it("生成 rune-intro skill(无对应 command)", async () => { await injectOpenCode(TMP_DIR); const commands = await readdir(join(TMP_DIR, ".opencode", "commands")); const skills = await readdir(join(TMP_DIR, ".opencode", "skills")); expect(commands).not.toContain("rune-intro.md"); expect(skills).toContain("rune-intro"); expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-intro", "SKILL.md"))).toBe(true); }); it("command 文件统一格式:只包含 skill 调用指令", async () => { await injectOpenCode(TMP_DIR); for (const stage of ["discuss", "create", "plan", "build", "archive"]) { const content = await readFile( join(TMP_DIR, ".opencode", "commands", `rune-${stage}.md`), "utf-8", ); expect(content).toContain(`rune-${stage} skill`); expect(content).not.toContain("变更名"); } }); it("create/plan/build/archive skill 包含变更名智能识别引导", async () => { await injectOpenCode(TMP_DIR); for (const stage of ["create", "plan", "build", "archive"]) { const content = await readFile( join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"), "utf-8", ); expect(content).toContain("智能识别"); expect(content).toContain("rune status"); } }); it("discuss skill 不包含智能识别引导", async () => { await injectOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"), "utf-8", ); expect(content).not.toContain("智能识别"); }); it("plan skill 包含运行 status 的引导", async () => { await injectOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "skills", "rune-plan", "SKILL.md"), "utf-8", ); expect(content).toContain("rune status"); }); it("discuss skill 不包含 status 引导", async () => { await injectOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"), "utf-8", ); expect(content).not.toContain("规划阶段应先运行"); }); it("重复注入时不覆盖已存在的文件", async () => { await injectOpenCode(TMP_DIR); const originalContent = await readFile( join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "utf-8", ); await injectOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "utf-8", ); expect(content).toBe(originalContent); }); }); describe("updateOpenCode", () => { it("文件不存在时创建", async () => { await updateOpenCode(TMP_DIR); expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true); expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"))).toBe(true); }); it("文件存在且内容一致时不覆盖", async () => { await injectOpenCode(TMP_DIR); const originalContent = await readFile( join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "utf-8", ); await updateOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "utf-8", ); expect(content).toBe(originalContent); }); it("文件存在但内容不一致时覆盖", async () => { await injectOpenCode(TMP_DIR); await writeFile(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "旧内容"); await updateOpenCode(TMP_DIR); const content = await readFile( join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "utf-8", ); expect(content).not.toBe("旧内容"); expect(content).toContain("rune-discuss"); }); it("更新时生成 rune-intro skill", async () => { await updateOpenCode(TMP_DIR); expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-intro", "SKILL.md"))).toBe(true); }); it("不生成 rune-status", async () => { await updateOpenCode(TMP_DIR); expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-status.md"))).toBe(false); expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md"))).toBe(false); }); }); describe("injectOpenCode with command prefix", () => { it("使用自定义前缀生成 skill 文件", async () => { const tmpDir = join(import.meta.dir, "__tmp_opencode_prefix_test__"); await mkdir(tmpDir, { recursive: true }); try { await injectOpenCode(tmpDir, "bunx @lanyuanxiaoyao/rune"); const content = await readFile( join(tmpDir, ".opencode", "skills", "rune-discuss", "SKILL.md"), "utf-8", ); expect(content).toContain("bunx @lanyuanxiaoyao/rune discuss"); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); it("不传前缀时使用默认 rune 前缀", async () => { const tmpDir = join(import.meta.dir, "__tmp_opencode_default_test__"); await mkdir(tmpDir, { recursive: true }); try { await injectOpenCode(tmpDir); const content = await readFile( join(tmpDir, ".opencode", "skills", "rune-discuss", "SKILL.md"), "utf-8", ); expect(content).toContain("rune discuss"); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); it("create skill 使用自定义前缀", async () => { const tmpDir = join(import.meta.dir, "__tmp_opencode_create_test__"); await mkdir(tmpDir, { recursive: true }); try { await injectOpenCode(tmpDir, "pnpx @lanyuanxiaoyao/rune"); const content = await readFile( join(tmpDir, ".opencode", "skills", "rune-create", "SKILL.md"), "utf-8", ); expect(content).toContain("pnpx @lanyuanxiaoyao/rune create"); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); it("rune-intro skill 使用自定义前缀", async () => { const tmpDir = join(import.meta.dir, "__tmp_opencode_intro_test__"); await mkdir(tmpDir, { recursive: true }); try { await injectOpenCode(tmpDir, "bunx @lanyuanxiaoyao/rune"); const content = await readFile( join(tmpDir, ".opencode", "skills", "rune-intro", "SKILL.md"), "utf-8", ); expect(content).toContain("bunx @lanyuanxiaoyao/rune status"); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); });