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
181 lines
6.8 KiB
Go
181 lines
6.8 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// --- env_var_users ---------------------------------------------------------
|
|
|
|
func TestAnalyzeEnvVarUsers_FiltersToEnvKeys(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
addConfigKeyNode(srv.graph, "cfg::env::DATABASE_URL", "DATABASE_URL", "env")
|
|
addConfigKeyNode(srv.graph, "viper::log.level", "log.level", "viper")
|
|
addReadConfigEdge(srv.graph, "f.go::A", "cfg::env::DATABASE_URL")
|
|
addReadConfigEdge(srv.graph, "f.go::B", "cfg::env::DATABASE_URL")
|
|
addReadConfigEdge(srv.graph, "f.go::C", "viper::log.level")
|
|
|
|
out := callAnalyze(t, srv, "env_var_users", map[string]any{})
|
|
rows, _ := out["env_vars"].([]any)
|
|
require.Len(t, rows, 1, "viper (non-env) config key must be excluded")
|
|
row := rows[0].(map[string]any)
|
|
require.Equal(t, "DATABASE_URL", row["name"])
|
|
require.Equal(t, float64(2), row["reads"])
|
|
}
|
|
|
|
func TestAnalyzeEnvVarUsers_NameFilter(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
addConfigKeyNode(srv.graph, "env::PORT", "PORT", "env")
|
|
addConfigKeyNode(srv.graph, "env::HOST", "HOST", "env")
|
|
addReadConfigEdge(srv.graph, "f.go::A", "env::PORT")
|
|
addReadConfigEdge(srv.graph, "f.go::B", "env::HOST")
|
|
|
|
out := callAnalyze(t, srv, "env_var_users", map[string]any{"name": "port"})
|
|
require.Equal(t, float64(1), out["total"])
|
|
}
|
|
|
|
// --- sql_call_sites --------------------------------------------------------
|
|
|
|
func TestAnalyzeSQLCallSites_GroupsByCaller(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
srv.graph.AddNode(&graph.Node{ID: "f.go::GetUser", Kind: graph.KindFunction, Name: "GetUser", FilePath: "f.go"})
|
|
srv.graph.AddNode(&graph.Node{ID: "tbl::users", Kind: graph.KindTable, Name: "users"})
|
|
srv.graph.AddNode(&graph.Node{ID: "tbl::orders", Kind: graph.KindTable, Name: "orders"})
|
|
srv.graph.AddEdge(&graph.Edge{From: "f.go::GetUser", To: "tbl::users", Kind: graph.EdgeQueries, Meta: map[string]any{"op": "read"}})
|
|
srv.graph.AddEdge(&graph.Edge{From: "f.go::GetUser", To: "tbl::orders", Kind: graph.EdgeQueries, Meta: map[string]any{"op": "write"}})
|
|
|
|
out := callAnalyze(t, srv, "sql_call_sites", map[string]any{})
|
|
rows, _ := out["call_sites"].([]any)
|
|
require.Len(t, rows, 1)
|
|
row := rows[0].(map[string]any)
|
|
require.Equal(t, "GetUser", row["name"])
|
|
require.Equal(t, float64(2), row["queries"])
|
|
require.Equal(t, float64(1), row["reads"])
|
|
require.Equal(t, float64(1), row["writes"])
|
|
}
|
|
|
|
// --- fixes_history ---------------------------------------------------------
|
|
|
|
func mustGit(t *testing.T, dir string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
|
cmd.Env = append(cmd.Environ(),
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t.test",
|
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t.test")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
|
|
func TestFixSubjectRe(t *testing.T) {
|
|
for _, s := range []string{"fix: x", "fixes #3", "fixed the bug", "bugfix in y", "hotfix", "FIX something"} {
|
|
require.True(t, fixSubjectRe.MatchString(s), "%q should be a fix subject", s)
|
|
}
|
|
for _, s := range []string{"add feature", "prefix handling", "refactor fixtures", "update docs"} {
|
|
require.False(t, fixSubjectRe.MatchString(s), "%q should NOT be a fix subject", s)
|
|
}
|
|
}
|
|
|
|
func TestMineFixCommits_DetectsFixSubjects(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not available")
|
|
}
|
|
dir := t.TempDir()
|
|
mustGit(t, dir, "init", "-q")
|
|
commit := func(body, msg string) {
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "a.go"), []byte(body), 0o644))
|
|
mustGit(t, dir, "add", "a.go")
|
|
mustGit(t, dir, "commit", "-q", "-m", msg)
|
|
}
|
|
commit("package a\n", "add feature a")
|
|
commit("package a\n// v2\n", "fix: nil deref in a")
|
|
commit("package a\n// v3\n", "fixes crash on startup")
|
|
|
|
commits := mineFixCommits(context.Background(), dir, 100)
|
|
require.Len(t, commits, 2, "two fix commits, the plain-feature commit excluded")
|
|
for _, c := range commits {
|
|
require.Contains(t, c.files, "a.go")
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeFixesHistory_EndToEnd(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not available")
|
|
}
|
|
srv, dir := setupTestServer(t)
|
|
mustGit(t, dir, "init", "-q")
|
|
mustGit(t, dir, "add", "main.go")
|
|
mustGit(t, dir, "commit", "-q", "-m", "initial commit")
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"),
|
|
[]byte("package main\n\nfunc main() {}\n"), 0o644))
|
|
mustGit(t, dir, "add", "main.go")
|
|
mustGit(t, dir, "commit", "-q", "-m", "fix: correct main logic")
|
|
|
|
out := callAnalyze(t, srv, "fixes_history", map[string]any{})
|
|
if got, _ := out["total_fix_commits"].(float64); got < 1 {
|
|
t.Fatalf("expected >=1 fix commit, got %v", got)
|
|
}
|
|
files, _ := out["files"].([]any)
|
|
require.NotEmpty(t, files)
|
|
require.Equal(t, "main.go", files[0].(map[string]any)["file"])
|
|
}
|
|
|
|
func TestSymbolNamesInFile(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
// setupTestServer indexes a main.go containing main + helper.
|
|
require.NotEmpty(t, srv.symbolNamesInFile("main.go"))
|
|
}
|
|
|
|
// --- edge_audit ------------------------------------------------------------
|
|
|
|
func TestAnalyzeEdgeAudit_Buckets(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
|
|
// An interface with no implementor.
|
|
g.AddNode(&graph.Node{ID: "z.go::Orphan", Kind: graph.KindInterface, Name: "Orphan"})
|
|
// An interface with an implementor — must NOT be flagged.
|
|
g.AddNode(&graph.Node{ID: "z.go::Used", Kind: graph.KindInterface, Name: "Used"})
|
|
g.AddNode(&graph.Node{ID: "z.go::Impl", Kind: graph.KindType, Name: "Impl"})
|
|
g.AddEdge(&graph.Edge{From: "z.go::Impl", To: "z.go::Used", Kind: graph.EdgeImplements})
|
|
// A function reached only from a test file, via a text-matched edge.
|
|
g.AddNode(&graph.Node{ID: "z.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "z.go"})
|
|
g.AddNode(&graph.Node{ID: "z_test.go::TestZ", Kind: graph.KindFunction, Name: "TestZ", FilePath: "z_test.go"})
|
|
g.AddEdge(&graph.Edge{
|
|
From: "z_test.go::TestZ", To: "z.go::Helper",
|
|
Kind: graph.EdgeCalls, Origin: graph.OriginTextMatched,
|
|
})
|
|
|
|
out := callAnalyze(t, srv, "edge_audit", map[string]any{})
|
|
require.Contains(t, out, "edge_tiers")
|
|
require.Contains(t, out, "summary")
|
|
|
|
ui := out["unimplemented_interfaces"].(map[string]any)
|
|
require.GreaterOrEqual(t, ui["count"].(float64), float64(1))
|
|
require.NotContains(t, sampleStrings(ui), "z.go::Used")
|
|
|
|
to := out["test_only_targets"].(map[string]any)
|
|
require.Contains(t, sampleStrings(to), "z.go::Helper")
|
|
|
|
wc := out["weak_call_edges"].(map[string]any)
|
|
require.GreaterOrEqual(t, wc["count"].(float64), float64(1))
|
|
}
|
|
|
|
func sampleStrings(bucket map[string]any) []string {
|
|
raw, _ := bucket["sample"].([]any)
|
|
out := make([]string, 0, len(raw))
|
|
for _, v := range raw {
|
|
if s, ok := v.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|