| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- import { describe, expect } from "bun:test"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { Effect, Layer } from "effect"
- import { Skill } from "../../src/skill"
- import { Discovery } from "../../src/skill/discovery"
- import { RuntimeFlags } from "../../src/effect/runtime-flags"
- import { EventV2Bridge } from "../../src/event-v2-bridge"
- import { Config } from "../../src/config/config"
- import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
- import { FSUtil } from "@kirincode-ai/core/fs-util"
- import { Global } from "@kirincode-ai/core/global"
- import { provideInstance, provideTmpdirInstance, testInstanceStoreLayer, tmpdir } from "../fixture/fixture"
- import { testEffect } from "../lib/effect"
- import path from "path"
- import fs from "fs/promises"
- const node = LayerNode.compile(CrossSpawnSpawner.node)
- const it = testEffect(Layer.mergeAll(LayerNode.compile(Skill.node), node, testInstanceStoreLayer))
- const itWithoutClaudeCodeSkills = testEffect(
- Layer.mergeAll(
- LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableClaudeCodeSkills: true })]]),
- node,
- testInstanceStoreLayer,
- ),
- )
- const itWithoutExternalSkills = testEffect(
- Layer.mergeAll(
- LayerNode.compile(Skill.node, [[RuntimeFlags.node, RuntimeFlags.layer({ disableExternalSkills: true })]]),
- node,
- testInstanceStoreLayer,
- ),
- )
- async function createGlobalSkill(homeDir: string) {
- const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
- await fs.mkdir(skillDir, { recursive: true })
- await Bun.write(
- path.join(skillDir, "SKILL.md"),
- `---
- name: global-test-skill
- description: A global skill from ~/.claude/skills for testing.
- ---
- # Global Test Skill
- This skill is loaded from the global home directory.
- `,
- )
- }
- const withHome = <A, E, R>(home: string, self: Effect.Effect<A, E, R>) =>
- Effect.acquireUseRelease(
- Effect.sync(() => {
- const prev = process.env.KIRINCODE_TEST_HOME
- process.env.KIRINCODE_TEST_HOME = home
- return prev
- }),
- () => self,
- (prev) =>
- Effect.sync(() => {
- process.env.KIRINCODE_TEST_HOME = prev
- }),
- )
- describe("skill", () => {
- it.effect("formats verbose locations as XML-safe filesystem paths", () =>
- Effect.sync(() => {
- const output = Skill.fmt(
- [
- {
- name: "tagged-skill",
- description: "A tagged skill.",
- location: "/tmp/plugin.git#v1.3.0/SKILL.md",
- content: "",
- },
- {
- name: "built-in-skill",
- description: "A built-in skill.",
- location: "<built-in>",
- content: "",
- },
- ],
- { verbose: true },
- )
- expect(output).toContain("<location>/tmp/plugin.git#v1.3.0/SKILL.md</location>")
- expect(output).toContain("<location><built-in></location>")
- expect(output).not.toContain("file://")
- expect(output).not.toContain("%23")
- }),
- )
- it.live("discovers skills from .kirincode/skill/ directory", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".kirincode", "skill", "test-skill", "SKILL.md"),
- `---
- name: test-skill
- description: A test skill for verification.
- ---
- # Test Skill
- Instructions here.
- `,
- ),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- const item = list.find((x) => x.name === "test-skill")
- expect(item).toBeDefined()
- expect(item!.description).toBe("A test skill for verification.")
- expect(item!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
- }),
- { git: true },
- ),
- )
- it.live("returns skill directories from Skill.dirs", () =>
- provideTmpdirInstance(
- (dir) =>
- withHome(
- dir,
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".kirincode", "skill", "dir-skill", "SKILL.md"),
- `---
- name: dir-skill
- description: Skill for dirs test.
- ---
- # Dir Skill
- `,
- ),
- )
- const skill = yield* Skill.Service
- const dirs = yield* skill.dirs()
- expect(dirs).toContain(path.join(dir, ".kirincode", "skill", "dir-skill"))
- expect(dirs.length).toBe(1)
- }),
- ),
- { git: true },
- ),
- )
- it.live("discovers multiple skills from .kirincode/skill/ directory", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all([
- Bun.write(
- path.join(dir, ".kirincode", "skill", "skill-one", "SKILL.md"),
- `---
- name: skill-one
- description: First test skill.
- ---
- # Skill One
- `,
- ),
- Bun.write(
- path.join(dir, ".kirincode", "skill", "skill-two", "SKILL.md"),
- `---
- name: skill-two
- description: Second test skill.
- ---
- # Skill Two
- `,
- ),
- ]),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(2)
- expect(list.find((x) => x.name === "skill-one")).toBeDefined()
- expect(list.find((x) => x.name === "skill-two")).toBeDefined()
- }),
- { git: true },
- ),
- )
- it.live("skips skills with missing frontmatter", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".kirincode", "skill", "no-frontmatter", "SKILL.md"),
- `# No Frontmatter
- Just some content without YAML frontmatter.
- `,
- ),
- )
- const skill = yield* Skill.Service
- expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
- }),
- { git: true },
- ),
- )
- it.live("discovers skills without descriptions", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".kirincode", "skill", "manual-skill", "SKILL.md"),
- `---
- name: manual-skill
- ---
- # Manual Skill
- Instructions here.
- `,
- ),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- const item = list.find((x) => x.name === "manual-skill")
- expect(item).toBeDefined()
- expect(item!.description).toBeUndefined()
- expect(Skill.fmt(list, { verbose: false })).toBe("No skills are currently available.")
- expect(Skill.fmt(list, { verbose: true })).toBe("No skills are currently available.")
- }),
- { git: true },
- ),
- )
- it.live("discovers skills from .claude/skills/ directory", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
- `---
- name: claude-skill
- description: A skill in the .claude/skills directory.
- ---
- # Claude Skill
- `,
- ),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- const item = list.find((x) => x.name === "claude-skill")
- expect(item).toBeDefined()
- expect(item!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
- }),
- { git: true },
- ),
- )
- it.live("discovers global skills from ~/.claude/skills/ directory", () =>
- Effect.gen(function* () {
- const tmp = yield* Effect.acquireRelease(
- Effect.promise(() => tmpdir({ git: true })),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- )
- yield* withHome(
- tmp.path,
- Effect.gen(function* () {
- yield* Effect.promise(() => createGlobalSkill(tmp.path))
- yield* Effect.gen(function* () {
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- expect(list[0].name).toBe("global-test-skill")
- expect(list[0].description).toBe("A global skill from ~/.claude/skills for testing.")
- expect(list[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
- }).pipe(provideInstance(tmp.path))
- }),
- )
- }),
- )
- it.live("returns empty array when no skills exist", () =>
- provideTmpdirInstance(
- () =>
- Effect.gen(function* () {
- const skill = yield* Skill.Service
- expect((yield* skill.all()).filter((s) => s.location !== "<built-in>")).toEqual([])
- }),
- { git: true },
- ),
- )
- it.live("fails with typed error when requiring a missing skill", () =>
- provideTmpdirInstance(
- () =>
- Effect.gen(function* () {
- const skill = yield* Skill.Service
- const error = yield* Effect.flip(skill.require("missing-skill"))
- expect(error).toBeInstanceOf(Skill.NotFoundError)
- expect(error._tag).toBe("Skill.NotFoundError")
- expect(error.name).toBe("missing-skill")
- expect(error.message).toContain('Skill "missing-skill" not found.')
- }),
- { git: true },
- ),
- )
- it.effect("exposes tagged expected skill failure classes", () =>
- Effect.sync(() => {
- const invalid = new Skill.InvalidError({ path: "/tmp/SKILL.md", message: "Invalid skill frontmatter" })
- const mismatch = new Skill.NameMismatchError({
- path: "/tmp/SKILL.md",
- expected: "expected-skill",
- actual: "actual-skill",
- })
- expect(invalid).toBeInstanceOf(Skill.InvalidError)
- expect(invalid._tag).toBe("SkillInvalidError")
- expect(mismatch).toBeInstanceOf(Skill.NameMismatchError)
- expect(mismatch._tag).toBe("SkillNameMismatchError")
- }),
- )
- it.live("discovers skills from .agents/skills/ directory", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Bun.write(
- path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
- `---
- name: agent-skill
- description: A skill in the .agents/skills directory.
- ---
- # Agent Skill
- `,
- ),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- const item = list.find((x) => x.name === "agent-skill")
- expect(item).toBeDefined()
- expect(item!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
- }),
- { git: true },
- ),
- )
- it.live("discovers global skills from ~/.agents/skills/ directory", () =>
- Effect.gen(function* () {
- const tmp = yield* Effect.acquireRelease(
- Effect.promise(() => tmpdir({ git: true })),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- )
- yield* withHome(
- tmp.path,
- Effect.gen(function* () {
- const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
- yield* Effect.promise(() => fs.mkdir(skillDir, { recursive: true }))
- yield* Effect.promise(() =>
- Bun.write(
- path.join(skillDir, "SKILL.md"),
- `---
- name: global-agent-skill
- description: A global skill from ~/.agents/skills for testing.
- ---
- # Global Agent Skill
- This skill is loaded from the global home directory.
- `,
- ),
- )
- yield* Effect.gen(function* () {
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(1)
- expect(list[0].name).toBe("global-agent-skill")
- expect(list[0].description).toBe("A global skill from ~/.agents/skills for testing.")
- expect(list[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
- }).pipe(provideInstance(tmp.path))
- }),
- )
- }),
- )
- it.live("discovers skills from both .claude/skills/ and .agents/skills/", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all([
- Bun.write(
- path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
- `---
- name: claude-skill
- description: A skill in the .claude/skills directory.
- ---
- # Claude Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
- `---
- name: agent-skill
- description: A skill in the .agents/skills directory.
- ---
- # Agent Skill
- `,
- ),
- ]),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.length).toBe(2)
- expect(list.find((x) => x.name === "claude-skill")).toBeDefined()
- expect(list.find((x) => x.name === "agent-skill")).toBeDefined()
- }),
- { git: true },
- ),
- )
- itWithoutClaudeCodeSkills.live("skips Claude Code skills when disabled", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all([
- Bun.write(
- path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
- `---
- name: claude-skill
- description: A skill in the .claude/skills directory.
- ---
- # Claude Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
- `---
- name: agent-skill
- description: A skill in the .agents/skills directory.
- ---
- # Agent Skill
- `,
- ),
- ]),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.map((s) => s.name)).toEqual(["agent-skill"])
- }),
- { git: true },
- ),
- )
- itWithoutExternalSkills.live("skips external skill directories when disabled", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all([
- Bun.write(
- path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
- `---
- name: claude-skill
- description: A skill in the .claude/skills directory.
- ---
- # Claude Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
- `---
- name: agent-skill
- description: A skill in the .agents/skills directory.
- ---
- # Agent Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".kirincode", "skill", "opencode-skill", "SKILL.md"),
- `---
- name: opencode-skill
- description: A skill in the .kirincode/skill directory.
- ---
- # KirinCode Skill
- `,
- ),
- ]),
- )
- const skill = yield* Skill.Service
- const list = (yield* skill.all()).filter((s) => s.location !== "<built-in>")
- expect(list.map((s) => s.name)).toEqual(["opencode-skill"])
- }),
- { git: true },
- ),
- )
- it.live("properly resolves directories that skills live in", () =>
- provideTmpdirInstance(
- (dir) =>
- Effect.gen(function* () {
- yield* Effect.promise(() =>
- Promise.all([
- Bun.write(
- path.join(dir, ".claude", "skills", "claude-skill", "SKILL.md"),
- `---
- name: claude-skill
- description: A skill in the .claude/skills directory.
- ---
- # Claude Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".agents", "skills", "agent-skill", "SKILL.md"),
- `---
- name: agent-skill
- description: A skill in the .agents/skills directory.
- ---
- # Agent Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".kirincode", "skill", "agent-skill", "SKILL.md"),
- `---
- name: opencode-skill
- description: A skill in the .kirincode/skill directory.
- ---
- # KirinCode Skill
- `,
- ),
- Bun.write(
- path.join(dir, ".kirincode", "skills", "agent-skill", "SKILL.md"),
- `---
- name: opencode-skill
- description: A skill in the .kirincode/skills directory.
- ---
- # KirinCode Skill
- `,
- ),
- ]),
- )
- const skill = yield* Skill.Service
- expect((yield* skill.dirs()).length).toBe(4)
- }),
- { git: true },
- ),
- )
- })
|