Files
Rune-Spec/tests/adapters/opencode.test.ts

254 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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、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", "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", "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("plan/build/archive skill 包含变更名智能识别引导", async () => {
await injectOpenCode(TMP_DIR);
for (const stage of ["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("不生成 rune-create 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-create.md");
expect(skills).not.toContain("rune-create");
});
it("plan skill 包含 create 引导", async () => {
await injectOpenCode(TMP_DIR);
const content = await readFile(
join(TMP_DIR, ".opencode", "skills", "rune-plan", "SKILL.md"),
"utf-8",
);
expect(content).toContain("如果变更目录尚不存在");
expect(content).toContain("rune create <变更名>");
});
it("discuss skill 包含 create 引导", async () => {
await injectOpenCode(TMP_DIR);
const content = await readFile(
join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"),
"utf-8",
);
expect(content).toContain("讨论结束后");
expect(content).toContain("rune create <变更名>");
});
it("intro skill 不包含 rune-create 作为独立命令", async () => {
await injectOpenCode(TMP_DIR);
const content = await readFile(
join(TMP_DIR, ".opencode", "skills", "rune-intro", "SKILL.md"),
"utf-8",
);
expect(content).not.toContain("/rune-create");
expect(content).toContain("rune create <变更名>");
});
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("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 });
}
});
});