154 lines
5.2 KiB
Go
154 lines
5.2 KiB
Go
package skill
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/tool"
|
|
)
|
|
|
|
type builtinTestTool struct {
|
|
name string
|
|
readOnly bool
|
|
}
|
|
|
|
func (t builtinTestTool) Name() string { return t.name }
|
|
func (t builtinTestTool) Description() string { return t.name }
|
|
func (t builtinTestTool) Schema() json.RawMessage {
|
|
return json.RawMessage(`{"type":"object"}`)
|
|
}
|
|
func (t builtinTestTool) Execute(context.Context, json.RawMessage) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (t builtinTestTool) ReadOnly() bool { return t.readOnly }
|
|
|
|
// TestBuiltinReviewSkillsDeclareReadOnly pins the tool-boundary contract behind
|
|
// the review/security-review "Read-only" promise: runners select the read-only
|
|
// subagent registry from this flag, so losing it silently re-opens writer bash.
|
|
func TestBuiltinReviewSkillsDeclareReadOnly(t *testing.T) {
|
|
want := map[string]bool{
|
|
"explore": false,
|
|
"research": false,
|
|
"review": true,
|
|
"security-review": true,
|
|
}
|
|
for _, sk := range builtinSkills() {
|
|
expected, tracked := want[sk.Name]
|
|
if !tracked {
|
|
continue
|
|
}
|
|
if sk.ReadOnly != expected {
|
|
t.Errorf("builtin %q ReadOnly = %v, want %v", sk.Name, sk.ReadOnly, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCodeGraphReadToolsRequireKnownNameAndReadOnly(t *testing.T) {
|
|
reg := tool.NewRegistry()
|
|
reg.Add(builtinTestTool{name: "mcp__codegraph__symbols", readOnly: true})
|
|
reg.Add(builtinTestTool{name: "codegraph_search", readOnly: true})
|
|
reg.Add(builtinTestTool{name: "mcp__codegraph__write_index", readOnly: false})
|
|
reg.Add(builtinTestTool{name: "mcp__other__codegraph_search", readOnly: true})
|
|
|
|
got := CodeGraphReadTools(reg)
|
|
want := []string{"codegraph_search", "mcp__codegraph__symbols"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("CodeGraphReadTools = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("CodeGraphReadTools = %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltinSkillsIncludeCodeGraphHintAndToolsWhenDiscovered(t *testing.T) {
|
|
reg := tool.NewRegistry()
|
|
reg.Add(builtinTestTool{name: "mcp__codegraph__symbols", readOnly: true})
|
|
|
|
var explore Skill
|
|
for _, sk := range builtinSkills() {
|
|
if sk.Name == "explore" {
|
|
explore = sk
|
|
break
|
|
}
|
|
}
|
|
if explore.Name == "" {
|
|
t.Fatal("explore skill not found")
|
|
}
|
|
if strings.Contains(explore.Body, "Optional installed code graph MCP tools") {
|
|
t.Fatalf("base explore body should not include session-specific codegraph hint:\n%s", explore.Body)
|
|
}
|
|
for _, name := range explore.AllowedTools {
|
|
if name == "mcp__codegraph__symbols" {
|
|
t.Fatalf("base explore allowed tools = %v, should not include session-specific codegraph tool", explore.AllowedTools)
|
|
}
|
|
}
|
|
|
|
explore = WithCodeGraphTools(explore, CodeGraphReadTools(reg))
|
|
if !strings.Contains(explore.Body, "Optional installed code graph MCP tools") {
|
|
t.Fatalf("explore body missing optional codegraph hint:\n%s", explore.Body)
|
|
}
|
|
for _, want := range []string{
|
|
"use LSP for language semantics",
|
|
"use code graph tools first for call graph, impact analysis, and architecture relationships",
|
|
"use code_index only as the built-in outline/definition-candidate fallback",
|
|
} {
|
|
if !strings.Contains(explore.Body, want) {
|
|
t.Fatalf("explore body missing priority hint %q:\n%s", want, explore.Body)
|
|
}
|
|
}
|
|
found := false
|
|
for _, name := range explore.AllowedTools {
|
|
if name == "mcp__codegraph__symbols" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("explore allowed tools = %v, want codegraph tool", explore.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestWithCodeGraphToolsOnlyTouchesCodeReadingBuiltins(t *testing.T) {
|
|
initSkill := Skill{Name: "init", Scope: ScopeBuiltin, Body: "body", AllowedTools: []string{"read_file"}}
|
|
got := WithCodeGraphTools(initSkill, []string{"mcp__codegraph__symbols"})
|
|
if strings.Contains(got.Body, "Optional installed code graph MCP tools") {
|
|
t.Fatalf("init skill should not receive codegraph hint:\n%s", got.Body)
|
|
}
|
|
if len(got.AllowedTools) != 1 || got.AllowedTools[0] != "read_file" {
|
|
t.Fatalf("init allowed tools = %v, want unchanged", got.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestWithCodeGraphToolsSkipsUserSkillOverrides(t *testing.T) {
|
|
sk := Skill{Name: "explore", Scope: ScopeProject, Body: "user body", AllowedTools: []string{"read_file"}}
|
|
got := WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
|
|
if strings.Contains(got.Body, "Optional installed code graph MCP tools") {
|
|
t.Fatalf("project skill override should not receive codegraph hint:\n%s", got.Body)
|
|
}
|
|
if len(got.AllowedTools) != 1 || got.AllowedTools[0] != "read_file" {
|
|
t.Fatalf("project skill override allowed tools = %v, want unchanged", got.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestWithCodeGraphToolsIsIdempotent(t *testing.T) {
|
|
sk := Skill{Name: "explore", Scope: ScopeBuiltin, Body: "body", AllowedTools: []string{"read_file"}}
|
|
sk = WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
|
|
sk = WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
|
|
if got := strings.Count(sk.Body, optionalCodeGraphHint); got != 1 {
|
|
t.Fatalf("codegraph hint count = %d, want 1; body:\n%s", got, sk.Body)
|
|
}
|
|
count := 0
|
|
for _, name := range sk.AllowedTools {
|
|
if name == "mcp__codegraph__symbols" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("codegraph tool count = %d, want 1; allowed=%v", count, sk.AllowedTools)
|
|
}
|
|
}
|