mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-11-02 13:13:20 +08:00
Introduce a primaryWorktree Effect that queries Git plumbing commands to locate the main checkout from any linked worktree. Integrate it into config loading so .kilo directory settings from the primary checkout act as local-scoped fallbacks beneath the active worktree and KILO_CONFIG_DIR overrides. Wire primaryPaths into skill discovery so project skills installed in the primary checkout appear as fallbacks unless overridden locally. Refactor session family matching to consider sandbox directories and filter out nested Git repositories when resolving worktree roots. Convert baselineDirectory in indexing to an Effect that delegates to primaryWorktree instead of reading Instance.project.worktree directly. Restore the content() signal usage in the TUI text part renderer. Provide Git.defaultLayer across all affected test layers and add coverage for linked worktrees, submodules, paths with spaces, sandbox project matching, and primary config directory precedence.
565 lines
16 KiB
TypeScript
565 lines
16 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import { Skill } from "../../src/skill"
|
|
import { Discovery } from "../../src/skill/discovery"
|
|
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
|
import { Bus } from "../../src/bus"
|
|
import { Config } from "../../src/config/config"
|
|
import { Git } from "../../src/git" // kilocode_change
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
import path from "path"
|
|
import fs from "fs/promises"
|
|
|
|
const node = CrossSpawnSpawner.defaultLayer
|
|
|
|
const skills = (disableExternalSkills: boolean, disableClaudeCodeSkills: boolean) =>
|
|
Skill.layer.pipe(
|
|
Layer.provide(Git.defaultLayer), // kilocode_change
|
|
Layer.provide(Discovery.defaultLayer),
|
|
Layer.provide(Config.defaultLayer),
|
|
Layer.provide(Bus.layer),
|
|
Layer.provide(AppFileSystem.defaultLayer),
|
|
Layer.provide(Global.layer),
|
|
Layer.provide(RuntimeFlags.layer({ disableExternalSkills, disableClaudeCodeSkills })),
|
|
)
|
|
|
|
const it = testEffect(Layer.mergeAll(skills(false, false), node))
|
|
const itWithoutExternalSkills = testEffect(Layer.mergeAll(skills(true, false), node))
|
|
const itWithoutClaudeCodeSkills = testEffect(Layer.mergeAll(skills(false, true), node)) // kilocode_change
|
|
|
|
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.KILO_TEST_HOME
|
|
process.env.KILO_TEST_HOME = home
|
|
return prev
|
|
}),
|
|
() => self,
|
|
(prev) =>
|
|
Effect.sync(() => {
|
|
process.env.KILO_TEST_HOME = prev
|
|
}),
|
|
)
|
|
|
|
const discovered = <T extends { location: string }>(list: readonly T[]) =>
|
|
list.filter((skill) => ![Skill.BUILTIN_LOCATION, "<built-in>"].includes(skill.location)) // kilocode_change
|
|
|
|
describe("skill", () => {
|
|
// kilocode_change start
|
|
it.live("discovers skills from .kilo/skill/ directory", () =>
|
|
// kilocode_change end
|
|
provideTmpdirInstance(
|
|
(dir) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(() =>
|
|
Bun.write(
|
|
path.join(dir, ".kilo", "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 = discovered(yield* skill.all()) // kilocode_change
|
|
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, ".kilo", "skill", "dir-skill", "SKILL.md"), // kilocode_change: .kilo is primary
|
|
`---
|
|
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, ".kilo", "skill", "dir-skill")) // kilocode_change: .kilo is primary
|
|
expect(dirs.length).toBe(1)
|
|
}),
|
|
),
|
|
{ git: true },
|
|
),
|
|
)
|
|
|
|
// kilocode_change start
|
|
it.live("discovers multiple skills from .kilo/skill/ directory", () =>
|
|
// kilocode_change end
|
|
provideTmpdirInstance(
|
|
(dir) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(() =>
|
|
Promise.all([
|
|
Bun.write(
|
|
path.join(dir, ".kilo", "skill", "skill-one", "SKILL.md"),
|
|
`---
|
|
name: skill-one
|
|
description: First test skill.
|
|
---
|
|
|
|
# Skill One
|
|
`,
|
|
),
|
|
Bun.write(
|
|
path.join(dir, ".kilo", "skill", "skill-two", "SKILL.md"),
|
|
`---
|
|
name: skill-two
|
|
description: Second test skill.
|
|
---
|
|
|
|
# Skill Two
|
|
`,
|
|
),
|
|
]),
|
|
)
|
|
|
|
const skill = yield* Skill.Service
|
|
const list = discovered(yield* skill.all()) // kilocode_change
|
|
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, ".kilo", "skill", "no-frontmatter", "SKILL.md"), // kilocode_change: .kilo is primary
|
|
`# No Frontmatter
|
|
|
|
Just some content without YAML frontmatter.
|
|
`,
|
|
),
|
|
)
|
|
|
|
const skill = yield* Skill.Service
|
|
expect(discovered(yield* skill.all())).toEqual([]) // kilocode_change
|
|
}),
|
|
{ git: true },
|
|
),
|
|
)
|
|
|
|
it.live("discovers skills without descriptions", () =>
|
|
provideTmpdirInstance(
|
|
(dir) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(() =>
|
|
Bun.write(
|
|
path.join(dir, ".opencode", "skill", "manual-skill", "SKILL.md"),
|
|
`---
|
|
name: manual-skill
|
|
---
|
|
|
|
# Manual Skill
|
|
|
|
Instructions here.
|
|
`,
|
|
),
|
|
)
|
|
|
|
const skill = yield* Skill.Service
|
|
const list = discovered(yield* skill.all()) // kilocode_change
|
|
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 = discovered(yield* skill.all()) // kilocode_change
|
|
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 = discovered(yield* skill.all()) // kilocode_change
|
|
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(discovered(yield* skill.all())).toEqual([]) // kilocode_change
|
|
}),
|
|
{ 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 = discovered(yield* skill.all()) // kilocode_change
|
|
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 = discovered(yield* skill.all()) // kilocode_change
|
|
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 = discovered(yield* skill.all()) // kilocode_change
|
|
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 = discovered(yield* skill.all()) // kilocode_change
|
|
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, ".opencode", "skill", "opencode-skill", "SKILL.md"),
|
|
`---
|
|
name: opencode-skill
|
|
description: A skill in the .opencode/skill directory.
|
|
---
|
|
|
|
# OpenCode Skill
|
|
`,
|
|
),
|
|
]),
|
|
)
|
|
|
|
const skill = yield* Skill.Service
|
|
const list = discovered(yield* skill.all()) // kilocode_change
|
|
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, ".opencode", "skill", "agent-skill", "SKILL.md"),
|
|
`---
|
|
name: opencode-skill
|
|
description: A skill in the .opencode/skill directory.
|
|
---
|
|
|
|
# OpenCode Skill
|
|
`,
|
|
),
|
|
Bun.write(
|
|
path.join(dir, ".opencode", "skills", "agent-skill", "SKILL.md"),
|
|
`---
|
|
name: opencode-skill
|
|
description: A skill in the .opencode/skills directory.
|
|
---
|
|
|
|
# OpenCode Skill
|
|
`,
|
|
),
|
|
]),
|
|
)
|
|
|
|
const skill = yield* Skill.Service
|
|
expect((yield* skill.dirs()).length).toBe(4)
|
|
}),
|
|
{ git: true },
|
|
),
|
|
)
|
|
})
|