From 1528e46faa77f49ae5be1ae114676d739c7e9157 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Mar 2026 11:16:24 +0900 Subject: [PATCH] fix(skill-context): gate discovered browser skills by provider Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/plugin/skill-context.test.ts | 88 ++++++++++++++++++++++++++++++++ src/plugin/skill-context.ts | 45 ++++++++++++++-- 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/plugin/skill-context.test.ts diff --git a/src/plugin/skill-context.test.ts b/src/plugin/skill-context.test.ts new file mode 100644 index 0000000000..4c80b2b610 --- /dev/null +++ b/src/plugin/skill-context.test.ts @@ -0,0 +1,88 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test" +import { mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +import { OhMyOpenCodeConfigSchema } from "../config" +import * as mcpLoader from "../features/claude-code-mcp-loader" +import * as skillLoader from "../features/opencode-skill-loader" +import { createSkillContext } from "./skill-context" + +describe("createSkillContext", () => { + const testDirectory = join(tmpdir(), `skill-context-test-${Date.now()}`) + + beforeEach(() => { + mkdirSync(testDirectory, { recursive: true }) + }) + + afterEach(() => { + rmSync(testDirectory, { recursive: true, force: true }) + }) + + it("excludes discovered playwright skill when browser provider is agent-browser", async () => { + // given + const discoveredPlaywrightDir = join(testDirectory, ".claude", "skills", "playwright") + mkdirSync(discoveredPlaywrightDir, { recursive: true }) + writeFileSync( + join(discoveredPlaywrightDir, "SKILL.md"), + [ + "---", + "name: playwright", + "description: Discovered playwright skill", + "---", + "Discovered playwright body.", + "", + ].join("\n"), + ) + + const discoverConfigSourceSkillsSpy = spyOn( + skillLoader, + "discoverConfigSourceSkills", + ).mockResolvedValue([]) + const discoverUserClaudeSkillsSpy = spyOn( + skillLoader, + "discoverUserClaudeSkills", + ).mockResolvedValue([]) + const discoverOpencodeGlobalSkillsSpy = spyOn( + skillLoader, + "discoverOpencodeGlobalSkills", + ).mockResolvedValue([]) + const discoverProjectAgentsSkillsSpy = spyOn( + skillLoader, + "discoverProjectAgentsSkills", + ).mockResolvedValue([]) + const discoverGlobalAgentsSkillsSpy = spyOn( + skillLoader, + "discoverGlobalAgentsSkills", + ).mockResolvedValue([]) + const getSystemMcpServerNamesSpy = spyOn( + mcpLoader, + "getSystemMcpServerNames", + ).mockReturnValue(new Set()) + + const pluginConfig = OhMyOpenCodeConfigSchema.parse({ + browser_automation_engine: { provider: "agent-browser" }, + }) + + try { + // when + const result = await createSkillContext({ + directory: testDirectory, + pluginConfig, + }) + + // then + expect(result.browserProvider).toBe("agent-browser") + expect(result.mergedSkills.some((skill) => skill.name === "agent-browser")).toBe(true) + expect(result.mergedSkills.some((skill) => skill.name === "playwright")).toBe(false) + expect(result.availableSkills.some((skill) => skill.name === "playwright")).toBe(false) + } finally { + discoverConfigSourceSkillsSpy.mockRestore() + discoverUserClaudeSkillsSpy.mockRestore() + discoverOpencodeGlobalSkillsSpy.mockRestore() + discoverProjectAgentsSkillsSpy.mockRestore() + discoverGlobalAgentsSkillsSpy.mockRestore() + getSystemMcpServerNamesSpy.mockRestore() + } + }) +}) diff --git a/src/plugin/skill-context.ts b/src/plugin/skill-context.ts index 5f3bd1717a..05a72d688f 100644 --- a/src/plugin/skill-context.ts +++ b/src/plugin/skill-context.ts @@ -26,12 +26,27 @@ export type SkillContext = { disabledSkills: Set } +const PROVIDER_GATED_SKILL_NAMES = new Set(["agent-browser", "playwright"]) + function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] { if (scope === "user" || scope === "opencode") return "user" if (scope === "project" || scope === "opencode-project") return "project" return "plugin" } +function filterProviderGatedSkills( + skills: LoadedSkill[], + browserProvider: BrowserAutomationProvider, +): LoadedSkill[] { + return skills.filter((skill) => { + if (!PROVIDER_GATED_SKILL_NAMES.has(skill.name)) { + return true + } + + return skill.name === browserProvider + }) +} + export async function createSkillContext(args: { directory: string pluginConfig: OhMyOpenCodeConfig @@ -71,14 +86,34 @@ export async function createSkillContext(args: { discoverGlobalAgentsSkills(), ]) + const filteredConfigSourceSkills = filterProviderGatedSkills( + configSourceSkills, + browserProvider, + ) + const filteredUserSkills = filterProviderGatedSkills(userSkills, browserProvider) + const filteredGlobalSkills = filterProviderGatedSkills(globalSkills, browserProvider) + const filteredProjectSkills = filterProviderGatedSkills(projectSkills, browserProvider) + const filteredOpencodeProjectSkills = filterProviderGatedSkills( + opencodeProjectSkills, + browserProvider, + ) + const filteredAgentsProjectSkills = filterProviderGatedSkills( + agentsProjectSkills, + browserProvider, + ) + const filteredAgentsGlobalSkills = filterProviderGatedSkills( + agentsGlobalSkills, + browserProvider, + ) + const mergedSkills = mergeSkills( builtinSkills, pluginConfig.skills, - configSourceSkills, - [...userSkills, ...agentsGlobalSkills], - globalSkills, - [...projectSkills, ...agentsProjectSkills], - opencodeProjectSkills, + filteredConfigSourceSkills, + [...filteredUserSkills, ...filteredAgentsGlobalSkills], + filteredGlobalSkills, + [...filteredProjectSkills, ...filteredAgentsProjectSkills], + filteredOpencodeProjectSkills, { configDir: directory }, )