11import { describe , expect , test } from "bun:test"
22import path from "path"
33import { loadClaudePlugin } from "../src/parsers/claude"
4- import { convertClaudeToOpenCode } from "../src/converters/claude-to-opencode"
4+ import { convertClaudeToOpenCode , transformSkillContentForOpenCode } from "../src/converters/claude-to-opencode"
55import { parseFrontmatter } from "../src/utils/frontmatter"
66import type { ClaudePlugin } from "../src/types/claude"
77
@@ -61,7 +61,7 @@ describe("convertClaudeToOpenCode", () => {
6161 test ( "normalizes models and infers temperature" , async ( ) => {
6262 const plugin = await loadClaudePlugin ( fixtureRoot )
6363 const bundle = convertClaudeToOpenCode ( plugin , {
64- agentMode : "subagent " ,
64+ agentMode : "primary " ,
6565 inferTemperature : true ,
6666 permissions : "none" ,
6767 } )
@@ -78,7 +78,7 @@ describe("convertClaudeToOpenCode", () => {
7878 expect ( commandParsed . data . model ) . toBe ( "openai/gpt-4o" )
7979 } )
8080
81- test ( "resolves bare Claude model aliases to full IDs " , ( ) => {
81+ test ( "resolves bare Claude model aliases for primary agents " , ( ) => {
8282 const plugin : ClaudePlugin = {
8383 root : "/tmp/plugin" ,
8484 manifest : { name : "fixture" , version : "1.0.0" } ,
@@ -96,7 +96,7 @@ describe("convertClaudeToOpenCode", () => {
9696 }
9797
9898 const bundle = convertClaudeToOpenCode ( plugin , {
99- agentMode : "subagent " ,
99+ agentMode : "primary " ,
100100 inferTemperature : false ,
101101 permissions : "none" ,
102102 } )
@@ -107,6 +107,91 @@ describe("convertClaudeToOpenCode", () => {
107107 expect ( parsed . data . model ) . toBe ( "anthropic/claude-haiku-4-5" )
108108 } )
109109
110+ test ( "omits model for subagents to allow provider inheritance (#477)" , ( ) => {
111+ const plugin : ClaudePlugin = {
112+ root : "/tmp/plugin" ,
113+ manifest : { name : "fixture" , version : "1.0.0" } ,
114+ agents : [
115+ {
116+ name : "cheap-agent" ,
117+ description : "Agent using bare alias" ,
118+ body : "Test agent." ,
119+ sourcePath : "/tmp/plugin/agents/cheap-agent.md" ,
120+ model : "haiku" ,
121+ } ,
122+ ] ,
123+ commands : [ ] ,
124+ skills : [ ] ,
125+ }
126+
127+ const bundle = convertClaudeToOpenCode ( plugin , {
128+ agentMode : "subagent" ,
129+ inferTemperature : false ,
130+ permissions : "none" ,
131+ } )
132+
133+ const agent = bundle . agents . find ( ( a ) => a . name === "cheap-agent" )
134+ expect ( agent ) . toBeDefined ( )
135+ const parsed = parseFrontmatter ( agent ! . content )
136+ expect ( parsed . data . model ) . toBeUndefined ( )
137+ } )
138+
139+ test ( "omits model when agent has no model field regardless of mode" , ( ) => {
140+ const plugin : ClaudePlugin = {
141+ root : "/tmp/plugin" ,
142+ manifest : { name : "fixture" , version : "1.0.0" } ,
143+ agents : [
144+ {
145+ name : "no-model-agent" ,
146+ description : "Agent without model" ,
147+ body : "Test agent." ,
148+ sourcePath : "/tmp/plugin/agents/no-model-agent.md" ,
149+ } ,
150+ ] ,
151+ commands : [ ] ,
152+ skills : [ ] ,
153+ }
154+
155+ for ( const mode of [ "primary" , "subagent" ] as const ) {
156+ const bundle = convertClaudeToOpenCode ( plugin , {
157+ agentMode : mode ,
158+ inferTemperature : false ,
159+ permissions : "none" ,
160+ } )
161+ const agent = bundle . agents . find ( ( a ) => a . name === "no-model-agent" )
162+ const parsed = parseFrontmatter ( agent ! . content )
163+ expect ( parsed . data . model ) . toBeUndefined ( )
164+ }
165+ } )
166+
167+ test ( "omits model: inherit even in primary mode" , ( ) => {
168+ const plugin : ClaudePlugin = {
169+ root : "/tmp/plugin" ,
170+ manifest : { name : "fixture" , version : "1.0.0" } ,
171+ agents : [
172+ {
173+ name : "inherit-agent" ,
174+ description : "Agent with inherit model" ,
175+ body : "Test agent." ,
176+ sourcePath : "/tmp/plugin/agents/inherit-agent.md" ,
177+ model : "inherit" ,
178+ } ,
179+ ] ,
180+ commands : [ ] ,
181+ skills : [ ] ,
182+ }
183+
184+ const bundle = convertClaudeToOpenCode ( plugin , {
185+ agentMode : "primary" ,
186+ inferTemperature : false ,
187+ permissions : "none" ,
188+ } )
189+
190+ const agent = bundle . agents . find ( ( a ) => a . name === "inherit-agent" )
191+ const parsed = parseFrontmatter ( agent ! . content )
192+ expect ( parsed . data . model ) . toBeUndefined ( )
193+ } )
194+
110195 test ( "converts hooks into plugin file" , async ( ) => {
111196 const plugin = await loadClaudePlugin ( fixtureRoot )
112197 const bundle = convertClaudeToOpenCode ( plugin , {
@@ -319,3 +404,91 @@ Run \`/compound-engineering-setup\` to create a settings file.`,
319404 expect ( parsed . body ) . toContain ( "Do the thing" )
320405 } )
321406} )
407+
408+ describe ( "transformSkillContentForOpenCode" , ( ) => {
409+ test ( "rewrites 3-segment FQ agent names to flat names" , ( ) => {
410+ const input = "- `compound-engineering:document-review:coherence-reviewer`"
411+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( "- `coherence-reviewer`" )
412+ } )
413+
414+ test ( "rewrites multiple FQ agent refs in one block" , ( ) => {
415+ const input = [
416+ "- `compound-engineering:document-review:coherence-reviewer`" ,
417+ "- `compound-engineering:document-review:feasibility-reviewer`" ,
418+ "- `compound-engineering:review:security-sentinel`" ,
419+ ] . join ( "\n" )
420+ const result = transformSkillContentForOpenCode ( input )
421+ expect ( result ) . toContain ( "- `coherence-reviewer`" )
422+ expect ( result ) . toContain ( "- `feasibility-reviewer`" )
423+ expect ( result ) . toContain ( "- `security-sentinel`" )
424+ expect ( result ) . not . toContain ( "compound-engineering:" )
425+ } )
426+
427+ test ( "preserves 2-segment skill references" , ( ) => {
428+ const input = 'load the `compound-engineering:document-review` skill'
429+ // 2-segment refs are skill names, not agent names — left unchanged
430+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
431+ } )
432+
433+ test ( "rewrites .claude/ paths to .opencode/" , ( ) => {
434+ const input = "Read `.claude/config.json`"
435+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( "Read `.opencode/config.json`" )
436+ } )
437+
438+ test ( "rewrites ~/. claude/ paths to ~/.config/opencode/" , ( ) => {
439+ const input = "Look in `~/.claude/plugins/`"
440+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( "Look in `~/.config/opencode/plugins/`" )
441+ } )
442+
443+ test ( "handles FQ names in JSON-like contexts" , ( ) => {
444+ const input = ' subagent_type: "compound-engineering:review:security-sentinel",'
445+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe (
446+ ' subagent_type: "security-sentinel",'
447+ )
448+ } )
449+
450+ test ( "does not match URLs or non-agent colon patterns" , ( ) => {
451+ const cases = [
452+ "Visit https://example.com/path" ,
453+ "Use http://localhost:8080/api" ,
454+ "Set font-size: 12px; color: red;" ,
455+ "Time is 10:30:45 UTC" ,
456+ 'key: "value"' ,
457+ ]
458+ for ( const input of cases ) {
459+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
460+ }
461+ } )
462+
463+ test ( "rewrites FQ names from any plugin namespace" , ( ) => {
464+ const input = "- `other-plugin:category:my-agent`"
465+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( "- `my-agent`" )
466+ } )
467+
468+ test ( "preserves bare agent names (no namespace)" , ( ) => {
469+ const input = "Use `coherence-reviewer` for review."
470+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
471+ } )
472+
473+ test ( "preserves 2-segment plugin:agent names (no category)" , ( ) => {
474+ const input = "Spawn `compound-engineering:coherence-reviewer` as subagent."
475+ // 2-segment names could be skill refs or flat agent refs — not rewritten
476+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
477+ } )
478+
479+ test ( "does not partially rewrite 4-segment colon patterns" , ( ) => {
480+ const input = "`a:b:c:d`"
481+ // Without the lookahead, this would become `c:d` — a broken partial rewrite
482+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
483+ } )
484+
485+ test ( "preserves 3-segment slash commands" , ( ) => {
486+ const cases = [
487+ "Run `/team:ops:deploy` to deploy." ,
488+ "Use /compound-engineering:review:check after changes." ,
489+ ]
490+ for ( const input of cases ) {
491+ expect ( transformSkillContentForOpenCode ( input ) ) . toBe ( input )
492+ }
493+ } )
494+ } )
0 commit comments