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
174 lines
6.6 KiB
Go
174 lines
6.6 KiB
Go
package skills
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/analysis"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func testGraph() (*graph.Graph, *analysis.CommunityResult, *analysis.ProcessResult) {
|
|
g := graph.New()
|
|
|
|
// Parser community.
|
|
g.AddNode(&graph.Node{ID: "parser/parse.go::Parse", Kind: graph.KindFunction, Name: "Parse", FilePath: "parser/parse.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "parser/parse.go::Tokenize", Kind: graph.KindFunction, Name: "Tokenize", FilePath: "parser/parse.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "parser/ast.go::BuildAST", Kind: graph.KindFunction, Name: "BuildAST", FilePath: "parser/ast.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "parser/ast.go::Node", Kind: graph.KindType, Name: "Node", FilePath: "parser/ast.go", Language: "go"})
|
|
|
|
// Server community.
|
|
g.AddNode(&graph.Node{ID: "server/handler.go::HandleRequest", Kind: graph.KindFunction, Name: "HandleRequest", FilePath: "server/handler.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "server/handler.go::Middleware", Kind: graph.KindFunction, Name: "Middleware", FilePath: "server/handler.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "server/router.go::Route", Kind: graph.KindFunction, Name: "Route", FilePath: "server/router.go", Language: "go"})
|
|
|
|
// Tiny community (should be filtered).
|
|
g.AddNode(&graph.Node{ID: "util/helper.go::Max", Kind: graph.KindFunction, Name: "Max", FilePath: "util/helper.go", Language: "go"})
|
|
|
|
// Cross-community edge.
|
|
g.AddEdge(&graph.Edge{From: "server/handler.go::HandleRequest", To: "parser/parse.go::Parse", Kind: graph.EdgeCalls})
|
|
|
|
communities := &analysis.CommunityResult{
|
|
Communities: []analysis.Community{
|
|
{
|
|
ID: "community-0", Label: "parser", Size: 4, Cohesion: 0.8,
|
|
Members: []string{"parser/parse.go::Parse", "parser/parse.go::Tokenize", "parser/ast.go::BuildAST", "parser/ast.go::Node"},
|
|
Files: []string{"parser/parse.go", "parser/ast.go"},
|
|
},
|
|
{
|
|
ID: "community-1", Label: "server", Size: 3, Cohesion: 0.65,
|
|
Members: []string{"server/handler.go::HandleRequest", "server/handler.go::Middleware", "server/router.go::Route"},
|
|
Files: []string{"server/handler.go", "server/router.go"},
|
|
},
|
|
{
|
|
ID: "community-2", Label: "util", Size: 1, Cohesion: 1.0,
|
|
Members: []string{"util/helper.go::Max"},
|
|
Files: []string{"util/helper.go"},
|
|
},
|
|
},
|
|
NodeToComm: map[string]string{
|
|
"parser/parse.go::Parse": "community-0",
|
|
"parser/parse.go::Tokenize": "community-0",
|
|
"parser/ast.go::BuildAST": "community-0",
|
|
"parser/ast.go::Node": "community-0",
|
|
"server/handler.go::HandleRequest": "community-1",
|
|
"server/handler.go::Middleware": "community-1",
|
|
"server/router.go::Route": "community-1",
|
|
"util/helper.go::Max": "community-2",
|
|
},
|
|
Modularity: 0.45,
|
|
}
|
|
|
|
processes := &analysis.ProcessResult{
|
|
Processes: []analysis.Process{
|
|
{
|
|
ID: "proc-0", Name: "request-handling", EntryPoint: "server/handler.go::HandleRequest",
|
|
Steps: []analysis.Step{{ID: "server/handler.go::HandleRequest", Depth: 0}, {ID: "parser/parse.go::Parse", Depth: 1}},
|
|
Files: []string{"server/handler.go", "parser/parse.go"},
|
|
},
|
|
},
|
|
}
|
|
|
|
return g, communities, processes
|
|
}
|
|
|
|
func TestGenerateAll_BasicCommunities(t *testing.T) {
|
|
g, communities, processes := testGraph()
|
|
gen := New(communities, processes, g)
|
|
|
|
skills := gen.GenerateAll()
|
|
require.Len(t, skills, 2, "should generate 2 skills (util filtered out)")
|
|
|
|
// Sorted by size descending: parser (4) then server (3).
|
|
assert.Equal(t, "parser", skills[0].Label)
|
|
assert.Equal(t, "gortex-parser", skills[0].DirName)
|
|
assert.Equal(t, "server", skills[1].Label)
|
|
|
|
// Verify parser skill content.
|
|
assert.Contains(t, skills[0].Content, "name: gortex-parser")
|
|
assert.Contains(t, skills[0].Content, "4 symbols")
|
|
assert.Contains(t, skills[0].Content, "2 files")
|
|
assert.Contains(t, skills[0].Content, "80% cohesion")
|
|
assert.Contains(t, skills[0].Content, "parser/parse.go")
|
|
assert.Contains(t, skills[0].Content, "parser/ast.go")
|
|
assert.Contains(t, skills[0].Content, "Parse, Tokenize")
|
|
|
|
// Verify server skill has entry point.
|
|
assert.Contains(t, skills[1].Content, "server/handler.go::HandleRequest")
|
|
assert.Contains(t, skills[1].Content, "## Entry Points")
|
|
}
|
|
|
|
func TestGenerateAll_FilterSmallCommunities(t *testing.T) {
|
|
g, communities, processes := testGraph()
|
|
gen := New(communities, processes, g)
|
|
|
|
skills := gen.GenerateAll()
|
|
|
|
// util community has size 1 → filtered out.
|
|
for _, s := range skills {
|
|
assert.NotEqual(t, "util", s.Label, "small communities should be excluded")
|
|
}
|
|
}
|
|
|
|
func TestGenerateAll_CrossCommunityConnections(t *testing.T) {
|
|
g, communities, processes := testGraph()
|
|
gen := New(communities, processes, g)
|
|
|
|
skills := gen.GenerateAll()
|
|
|
|
// Server calls parser → server skill should mention parser connection.
|
|
var serverSkill *GeneratedSkill
|
|
for i := range skills {
|
|
if skills[i].Label == "server" {
|
|
serverSkill = &skills[i]
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, serverSkill)
|
|
assert.Contains(t, serverSkill.Content, "## Connected Communities")
|
|
assert.Contains(t, serverSkill.Content, "parser")
|
|
}
|
|
|
|
func TestGenerateRouting(t *testing.T) {
|
|
g, communities, processes := testGraph()
|
|
gen := New(communities, processes, g)
|
|
|
|
skills := gen.GenerateAll()
|
|
routing := gen.GenerateRouting(skills)
|
|
|
|
assert.Contains(t, routing, "<!-- gortex:skills:start -->")
|
|
assert.Contains(t, routing, "<!-- gortex:skills:end -->")
|
|
assert.Contains(t, routing, "| Parser |")
|
|
assert.Contains(t, routing, "| Server |")
|
|
assert.Contains(t, routing, "`/gortex-parser`")
|
|
assert.Contains(t, routing, "`/gortex-server`")
|
|
}
|
|
|
|
func TestGenerateAll_NilCommunities(t *testing.T) {
|
|
g := graph.New()
|
|
gen := New(nil, nil, g)
|
|
skills := gen.GenerateAll()
|
|
assert.Nil(t, skills)
|
|
}
|
|
|
|
func TestToKebab(t *testing.T) {
|
|
assert.Equal(t, "mcp-server", toKebab("MCP Server"))
|
|
assert.Equal(t, "parser", toKebab("parser"))
|
|
assert.Equal(t, "internal-graph", toKebab("internal/graph"))
|
|
assert.Equal(t, "foo-bar-baz", toKebab("Foo Bar--Baz"))
|
|
}
|
|
|
|
func TestGenerateAll_HowToExplore(t *testing.T) {
|
|
g, communities, processes := testGraph()
|
|
gen := New(communities, processes, g)
|
|
|
|
skills := gen.GenerateAll()
|
|
for _, s := range skills {
|
|
assert.True(t, strings.Contains(s.Content, "## How to Explore"), "skill %s should have How to Explore section", s.Label)
|
|
assert.True(t, strings.Contains(s.Content, "get_communities"), "skill %s should reference get_communities tool", s.Label)
|
|
assert.True(t, strings.Contains(s.Content, "smart_context"), "skill %s should reference smart_context tool", s.Label)
|
|
}
|
|
}
|