package mcp import ( "fmt" "os" "path/filepath" "strings" "testing" mcplib "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/zzet/gortex/internal/config" "github.com/zzet/gortex/internal/graph" "github.com/zzet/gortex/internal/indexer" "github.com/zzet/gortex/internal/query" ) // setupDeterminismServer indexes a fixture deliberately full of // interchangeable symbols — 40 identically-shaped Handler functions // that each call shared() — so BM25 scores and the structural rerank // signals tie en masse. Any reliance on Go's randomised map iteration // then surfaces as run-to-run variance. func setupDeterminismServer(t *testing.T) *Server { t.Helper() dir := t.TempDir() var src strings.Builder src.WriteString("package handlers\n\nfunc shared() {}\n\n") for i := 0; i < 40; i++ { fmt.Fprintf(&src, "func Handler%02d() { shared() }\n", i) } src.WriteString("\nfunc dispatch() {\n") for i := 0; i < 40; i++ { fmt.Fprintf(&src, "\tHandler%02d()\n", i) } src.WriteString("}\n") require.NoError(t, os.WriteFile(filepath.Join(dir, "handlers.go"), []byte(src.String()), 0o644)) g := graph.New() reg := testRegistry() cfg := config.Default() idx := indexer.New(g, reg, cfg.Index, zap.NewNop()) _, err := idx.Index(dir) require.NoError(t, err) eng := query.NewEngine(g) srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil) srv.RunAnalysis() return srv } func stableToolText(t *testing.T, srv *Server, tool string, args map[string]any) string { t.Helper() res := callTool(t, srv, tool, args) require.Falsef(t, res.IsError, "%s returned an error", tool) require.NotEmpty(t, res.Content) return res.Content[0].(mcplib.TextContent).Text } // TestDeterminism_ReadToolsByteIdentical asserts that repeated // invocations of the result-set tools produce byte-identical output // on a fixture engineered to tie BM25 and structural signals. func TestDeterminism_ReadToolsByteIdentical(t *testing.T) { srv := setupDeterminismServer(t) const runs = 25 assertStable := func(name, tool string, args map[string]any) { t.Run(name, func(t *testing.T) { first := stableToolText(t, srv, tool, args) for run := 1; run < runs; run++ { if got := stableToolText(t, srv, tool, args); got != first { t.Fatalf("%s run %d is not byte-identical to run 0", tool, run) } } }) } assertStable("search_concept", "search_symbols", map[string]any{"query": "handler"}) assertStable("search_identifier", "search_symbols", map[string]any{"query": "Handler17"}) assertStable("get_repo_outline", "get_repo_outline", map[string]any{}) assertStable("get_file_summary", "get_file_summary", map[string]any{"path": "handlers.go"}) assertStable("suggest_queries", "suggest_queries", map[string]any{}) // find_usages over a 40-caller symbol exercises the usage-map // extraction order most heavily. sharedID := "" for _, id := range resultIDs(searchSymbolsResp(t, srv, "shared")) { if strings.HasSuffix(id, "::shared") { sharedID = id } } require.NotEmpty(t, sharedID, "could not resolve the shared() symbol ID") assertStable("find_usages", "find_usages", map[string]any{"id": sharedID}) }