Files
oh-my-opencode/src/tools/skill/tools.factory.test.ts
T

133 lines
4.1 KiB
TypeScript

/// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
import type { ToolContext } from "@opencode-ai/plugin/tool"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import * as skillContent from "../../features/opencode-skill-loader/skill-content"
const discoverCommandsSync = mock(() => [])
mock.module("../slashcommand/command-discovery", () => ({
discoverCommandsSync,
}))
function createMockSkill(name: string): LoadedSkill {
return {
name,
definition: {
name,
description: `Test skill ${name}`,
template: `Test skill template for ${name}`,
},
scope: "config",
}
}
async function flushMicrotasks(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
}
const loadedSkill = createMockSkill("lazy-skill")
const getAllSkills = mock(async () => [loadedSkill])
const clearSkillCache = mock(() => {})
const mockContext: ToolContext = {
sessionID: "test-session",
messageID: "msg-1",
agent: "test-agent",
directory: "/test",
worktree: "/test",
abort: new AbortController().signal,
metadata: () => {},
ask: async () => {},
}
function createMockContext(sessionID: string): ToolContext {
return {
...mockContext,
sessionID,
}
}
beforeEach(() => {
spyOn(skillContent, "getAllSkills").mockImplementation(getAllSkills)
spyOn(skillContent, "clearSkillCache").mockImplementation(clearSkillCache)
})
afterEach(async () => {
await flushMicrotasks()
mock.restore()
})
describe("createSkillTool", () => {
it("delays command discovery until the description getter is accessed", async () => {
// given
const baselineDiscoverCommandsSyncCalls = discoverCommandsSync.mock.calls.length
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
// then
expect(discoverCommandsSync.mock.calls.length).toBe(baselineDiscoverCommandsSyncCalls)
void skillTool.description
await flushMicrotasks()
expect(discoverCommandsSync.mock.calls.length).toBe(baselineDiscoverCommandsSyncCalls + 1)
})
it("delays skill loading until execute is invoked", async () => {
// given
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
// then
expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls)
await skillTool.execute({ name: "lazy-skill" }, mockContext)
expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls + 1)
})
it("clears the shared skill cache once on first execute in a session", async () => {
// given
const baselineClearSkillCacheCalls = clearSkillCache.mock.calls.length
const sessionContext = createMockContext("session-clear-once")
// when
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
void skillTool.description
await flushMicrotasks()
await skillTool.execute({ name: "lazy-skill" }, sessionContext)
await skillTool.execute({ name: "lazy-skill" }, sessionContext)
// then
expect(clearSkillCache.mock.calls.length).toBe(baselineClearSkillCacheCalls + 1)
})
it("clears the skill discovery cache once per session", async () => {
// given
const baselineClearSkillCacheCalls = clearSkillCache.mock.calls.length
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
const sessionAContext = createMockContext("session-a")
const sessionBContext = createMockContext("session-b")
const { createSkillTool } = await import("./tools")
const skillTool = createSkillTool({})
// when
await skillTool.execute({ name: "lazy-skill" }, sessionAContext)
await skillTool.execute({ name: "lazy-skill" }, sessionAContext)
await skillTool.execute({ name: "lazy-skill" }, sessionBContext)
await skillTool.execute({ name: "lazy-skill" }, sessionBContext)
// then
expect(clearSkillCache.mock.calls.length).toBe(baselineClearSkillCacheCalls + 2)
expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls + 4)
})
})