| 1 | package skill |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/config" |
| 7 | "reasonix/internal/tool" |
| 8 | _ "reasonix/internal/tool/builtin" |
| 9 | ) |
| 10 | |
| 11 | func TestToolReferenceDiagnostics(t *testing.T) { |
| 12 | binding := tool.MCPBinding{Package: "example-plugin", Server: "github", RawName: "search", VisibleName: "search", CallableName: "mcp__github__search", CapabilityID: "mcp-tool:github/search"} |
| 13 | other := binding |
| 14 | other.Server, other.CallableName, other.CapabilityID = "other", "mcp__other__search", "mcp-tool:other/search" |
| 15 | for _, tc := range []struct { |
| 16 | ref, code string |
| 17 | bindings []tool.MCPBinding |
| 18 | }{ |
| 19 | {"use_capability", "", nil}, |
| 20 | {"grep", "", nil}, |
| 21 | {"new_hidden_tool", "", nil}, |
| 22 | {"read_*", "", nil}, |
| 23 | {"*", "", nil}, |
| 24 | {"[", "skill.tool_reference_invalid", nil}, |
| 25 | {"mcp-tool:github", "skill.tool_reference_invalid", nil}, |
| 26 | {"mcp-server:", "skill.tool_reference_invalid", nil}, |
| 27 | {"mcp__github__", "skill.tool_reference_invalid", nil}, |
| 28 | {"typo_read_file", "skill.tool_reference_unknown", nil}, |
| 29 | {"lsp_typo", "skill.tool_reference_unknown", nil}, |
| 30 | {"mcp__future__search", "skill.tool_reference_unverified", nil}, |
| 31 | {"future/*", "skill.tool_reference_unverified", nil}, |
| 32 | {"session:tool_result", "skill.tool_reference_unverified", nil}, |
| 33 | {"tool:docs", "skill.tool_reference_unverified", nil}, |
| 34 | {"skill:review", "skill.tool_reference_unverified", nil}, |
| 35 | {"memory:remember", "skill.tool_reference_unverified", nil}, |
| 36 | {"github/search", "", []tool.MCPBinding{binding}}, |
| 37 | {"search", "", []tool.MCPBinding{binding}}, |
| 38 | {"mcp-tool:github/search", "", []tool.MCPBinding{binding}}, |
| 39 | {"mcp__github__search", "", []tool.MCPBinding{binding}}, |
| 40 | {"search", "skill.tool_reference_ambiguous", []tool.MCPBinding{binding, other}}, |
| 41 | {"mcp__*", "", []tool.MCPBinding{binding, other}}, |
| 42 | } { |
| 43 | t.Run(tc.ref+tc.code, func(t *testing.T) { |
| 44 | d := CheckToolReferences([]Skill{{Name: "example", Plugin: "example-plugin", AllowedTools: []string{tc.ref}}}, ToolReferenceOptions{ |
| 45 | Known: tool.KnownToolNames(), Registered: []tool.ContractEntry{{Name: "new_hidden_tool"}}, Bindings: tc.bindings, |
| 46 | }) |
| 47 | if tc.code == "" { |
| 48 | if len(d) != 0 { |
| 49 | t.Fatalf("unexpected diagnostic: %+v", d) |
| 50 | } |
| 51 | return |
| 52 | } |
| 53 | if len(d) != 1 || d[0].Code != tc.code || d[0].Reference != tc.ref || d[0].Skill != "example" { |
| 54 | t.Fatalf("got %+v, want %s", d, tc.code) |
| 55 | } |
| 56 | wantSeverity := "warning" |
| 57 | if tc.code == "skill.tool_reference_unverified" { |
| 58 | wantSeverity = "info" |
| 59 | } |
| 60 | if d[0].Severity != wantSeverity { |
| 61 | t.Fatalf("severity: %+v", d) |
| 62 | } |
| 63 | }) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestBuiltinSkillReferencesAndMCPRequirements(t *testing.T) { |
| 68 | store := DiagnosticStore(t.TempDir(), t.TempDir(), t.TempDir(), config.Default()) |
| 69 | if len(store.List()) == 0 { |
| 70 | t.Fatal("no built-in skills loaded") |
| 71 | } |
| 72 | if d := CheckToolReferences(store.List(), ToolReferenceOptions{Known: tool.KnownToolNames()}); len(d) != 0 { |
| 73 | t.Fatalf("builtin references: %+v", d) |
| 74 | } |
| 75 | sk := []Skill{{Name: "dependent", AutoUse: "require", Requires: []string{"mcp-server:github"}, AllowedTools: []string{"use_capability"}}} |
| 76 | for _, tc := range []struct { |
| 77 | configured bool |
| 78 | failed, code string |
| 79 | }{ |
| 80 | {false, "", "skill.mcp_dependency_missing"}, |
| 81 | {true, "", ""}, |
| 82 | {true, "spawn failed", "skill.mcp_dependency_failed"}, |
| 83 | } { |
| 84 | var plugins []config.PluginEntry |
| 85 | if tc.configured { |
| 86 | plugins = []config.PluginEntry{{Name: "github"}} |
| 87 | } |
| 88 | if d := CheckToolReferences(sk, ToolReferenceOptions{Known: tool.KnownToolNames()}); len(d) != 0 { |
| 89 | t.Fatal(d) |
| 90 | } |
| 91 | d := CheckMCPRequirements(sk, plugins, map[string]string{"github": tc.failed}) |
| 92 | if tc.code == "" { |
| 93 | if len(d) != 0 { |
| 94 | t.Fatal(d) |
| 95 | } |
| 96 | continue |
| 97 | } |
| 98 | if len(d) != 1 || d[0].Code != tc.code { |
| 99 | t.Fatalf("got %+v, want %s", d, tc.code) |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 |