Files
oh-my-opencode/src/agents/builtin-agents/available-skills.test.ts
T
Z 088693697a feat: filter agent-restricted skills from prompts and tool description
Skills with an `agent` frontmatter field are intended for a specific
agent. Previously they still appeared in:
- every agent's system prompt (via `buildAvailableSkills`)
- the `skill` tool's `<available_items>` description visible to all agents

This wasted tokens and could mislead agents into attempting calls that
would be rejected at execution time.

Changes:
- `buildAvailableSkills`: new optional `agentName` parameter; when
  provided, skills whose `definition.agent` does not match are excluded
- `builtin-agents.ts`: pass per-agent name to `buildAvailableSkills`
  for sisyphus, hephaestus, and atlas, so each agent's prompt only
  lists the skills it is allowed to use
- `createSkillTool` (`tools.ts`): exclude agent-restricted skills from
  both the eager and lazy description builds, keeping the shared tool
  description free of skills the current agent cannot access

Execution-time enforcement (throwing on mismatch) is unchanged; this
change adds the earlier, description-level visibility gate.

Tests: new `available-skills.test.ts` (5 cases) + 3 new cases in
`tools.test.ts` covering the description-filter and execute paths.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-18 17:02:37 +08:00

109 lines
3.3 KiB
TypeScript

/// <reference types="bun-types" />
import { describe, expect, it, test } from "bun:test"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import { buildAvailableSkills } from "./available-skills"
type DiscoveredSkills = Parameters<typeof buildAvailableSkills>[0]
function makeSkill(name: string, agent?: string): LoadedSkill {
return {
name,
resolvedPath: `/test/skills/${name}`,
definition: {
name,
description: `Skill ${name}`,
template: "",
agent,
},
scope: "user",
}
}
describe("buildAvailableSkills", () => {
test("includes team-mode when team mode is enabled", () => {
// given
const discoveredSkills: DiscoveredSkills = []
// when
const availableSkills = buildAvailableSkills(discoveredSkills, undefined, undefined, true)
// then
expect(availableSkills.some((skill) => skill.name === "team-mode")).toBe(true)
})
test("excludes team-mode when team mode is disabled", () => {
// given
const discoveredSkills: DiscoveredSkills = []
// when
const availableSkills = buildAvailableSkills(discoveredSkills, undefined, undefined, false)
// then
expect(availableSkills.some((skill) => skill.name === "team-mode")).toBe(false)
})
})
describe("buildAvailableSkills - agentName filtering", () => {
it("includes agent-restricted skill when agentName is not provided (backward compat)", () => {
// given
const skills = [makeSkill("oracle-only", "oracle")]
// when
const result = buildAvailableSkills(skills, undefined, undefined, undefined, undefined)
// then: no agentName → no filtering, skill is included
expect(result.map((s) => s.name)).toContain("oracle-only")
})
it("includes skill when agentName matches the skill's agent field", () => {
// given
const skills = [makeSkill("sisyphus-only", "sisyphus")]
// when
const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
// then: matching agent → included
expect(result.map((s) => s.name)).toContain("sisyphus-only")
})
it("excludes skill when agentName does not match the skill's agent field", () => {
// given
const skills = [makeSkill("sisyphus-only", "sisyphus")]
// when
const result = buildAvailableSkills(skills, undefined, undefined, undefined, "oracle")
// then: wrong agent → excluded
expect(result.map((s) => s.name)).not.toContain("sisyphus-only")
})
it("includes skill with no agent field regardless of agentName", () => {
// given
const skills = [makeSkill("public-skill")]
// when
const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
// then: no agent restriction → always included
expect(result.map((s) => s.name)).toContain("public-skill")
})
it("filters per-agent while keeping public skills", () => {
// given
const skills = [
makeSkill("public-skill"),
makeSkill("sisyphus-only", "sisyphus"),
makeSkill("oracle-only", "oracle"),
]
// when
const result = buildAvailableSkills(skills, undefined, undefined, undefined, "sisyphus")
// then
const names = result.map((s) => s.name)
expect(names).toContain("public-skill")
expect(names).toContain("sisyphus-only")
expect(names).not.toContain("oracle-only")
})
})