import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" import { SkillV2 } from "@opencode-ai/core/skill" import { Instructions } from "@opencode-ai/core/instructions" import { SkillGuidance } from "@opencode-ai/core/skill/guidance" import { it } from "../lib/effect" const build = AgentV2.ID.make("build") const effect = SkillV2.Info.make({ name: "effect", description: "Build applications with Effect", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Effect guidance", }) const hidden = SkillV2.Info.make({ name: "hidden", location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")), content: "Undescribed guidance", }) const denied = SkillV2.Info.make({ name: "denied", description: "Must not be advertised", location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")), content: "Denied guidance", }) const manual = SkillV2.Info.make({ name: "manual", description: "Load only when explicitly selected", autoinvoke: false, location: AbsolutePath.make(path.resolve("/skills/manual/SKILL.md")), content: "Manual guidance", }) const layer = (list: () => SkillV2.Info[]) => AppNodeBuilder.build(SkillGuidance.node, [ [SkillV2.node, Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })], ]) describe("SkillGuidance", () => { it.effect("renders described agent skills and reconciles the complete available list", () => { const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [{ action: "skill", resource: "denied", effect: "deny" }], }) let skills = [hidden, denied, manual, effect] return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service const initialized = yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap(Instructions.initialize)) expect(initialized.text).toBe( [ "Skills provide specialized instructions and workflows for specific tasks.", "Use the skill tool to load a skill when a task matches its description.", "", " ", " effect", " Build applications with Effect", " ", "", ].join("\n"), ) expect(initialized.text).not.toContain("manual") skills = [] expect( yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))), ).toMatchObject({ _tag: "Updated", text: "The following skills are no longer available and must not be used: effect.", }) }).pipe(Effect.provide(layer(() => skills))) }) it.effect("announces added and removed skills as deltas without restating the list", () => { const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) const debugging = SkillV2.Info.make({ name: "debugging", description: "Diagnose hard bugs", location: AbsolutePath.make(path.resolve("/skills/debugging/SKILL.md")), content: "Debugging guidance", }) let skills = [effect] return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service const initialized = yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap(Instructions.initialize)) skills = [effect, debugging] const added = yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))) expect(added).toMatchObject({ _tag: "Updated", text: [ "New skills are available in addition to those previously listed:", " ", " debugging", " Diagnose hard bugs", " ", ].join("\n"), }) skills = [debugging] const removed = yield* guidance .load({ id: agent.id, info: agent }) .pipe( Effect.flatMap((context) => Instructions.reconcile(context, added._tag === "Updated" ? added.applied : {})), ) expect(removed).toMatchObject({ _tag: "Updated", text: "The following skills are no longer available and must not be used: effect.", }) }).pipe(Effect.provide(layer(() => skills))) }) it.effect("restates the full skill list when a description changes", () => { const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) let skills = [effect] return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service const initialized = yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap(Instructions.initialize)) skills = [SkillV2.Info.make({ ...effect, description: "Build applications with Effect v4" })] expect( yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))), ).toMatchObject({ _tag: "Updated", text: expect.stringContaining( "The available skills have changed. This list supersedes the previous available skills list.", ), }) }).pipe(Effect.provide(layer(() => skills))) }) it.effect("omits guidance when the selected agent denies all skills", () => { const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [{ action: "skill", resource: "*", effect: "deny" }], }) return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual( { text: "", applied: {}, }, ) }).pipe(Effect.provide(layer(() => [effect]))) }) it.effect("omits guidance when a resource-specific denial follows the global denial", () => { const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "hidden", effect: "deny" }, ], }) return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual( { text: "", applied: {}, }, ) }).pipe(Effect.provide(layer(() => [effect]))) }) it.effect("retains specifically allowed skills after a global denial", () => { const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, ], }) return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service expect( (yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).text, ).toContain("effect") }).pipe(Effect.provide(layer(() => [effect]))) }) it.effect("omits guidance when a specifically allowed skill is denied again", () => { const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, { action: "skill", resource: "effect", effect: "deny" }, ], }) return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service expect(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).toEqual( { text: "", applied: {}, }, ) }).pipe(Effect.provide(layer(() => [effect]))) }) })