1- import { describe , expect , test } from "bun:test"
1+ import { afterEach , describe , expect , test } from "bun:test"
22import path from "path"
33import { pathToFileURL } from "url"
44import type { PermissionNext } from "../../src/permission/next"
@@ -7,6 +7,9 @@ import { Instance } from "../../src/project/instance"
77import { SkillTool } from "../../src/tool/skill"
88import { tmpdir } from "../fixture/fixture"
99import { SessionID , MessageID } from "../../src/session/schema"
10+ import { resetSkillSelectorCache , selectSkillsWithLLM , type SkillSelectorDeps } from "../../src/altimate/skill-selector"
11+ import type { Skill } from "../../src/skill"
12+ import type { LanguageModelV2 } from "@openrouter/ai-sdk-provider"
1013
1114const baseCtx : Omit < Tool . Context , "ask" > = {
1215 sessionID : SessionID . make ( "ses_test" ) ,
@@ -18,6 +21,25 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
1821 metadata : ( ) => { } ,
1922}
2023
24+ const FAKE_MODEL = { modelId : "test" , provider : "test" } as unknown as LanguageModelV2
25+
26+ /** Pre-populate the skill selector cache with a specific subset */
27+ function seedCache ( skillNames : string [ ] ) {
28+ resetSkillSelectorCache ( )
29+ const skills = skillNames . map ( ( name ) => ( {
30+ name,
31+ description : `Skill: ${ name } ` ,
32+ location : `/fake/${ name } /SKILL.md` ,
33+ content : `# ${ name } ` ,
34+ } ) ) as Skill . Info [ ]
35+ // Call with mock deps that return exactly these names → populates the cache
36+ const deps : SkillSelectorDeps = {
37+ resolveModel : async ( ) => FAKE_MODEL ,
38+ generate : async ( ) => ( { object : { selected : skillNames } } ) ,
39+ }
40+ return selectSkillsWithLLM ( skills , undefined , deps )
41+ }
42+
2143describe ( "tool.skill" , ( ) => {
2244 test ( "description lists skill location URL" , async ( ) => {
2345 await using tmp = await tmpdir ( {
@@ -46,14 +68,19 @@ description: Skill for tool tests.
4668 fn : async ( ) => {
4769 const tool = await SkillTool . init ( )
4870 const skillPath = path . join ( tmp . path , ".opencode" , "skill" , "tool-skill" , "SKILL.md" )
49- expect ( tool . description ) . toContain ( `**tool-skill**: Skill for tool tests.` )
71+ expect ( tool . description ) . toContain ( `<name>tool-skill</name>` )
72+ expect ( tool . description ) . toContain ( `<description>Skill for tool tests.</description>` )
5073 } ,
5174 } )
5275 } finally {
5376 process . env . OPENCODE_TEST_HOME = home
5477 }
5578 } )
5679
80+ afterEach ( ( ) => {
81+ resetSkillSelectorCache ( )
82+ } )
83+
5784 test ( "execute returns skill content block with files" , async ( ) => {
5885 await using tmp = await tmpdir ( {
5986 git : true ,
@@ -110,4 +137,92 @@ Use this skill.
110137 process . env . OPENCODE_TEST_HOME = home
111138 }
112139 } )
140+
141+ test ( "env_fingerprint_skill_selection: false → selector bypassed, all skills shown" , async ( ) => {
142+ // Pre-populate cache with only "skill-alpha" — if selector is called, it returns this cached subset
143+ await seedCache ( [ "skill-alpha" ] )
144+
145+ await using tmp = await tmpdir ( {
146+ git : true ,
147+ config : {
148+ experimental : {
149+ env_fingerprint_skill_selection : false ,
150+ } ,
151+ } ,
152+ init : async ( dir ) => {
153+ for ( const name of [ "skill-alpha" , "skill-beta" ] ) {
154+ await Bun . write (
155+ path . join ( dir , ".opencode" , "skill" , name , "SKILL.md" ) ,
156+ `---\nname: ${ name } \ndescription: Test ${ name } \n---\n# ${ name } \n` ,
157+ )
158+ }
159+ } ,
160+ } )
161+
162+ const home = process . env . OPENCODE_TEST_HOME
163+ process . env . OPENCODE_TEST_HOME = tmp . path
164+
165+ try {
166+ await Instance . provide ( {
167+ directory : tmp . path ,
168+ fn : async ( ) => {
169+ const tool = await SkillTool . init ( )
170+ // Selector was bypassed → both skills appear (from Skill.available, not cache)
171+ expect ( tool . description ) . toContain ( "<name>skill-alpha</name>" )
172+ expect ( tool . description ) . toContain ( "<name>skill-beta</name>" )
173+ } ,
174+ } )
175+ } finally {
176+ process . env . OPENCODE_TEST_HOME = home
177+ }
178+ } )
179+
180+ test ( "env_fingerprint_skill_selection: true → selector called, uses cached subset" , async ( ) => {
181+ await using tmp = await tmpdir ( {
182+ git : true ,
183+ config : {
184+ experimental : {
185+ env_fingerprint_skill_selection : true ,
186+ } ,
187+ } ,
188+ init : async ( dir ) => {
189+ for ( const name of [ "skill-alpha" , "skill-beta" ] ) {
190+ await Bun . write (
191+ path . join ( dir , ".opencode" , "skill" , name , "SKILL.md" ) ,
192+ `---\nname: ${ name } \ndescription: Test ${ name } \n---\n# ${ name } \n` ,
193+ )
194+ }
195+ } ,
196+ } )
197+
198+ // Pre-populate cache with only "skill-alpha" AFTER tmpdir so location matches
199+ const alphaLocation = path . join ( tmp . path , ".opencode" , "skill" , "skill-alpha" , "SKILL.md" )
200+ resetSkillSelectorCache ( )
201+ const deps : SkillSelectorDeps = {
202+ resolveModel : async ( ) => FAKE_MODEL ,
203+ generate : async ( ) => ( { object : { selected : [ "skill-alpha" ] } } ) ,
204+ }
205+ await selectSkillsWithLLM (
206+ [ { name : "skill-alpha" , description : "Test skill-alpha" , location : alphaLocation , content : "# skill-alpha" } as Skill . Info ] ,
207+ undefined ,
208+ deps ,
209+ )
210+
211+ const home = process . env . OPENCODE_TEST_HOME
212+ process . env . OPENCODE_TEST_HOME = tmp . path
213+
214+ try {
215+ await Instance . provide ( {
216+ directory : tmp . path ,
217+ fn : async ( ) => {
218+ const tool = await SkillTool . init ( )
219+ // Selector was called → returns cached subset (only skill-alpha)
220+ expect ( tool . description ) . toContain ( "<name>skill-alpha</name>" )
221+ expect ( tool . description ) . not . toContain ( "<name>skill-beta</name>" )
222+ } ,
223+ } )
224+ } finally {
225+ process . env . OPENCODE_TEST_HOME = home
226+ }
227+ } )
113228} )
0 commit comments