From dfb4f8abc3b22ca544aa6fae71f56e1d2b930d5f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 13:50:14 +0900 Subject: [PATCH] feat(skill-loader): add skill-content resolver for agent skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add resolveMultipleSkills() for resolving skill content to prepend to agent prompts. Includes test coverage for resolution logic. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/features/opencode-skill-loader/index.ts | 1 + .../skill-content.test.ts | 111 ++++++++++++++++++ .../opencode-skill-loader/skill-content.ts | 29 +++++ 3 files changed, 141 insertions(+) create mode 100644 src/features/opencode-skill-loader/skill-content.test.ts create mode 100644 src/features/opencode-skill-loader/skill-content.ts diff --git a/src/features/opencode-skill-loader/index.ts b/src/features/opencode-skill-loader/index.ts index 027427a7a..cb4646289 100644 --- a/src/features/opencode-skill-loader/index.ts +++ b/src/features/opencode-skill-loader/index.ts @@ -1,3 +1,4 @@ export * from "./types" export * from "./loader" export * from "./merger" +export * from "./skill-content" diff --git a/src/features/opencode-skill-loader/skill-content.test.ts b/src/features/opencode-skill-loader/skill-content.test.ts new file mode 100644 index 000000000..66b432b6d --- /dev/null +++ b/src/features/opencode-skill-loader/skill-content.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from "bun:test" +import { resolveSkillContent, resolveMultipleSkills } from "./skill-content" + +describe("resolveSkillContent", () => { + it("should return template for existing skill", () => { + // #given: builtin skills with 'frontend-ui-ux' skill + // #when: resolving content for 'frontend-ui-ux' + const result = resolveSkillContent("frontend-ui-ux") + + // #then: returns template string + expect(result).not.toBeNull() + expect(typeof result).toBe("string") + expect(result).toContain("Role: Designer-Turned-Developer") + }) + + it("should return template for 'playwright' skill", () => { + // #given: builtin skills with 'playwright' skill + // #when: resolving content for 'playwright' + const result = resolveSkillContent("playwright") + + // #then: returns template string + expect(result).not.toBeNull() + expect(typeof result).toBe("string") + expect(result).toContain("Playwright Browser Automation") + }) + + it("should return null for non-existent skill", () => { + // #given: builtin skills without 'nonexistent' skill + // #when: resolving content for 'nonexistent' + const result = resolveSkillContent("nonexistent") + + // #then: returns null + expect(result).toBeNull() + }) + + it("should return null for empty string", () => { + // #given: builtin skills + // #when: resolving content for empty string + const result = resolveSkillContent("") + + // #then: returns null + expect(result).toBeNull() + }) +}) + +describe("resolveMultipleSkills", () => { + it("should resolve all existing skills", () => { + // #given: list of existing skill names + const skillNames = ["frontend-ui-ux", "playwright"] + + // #when: resolving multiple skills + const result = resolveMultipleSkills(skillNames) + + // #then: all skills resolved, none not found + expect(result.resolved.size).toBe(2) + expect(result.notFound).toEqual([]) + expect(result.resolved.get("frontend-ui-ux")).toContain("Designer-Turned-Developer") + expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation") + }) + + it("should handle partial success - some skills not found", () => { + // #given: list with existing and non-existing skills + const skillNames = ["frontend-ui-ux", "nonexistent", "playwright", "another-missing"] + + // #when: resolving multiple skills + const result = resolveMultipleSkills(skillNames) + + // #then: resolves existing skills, lists not found skills + expect(result.resolved.size).toBe(2) + expect(result.notFound).toEqual(["nonexistent", "another-missing"]) + expect(result.resolved.get("frontend-ui-ux")).toContain("Designer-Turned-Developer") + expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation") + }) + + it("should handle empty array", () => { + // #given: empty skill names list + const skillNames: string[] = [] + + // #when: resolving multiple skills + const result = resolveMultipleSkills(skillNames) + + // #then: returns empty resolved and notFound + expect(result.resolved.size).toBe(0) + expect(result.notFound).toEqual([]) + }) + + it("should handle all skills not found", () => { + // #given: list of non-existing skills + const skillNames = ["skill-one", "skill-two", "skill-three"] + + // #when: resolving multiple skills + const result = resolveMultipleSkills(skillNames) + + // #then: no skills resolved, all in notFound + expect(result.resolved.size).toBe(0) + expect(result.notFound).toEqual(["skill-one", "skill-two", "skill-three"]) + }) + + it("should preserve skill order in resolved map", () => { + // #given: list of skill names in specific order + const skillNames = ["playwright", "frontend-ui-ux"] + + // #when: resolving multiple skills + const result = resolveMultipleSkills(skillNames) + + // #then: map contains skills with expected keys + expect(result.resolved.has("playwright")).toBe(true) + expect(result.resolved.has("frontend-ui-ux")).toBe(true) + expect(result.resolved.size).toBe(2) + }) +}) diff --git a/src/features/opencode-skill-loader/skill-content.ts b/src/features/opencode-skill-loader/skill-content.ts new file mode 100644 index 000000000..a6a058a57 --- /dev/null +++ b/src/features/opencode-skill-loader/skill-content.ts @@ -0,0 +1,29 @@ +import { createBuiltinSkills } from "../builtin-skills/skills" + +export function resolveSkillContent(skillName: string): string | null { + const skills = createBuiltinSkills() + const skill = skills.find((s) => s.name === skillName) + return skill?.template ?? null +} + +export function resolveMultipleSkills(skillNames: string[]): { + resolved: Map + notFound: string[] +} { + const skills = createBuiltinSkills() + const skillMap = new Map(skills.map((s) => [s.name, s.template])) + + const resolved = new Map() + const notFound: string[] = [] + + for (const name of skillNames) { + const template = skillMap.get(name) + if (template) { + resolved.set(name, template) + } else { + notFound.push(name) + } + } + + return { resolved, notFound } +}