import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Cause, Effect, Exit, Layer } from "effect" import { afterEach, describe, expect } from "bun:test" import path from "path" import type { Permission } from "../../src/permission" import type { Tool } from "@/tool/tool" import { SkillTool } from "../../src/tool/skill" import { ToolRegistry } from "@/tool/registry" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" import { testEffect } from "../lib/effect" const baseCtx: Omit = { sessionID: SessionID.make("ses_test"), messageID: MessageID.make("msg_test"), callID: "", agent: "build", abort: AbortSignal.any([]), messages: [], metadata: () => Effect.void, } afterEach(async () => { await disposeAllInstances() }) const it = testEffect(LayerNode.compile(LayerNode.group([ToolRegistry.node, CrossSpawnSpawner.node, Ripgrep.node]))) describe("tool.skill", () => { it.instance("execute returns skill content block with files", () => Effect.gen(function* () { const dir = (yield* TestInstance).directory const skill = path.join(dir, ".opencode", "skill", "tool-skill") yield* Effect.promise(() => Bun.write( path.join(skill, "SKILL.md"), `--- name: tool-skill description: Skill for tool tests. --- # Tool Skill Use this skill. `, ), ) yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo")) const home = process.env.OPENCODE_TEST_HOME process.env.OPENCODE_TEST_HOME = dir yield* Effect.addFinalizer(() => Effect.sync(() => { process.env.OPENCODE_TEST_HOME = home }), ) const registry = yield* ToolRegistry.Service const agent = { name: "build", mode: "primary" as const, permission: [], options: {} } const tool = (yield* registry.tools({ providerID: "opencode" as any, modelID: "gpt-5" as any, agent, })).find((tool) => tool.id === SkillTool.id) if (!tool) throw new Error("Skill tool not found") expect(tool.description).not.toContain("tool-skill") expect(tool.description).not.toContain("Skill for tool tests.") const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: (req) => Effect.sync(() => { requests.push(req) }), } const result = yield* tool.execute({ name: "tool-skill" }, ctx) const file = path.resolve(skill, "scripts", "demo.txt") expect(requests.length).toBe(1) expect(requests[0].permission).toBe("skill") expect(requests[0].patterns).toContain("tool-skill") expect(requests[0].always).toContain("tool-skill") expect(result.metadata.dir).toBe(skill) expect(result.output).toContain(``) expect(result.output).toContain(`Base directory for this skill: ${skill}`) expect(result.output).toContain(`${file}`) }), ) it.instance("execute preserves not found message", () => Effect.gen(function* () { const dir = (yield* TestInstance).directory const home = process.env.OPENCODE_TEST_HOME process.env.OPENCODE_TEST_HOME = dir yield* Effect.addFinalizer(() => Effect.sync(() => { process.env.OPENCODE_TEST_HOME = home }), ) const registry = yield* ToolRegistry.Service const agent = { name: "build", mode: "primary" as const, permission: [], options: {} } const tool = (yield* registry.tools({ providerID: "opencode" as any, modelID: "gpt-5" as any, agent, })).find((tool) => tool.id === SkillTool.id) if (!tool) throw new Error("Skill tool not found") const exit = yield* tool .execute( { name: "missing-skill" }, { ...baseCtx, ask: () => Effect.void, }, ) .pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) if (Exit.isFailure(exit)) { const error = Cause.squash(exit.cause) expect(error).toBeInstanceOf(Error) if (error instanceof Error) expect(error.message).toContain('Skill "missing-skill" not found.') } }), ) })