Files
zzet--gortex/internal/mcp/tools_generate_skill_test.go
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

267 lines
8.2 KiB
Go

package mcp
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
func newGenerateSkillTestServer(t *testing.T) *Server {
t.Helper()
return &Server{
graph: graph.New(),
session: newSessionState(),
tokenStats: &tokenStats{},
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
sessions: newSessionMap(),
toolScopes: newScopeRegistry(),
}
}
// seedSkillSource builds a small source tree the tool will bundle.
// Returns the absolute path to the directory.
func seedSkillSource(t *testing.T) string {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"),
[]byte("package main\n\nfunc Run() int {\n\tfor i := 0; i < 5; i++ {\n\t\t_ = i\n\t}\n\treturn 42\n}\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "helpers.go"),
[]byte("package main\n\nfunc Helper() string { return \"hi\" }\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"),
[]byte("# Demo project\n"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "node_modules"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "node_modules", "junk.js"),
[]byte("// noise\n"), 0o644))
return dir
}
func callGenerateSkill(t *testing.T, s *Server, args map[string]any) map[string]any {
t.Helper()
req := mcp.CallToolRequest{}
req.Params.Arguments = args
res, err := s.handleGenerateSkill(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, res)
if res.IsError {
return map[string]any{"is_error": true, "content": res.Content}
}
tc, ok := res.Content[0].(mcp.TextContent)
require.True(t, ok)
var m map[string]any
require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
return m
}
func TestGenerateSkill_HappyPath(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := seedSkillSource(t)
outDir := t.TempDir()
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": outDir,
})
skillPath := out["skill_path"].(string)
require.FileExists(t, skillPath)
body, _ := os.ReadFile(skillPath)
assert.Contains(t, string(body), "name:", "frontmatter present")
assert.Contains(t, string(body), "description:", "frontmatter present")
assert.Contains(t, string(body), "## References", "reference section present")
// References tree exists with the source files but not node_modules.
assert.FileExists(t, filepath.Join(outDir, "references", "main.go"))
assert.FileExists(t, filepath.Join(outDir, "references", "helpers.go"))
assert.NoFileExists(t, filepath.Join(outDir, "references", "node_modules", "junk.js"))
}
func TestGenerateSkill_SluggifiesDefaultName(t *testing.T) {
s := newGenerateSkillTestServer(t)
// Directory with spaces and special chars.
parent := t.TempDir()
srcDir := filepath.Join(parent, "My Demo Project!")
require.NoError(t, os.MkdirAll(srcDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "x.go"), []byte("package x\n"), 0o644))
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": filepath.Join(t.TempDir(), "out"),
})
assert.Equal(t, "my-demo-project", out["skill_name"])
}
func TestGenerateSkill_IncludeFilter(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := seedSkillSource(t)
outDir := t.TempDir()
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": outDir,
"include_patterns": "*.go",
})
refs, _ := out["references"].([]any)
assert.NotEmpty(t, refs)
for _, r := range refs {
m := r.(map[string]any)
assert.True(t, strings.HasSuffix(m["rel_path"].(string), ".go"))
}
}
func TestGenerateSkill_IgnoreFilter(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := seedSkillSource(t)
outDir := t.TempDir()
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": outDir,
"ignore_patterns": "helpers.go",
})
refs, _ := out["references"].([]any)
for _, r := range refs {
m := r.(map[string]any)
assert.NotEqual(t, "helpers.go", m["rel_path"])
}
}
func TestGenerateSkill_DryRunWritesNothing(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := seedSkillSource(t)
outDir := filepath.Join(t.TempDir(), "out")
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": outDir,
"dry_run": true,
})
assert.Equal(t, true, out["dry_run"])
_, statErr := os.Stat(outDir)
assert.True(t, os.IsNotExist(statErr), "dry_run must not create output dir")
refs, _ := out["references"].([]any)
assert.NotEmpty(t, refs, "dry_run still reports the candidate references")
}
func TestGenerateSkill_CompressBraceBodies(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := seedSkillSource(t)
outDir := t.TempDir()
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": outDir,
"compress": true,
})
body, _ := os.ReadFile(filepath.Join(outDir, "references", "main.go"))
assert.Contains(t, string(body), "lines elided", "Run's body should be elided")
assert.Contains(t, string(body), "func Run()", "signature kept")
// At least one reference should report compressed=true.
refs, _ := out["references"].([]any)
hasCompressed := false
for _, r := range refs {
if r.(map[string]any)["compressed"] == true {
hasCompressed = true
break
}
}
assert.True(t, hasCompressed)
}
func TestGenerateSkill_MaxReferencesCap(t *testing.T) {
s := newGenerateSkillTestServer(t)
srcDir := t.TempDir()
for i := range 10 {
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "f"+string(rune('A'+i))+".go"),
[]byte("package x\n"), 0o644))
}
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"output_dir": t.TempDir(),
"max_references": 3,
})
refs, _ := out["references"].([]any)
assert.LessOrEqual(t, len(refs), 3)
assert.GreaterOrEqual(t, out["skipped"].(float64), 7.0)
}
func TestGenerateSkill_DefaultsToClaudeSkillsPath(t *testing.T) {
s := newGenerateSkillTestServer(t)
// Init a git repo so repoRootContaining resolves the root.
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
srcDir := filepath.Join(dir, "src")
require.NoError(t, os.MkdirAll(srcDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "a.go"), []byte("package x\n"), 0o644))
out := callGenerateSkill(t, s, map[string]any{
"directory": srcDir,
"skill_name": "demo",
})
expectedSkillDir := filepath.Join(dir, ".claude", "skills", "demo")
assert.Equal(t, expectedSkillDir, out["output_dir"])
assert.FileExists(t, filepath.Join(expectedSkillDir, "SKILL.md"))
}
func TestGenerateSkill_RejectsMissingDirectory(t *testing.T) {
s := newGenerateSkillTestServer(t)
out := callGenerateSkill(t, s, map[string]any{})
assert.True(t, out["is_error"] == true)
}
func TestGenerateSkill_RejectsNonDirectory(t *testing.T) {
s := newGenerateSkillTestServer(t)
dir := t.TempDir()
file := filepath.Join(dir, "not_a_dir.txt")
require.NoError(t, os.WriteFile(file, []byte("x"), 0o644))
out := callGenerateSkill(t, s, map[string]any{"directory": file})
assert.True(t, out["is_error"] == true)
}
// Unit-level checks for the helpers.
func TestSluggify(t *testing.T) {
cases := map[string]string{
"my-skill": "my-skill",
"My Skill": "my-skill",
"my!skill!": "my-skill",
" spaces ": "spaces",
"My_skill.123": "my-skill-123",
}
for in, want := range cases {
assert.Equal(t, want, sluggify(in), "sluggify(%q)", in)
}
}
func TestElideBraceBodies_KeepsSignatures(t *testing.T) {
src := "func A() {\n\tfor i := 0; i < 10; i++ {\n\t\tdoWork(i)\n\t}\n}\n\nfunc B() int { return 1 }\n"
out := elideBraceBodies(src)
assert.Contains(t, out, "func A()")
assert.Contains(t, out, "lines elided")
// One-line body should stay verbatim.
assert.Contains(t, out, "{ return 1 }")
}
func TestMatchPathPattern(t *testing.T) {
assert.True(t, matchPathPattern("*.go", "foo.go"))
assert.True(t, matchPathPattern("*.go", "sub/foo.go"))
assert.True(t, matchPathPattern("vendor", "vendor/x.go"))
assert.True(t, matchPathPattern("vendor/*", "vendor/x.go"))
assert.False(t, matchPathPattern("*.ts", "foo.go"))
}