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
196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func callAnalyze(t *testing.T, srv *Server, kind string, extra map[string]any) map[string]any {
|
|
t.Helper()
|
|
args := map[string]any{"kind": kind}
|
|
for k, v := range extra {
|
|
args[k] = v
|
|
}
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Name = "analyze"
|
|
req.Params.Arguments = args
|
|
res, err := srv.handleAnalyze(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("handleAnalyze(%s): %v", kind, err)
|
|
}
|
|
if res.IsError {
|
|
t.Fatalf("analyze(%s) returned error: %+v", kind, res.Content)
|
|
}
|
|
textBlock := res.Content[0].(mcplib.TextContent)
|
|
var out map[string]any
|
|
if err := json.Unmarshal([]byte(textBlock.Text), &out); err != nil {
|
|
t.Fatalf("analyze(%s) json: %v\n%s", kind, err, textBlock.Text)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// addEmitToKindString builds a (caller, KindString) emit pair with
|
|
// the given context and meta. Used by the registry-downstream
|
|
// analyzers' tests.
|
|
func addEmitToKindString(g graph.Store, caller, strID, value, ctx string, nodeMeta, edgeMeta map[string]any) {
|
|
meta := map[string]any{
|
|
"context": ctx,
|
|
"value": value,
|
|
}
|
|
for k, v := range nodeMeta {
|
|
meta[k] = v
|
|
}
|
|
g.AddNode(&graph.Node{
|
|
ID: strID,
|
|
Kind: graph.KindString,
|
|
Name: value,
|
|
Language: "go",
|
|
Meta: meta,
|
|
})
|
|
em := map[string]any{"context": ctx}
|
|
for k, v := range edgeMeta {
|
|
em[k] = v
|
|
}
|
|
g.AddEdge(&graph.Edge{
|
|
From: caller,
|
|
To: strID,
|
|
Kind: graph.EdgeEmits,
|
|
Meta: em,
|
|
})
|
|
}
|
|
|
|
// analyze kind=log_events aggregates EdgeEmits → KindString
|
|
// context="log_message" by literal value.
|
|
func TestAnalyzeLogEvents_AggregatesByValue(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
addEmitToKindString(g, "f.go::A", "string::log_message::user.signup", "user.signup", "log_message",
|
|
map[string]any{"level": "log", "event": "event::log::user.signup"},
|
|
map[string]any{"method": "Info", "level": "log"})
|
|
addEmitToKindString(g, "f.go::B", "string::log_message::user.signup", "user.signup", "log_message",
|
|
map[string]any{"level": "log"},
|
|
map[string]any{"method": "Info", "level": "log"})
|
|
|
|
out := callAnalyze(t, srv, "log_events", nil)
|
|
events, _ := out["events"].([]any)
|
|
if len(events) != 1 {
|
|
t.Fatalf("expected 1 log_event row, got %d", len(events))
|
|
}
|
|
row := events[0].(map[string]any)
|
|
if row["value"] != "user.signup" {
|
|
t.Errorf("value = %v", row["value"])
|
|
}
|
|
if emits, _ := row["emits"].(float64); emits != 2 {
|
|
t.Errorf("emits = %v, want 2", emits)
|
|
}
|
|
if level, _ := row["level"].(string); level != "log" {
|
|
t.Errorf("level = %q", level)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeLogEvents_IgnoresNonLogContexts(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
addEmitToKindString(g, "f.go::A", "string::error_msg::bad", "bad", "error_msg", nil, nil)
|
|
|
|
out := callAnalyze(t, srv, "log_events", nil)
|
|
events, _ := out["events"].([]any)
|
|
if len(events) != 0 {
|
|
t.Errorf("expected 0 rows (no log_message strings), got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeLogEvents_LevelFilter(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
addEmitToKindString(g, "f.go::A", "string::log_message::user.signup", "user.signup", "log_message",
|
|
nil, map[string]any{"method": "Info", "level": "info"})
|
|
addEmitToKindString(g, "f.go::B", "string::log_message::payment.failed", "payment.failed", "log_message",
|
|
nil, map[string]any{"method": "Error", "level": "error"})
|
|
|
|
out := callAnalyze(t, srv, "log_events", map[string]any{"level": "error"})
|
|
events, _ := out["events"].([]any)
|
|
if len(events) != 1 {
|
|
t.Fatalf("expected 1 row for level=error, got %d", len(events))
|
|
}
|
|
row := events[0].(map[string]any)
|
|
if row["value"] != "payment.failed" {
|
|
t.Errorf("value = %v", row["value"])
|
|
}
|
|
}
|
|
|
|
// analyze kind=sql_rebuild walks KindString context="sql" and
|
|
// derives KindTable / EdgeQueries.
|
|
func TestAnalyzeSQLRebuild_DerivesTables(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
g.AddNode(&graph.Node{ID: "f.go::Run", Kind: graph.KindFunction, Name: "Run"})
|
|
addEmitToKindString(g, "f.go::Run", "string::sql::SELECT id FROM users", "SELECT id FROM users", "sql",
|
|
map[string]any{"dialect": "postgres"},
|
|
map[string]any{"dialect": "postgres"})
|
|
|
|
out := callAnalyze(t, srv, "sql_rebuild", nil)
|
|
if got, _ := out["strings_visited"].(float64); got != 1 {
|
|
t.Errorf("strings_visited = %v, want 1", got)
|
|
}
|
|
if got, _ := out["tables_created"].(float64); got != 1 {
|
|
t.Errorf("tables_created = %v, want 1", got)
|
|
}
|
|
if got, _ := out["query_edges_created"].(float64); got != 1 {
|
|
t.Errorf("query_edges_created = %v, want 1", got)
|
|
}
|
|
// Verify the table actually landed in the graph.
|
|
if n := g.GetNode("db::postgres::users"); n == nil {
|
|
t.Errorf("expected db::postgres::users node to be created")
|
|
}
|
|
}
|
|
|
|
// error_surface now includes error_msg KindString literals.
|
|
func TestAnalyzeErrorSurface_IncludesErrorMsgRegistry(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
addThrowsEdge(g, "f.go::DoThing", "external::error", "f.go", 10)
|
|
addEmitToKindString(g, "f.go::DoThing", "string::error_msg::bad token",
|
|
"bad token", "error_msg", nil, nil)
|
|
addEmitToKindString(g, "f.go::DoThing", "string::error_msg::unauthorized",
|
|
"unauthorized", "error_msg", nil, nil)
|
|
// A non-error_msg KindString shouldn't leak into the registry.
|
|
addEmitToKindString(g, "f.go::DoThing", "string::metric::api.calls",
|
|
"api.calls", "metric", nil, nil)
|
|
|
|
out := callAnalyzeErrorSurface(t, srv, map[string]any{})
|
|
throwers, _ := out["throwers"].([]any)
|
|
if len(throwers) != 1 {
|
|
t.Fatalf("expected 1 thrower, got %d", len(throwers))
|
|
}
|
|
row := throwers[0].(map[string]any)
|
|
msgs, _ := row["error_msgs"].([]any)
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("expected 2 error_msgs, got %d (%v)", len(msgs), msgs)
|
|
}
|
|
// Sorted alphabetically.
|
|
if msgs[0] != "bad token" || msgs[1] != "unauthorized" {
|
|
t.Errorf("error_msgs = %v, want [bad token, unauthorized] sorted", msgs)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeErrorSurface_ErrorMsgsOmittedWhenEmpty(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
g := srv.graph
|
|
addThrowsEdge(g, "f.go::DoThing", "external::error", "f.go", 10)
|
|
|
|
out := callAnalyzeErrorSurface(t, srv, map[string]any{})
|
|
throwers, _ := out["throwers"].([]any)
|
|
if len(throwers) != 1 {
|
|
t.Fatalf("expected 1 thrower, got %d", len(throwers))
|
|
}
|
|
row := throwers[0].(map[string]any)
|
|
if _, present := row["error_msgs"]; present {
|
|
t.Errorf("error_msgs should be omitted when empty, got %v", row["error_msgs"])
|
|
}
|
|
}
|