Files
2026-07-13 13:00:08 +08:00

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)
}
}