@@ -184,3 +184,211 @@ func TestGetToolsetInstructions(t *testing.T) {
184184 })
185185 }
186186}
187+
188+ // =============================================================================
189+ // SPIKE TESTS: InstructionResolver with most-specificity rule
190+ // =============================================================================
191+
192+ func TestInstructionResolver_BasicMatching (t * testing.T ) {
193+ resolver := NewInstructionResolver ([]InstructionRule {
194+ NewInstructionRule ("rule-a" , "Instruction A" , "tool1" ),
195+ NewInstructionRule ("rule-b" , "Instruction B" , "tool2" ),
196+ NewInstructionRule ("rule-c" , "Instruction C" , "tool3" ),
197+ })
198+
199+ tests := []struct {
200+ name string
201+ activeTools []string
202+ expectedIDs []string
203+ expectedInst []string
204+ }{
205+ {
206+ name : "single tool matches single rule" ,
207+ activeTools : []string {"tool1" },
208+ expectedIDs : []string {"rule-a" },
209+ expectedInst : []string {"Instruction A" },
210+ },
211+ {
212+ name : "two tools match two rules" ,
213+ activeTools : []string {"tool1" , "tool2" },
214+ expectedIDs : []string {"rule-a" , "rule-b" },
215+ expectedInst : []string {"Instruction A" , "Instruction B" },
216+ },
217+ {
218+ name : "no matching tools" ,
219+ activeTools : []string {"tool4" },
220+ expectedIDs : []string {},
221+ expectedInst : []string {},
222+ },
223+ {
224+ name : "all tools match all rules" ,
225+ activeTools : []string {"tool1" , "tool2" , "tool3" },
226+ expectedIDs : []string {"rule-a" , "rule-b" , "rule-c" },
227+ expectedInst : []string {"Instruction A" , "Instruction B" , "Instruction C" },
228+ },
229+ }
230+
231+ for _ , tt := range tests {
232+ t .Run (tt .name , func (t * testing.T ) {
233+ ids := resolver .MatchingRuleIDs (tt .activeTools )
234+ instructions := resolver .ResolveInstructions (tt .activeTools )
235+
236+ if len (ids ) != len (tt .expectedIDs ) {
237+ t .Errorf ("Expected %d rule IDs, got %d: %v" , len (tt .expectedIDs ), len (ids ), ids )
238+ }
239+ for i , id := range tt .expectedIDs {
240+ if i >= len (ids ) || ids [i ] != id {
241+ t .Errorf ("Expected rule ID %s at position %d, got %v" , id , i , ids )
242+ }
243+ }
244+
245+ if len (instructions ) != len (tt .expectedInst ) {
246+ t .Errorf ("Expected %d instructions, got %d: %v" , len (tt .expectedInst ), len (instructions ), instructions )
247+ }
248+ })
249+ }
250+ }
251+
252+ func TestInstructionResolver_SupersetShadowing (t * testing.T ) {
253+ // Rule with more tools (superset) shadows rules with fewer tools
254+ resolver := NewInstructionResolver ([]InstructionRule {
255+ NewInstructionRule ("issues-read" , "Read issues instruction" , "get_issue" , "list_issues" ),
256+ NewInstructionRule ("issues-all" , "All issues instruction" , "get_issue" , "list_issues" , "create_issue" ),
257+ NewInstructionRule ("create-only" , "Create only instruction" , "create_issue" ),
258+ })
259+
260+ tests := []struct {
261+ name string
262+ activeTools []string
263+ expectedIDs []string
264+ description string
265+ }{
266+ {
267+ name : "superset rule shadows subset rules" ,
268+ activeTools : []string {"get_issue" , "list_issues" , "create_issue" },
269+ expectedIDs : []string {"issues-all" },
270+ description : "issues-all shadows issues-read (superset) and create-only (superset)" ,
271+ },
272+ {
273+ name : "no shadowing when superset rule doesn't match" ,
274+ activeTools : []string {"get_issue" , "list_issues" },
275+ expectedIDs : []string {"issues-read" },
276+ description : "issues-all doesn't match (missing create_issue), so issues-read is not shadowed" ,
277+ },
278+ {
279+ name : "single tool rule not shadowed when superset doesn't match" ,
280+ activeTools : []string {"create_issue" },
281+ expectedIDs : []string {"create-only" },
282+ description : "Only create-only matches" ,
283+ },
284+ {
285+ name : "extra active tools don't affect shadowing" ,
286+ activeTools : []string {"get_issue" , "list_issues" , "create_issue" , "get_me" , "other_tool" },
287+ expectedIDs : []string {"issues-all" },
288+ description : "Extra tools in activeTools don't prevent shadowing" ,
289+ },
290+ }
291+
292+ for _ , tt := range tests {
293+ t .Run (tt .name , func (t * testing.T ) {
294+ ids := resolver .MatchingRuleIDs (tt .activeTools )
295+
296+ if len (ids ) != len (tt .expectedIDs ) {
297+ t .Errorf ("%s: Expected %d rule IDs %v, got %d: %v" ,
298+ tt .description , len (tt .expectedIDs ), tt .expectedIDs , len (ids ), ids )
299+ return
300+ }
301+ for i , id := range tt .expectedIDs {
302+ if i >= len (ids ) || ids [i ] != id {
303+ t .Errorf ("%s: Expected rule ID %s at position %d, got %v" ,
304+ tt .description , id , i , ids )
305+ }
306+ }
307+ })
308+ }
309+ }
310+
311+ func TestInstructionResolver_PartialOverlapNoShadowing (t * testing.T ) {
312+ // Rules with partial overlap (neither is superset of other) should both apply
313+ resolver := NewInstructionResolver ([]InstructionRule {
314+ NewInstructionRule ("rule-ab" , "AB instruction" , "tool_a" , "tool_b" ),
315+ NewInstructionRule ("rule-bc" , "BC instruction" , "tool_b" , "tool_c" ),
316+ })
317+
318+ // When all three tools are active, both rules match
319+ // Neither is a superset of the other, so neither is shadowed
320+ ids := resolver .MatchingRuleIDs ([]string {"tool_a" , "tool_b" , "tool_c" })
321+
322+ if len (ids ) != 2 {
323+ t .Errorf ("Expected 2 rules (partial overlap, no shadowing), got %d: %v" , len (ids ), ids )
324+ }
325+ }
326+
327+ func TestInstructionResolver_ComplexHierarchy (t * testing.T ) {
328+ // Create a hierarchy: rule1 ⊂ rule2 ⊂ rule3
329+ resolver := NewInstructionResolver ([]InstructionRule {
330+ NewInstructionRule ("level1" , "Level 1 instruction" , "t1" ),
331+ NewInstructionRule ("level2" , "Level 2 instruction" , "t1" , "t2" ),
332+ NewInstructionRule ("level3" , "Level 3 instruction" , "t1" , "t2" , "t3" ),
333+ })
334+
335+ tests := []struct {
336+ name string
337+ activeTools []string
338+ expectedIDs []string
339+ }{
340+ {
341+ name : "only level1 active" ,
342+ activeTools : []string {"t1" },
343+ expectedIDs : []string {"level1" },
344+ },
345+ {
346+ name : "level1 and level2 tools active" ,
347+ activeTools : []string {"t1" , "t2" },
348+ expectedIDs : []string {"level2" }, // shadows level1
349+ },
350+ {
351+ name : "all three levels active" ,
352+ activeTools : []string {"t1" , "t2" , "t3" },
353+ expectedIDs : []string {"level3" }, // shadows level1 and level2
354+ },
355+ }
356+
357+ for _ , tt := range tests {
358+ t .Run (tt .name , func (t * testing.T ) {
359+ ids := resolver .MatchingRuleIDs (tt .activeTools )
360+ if len (ids ) != len (tt .expectedIDs ) {
361+ t .Errorf ("Expected %v, got %v" , tt .expectedIDs , ids )
362+ }
363+ })
364+ }
365+ }
366+
367+ func TestInstructionResolver_EmptyRules (t * testing.T ) {
368+ resolver := NewInstructionResolver ([]InstructionRule {})
369+
370+ ids := resolver .MatchingRuleIDs ([]string {"tool1" , "tool2" })
371+ if len (ids ) != 0 {
372+ t .Errorf ("Expected no matches for empty resolver, got %v" , ids )
373+ }
374+
375+ instructions := resolver .ResolveInstructions ([]string {"tool1" })
376+ if len (instructions ) != 0 {
377+ t .Errorf ("Expected no instructions for empty resolver, got %v" , instructions )
378+ }
379+ }
380+
381+ func TestInstructionResolver_EmptyActiveTools (t * testing.T ) {
382+ resolver := NewInstructionResolver ([]InstructionRule {
383+ NewInstructionRule ("rule1" , "Instruction 1" , "tool1" ),
384+ })
385+
386+ ids := resolver .MatchingRuleIDs ([]string {})
387+ if len (ids ) != 0 {
388+ t .Errorf ("Expected no matches for empty active tools, got %v" , ids )
389+ }
390+ }
391+
392+ // =============================================================================
393+ // END SPIKE TESTS
394+ // =============================================================================
0 commit comments