项目文件夹

文件
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

387 行
12 KiB
Go

package mcp
import (
"encoding/json"
"strings"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/query"
)
func TestParseAssistMode(t *testing.T) {
cases := []struct {
in string
want assistMode
}{
{"", assistAuto},
{"auto", assistAuto},
{"AUTO", assistAuto},
{" auto ", assistAuto},
{"on", assistOn},
{"ON", assistOn},
{"yes", assistOn},
{"true", assistOn},
{"force", assistOn},
{"off", assistOff},
{"OFF", assistOff},
{"no", assistOff},
{"false", assistOff},
{"skip", assistOff},
{"deep", assistDeep},
{"DEEP", assistDeep},
{"verify", assistDeep},
{"body", assistDeep},
{"garbage", assistAuto},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{"assist": tc.in}
got := parseAssistMode(req)
assert.Equal(t, tc.want, got, "input=%q", tc.in)
})
}
}
func TestLooksNaturalLanguage(t *testing.T) {
cases := []struct {
name string
q string
want bool
}{
{"empty", "", false},
{"blanks", " ", false},
{"single token", "handler", false},
{"two tokens", "handle user", false},
{"qualified identifier", "pkg/foo bar baz", false},
{"camelCase token", "handleSomething for fun", false},
{"PascalCase token", "MyHandler tests pass", false},
{"dotted identifier", "foo.Bar baz qux", false},
{"snake_case identifier", "do_thing in cluster", false},
{"scoped identifier", "ns::Type does stuff", false},
{"NL with stop word", "where do we hash passwords", true},
{"NL plain 4 tokens", "validate token auth flow", true},
{"NL plain 3 tokens no stop word", "validate token auth", false},
{"NL with the", "the user login flow", true},
{"mixed identifier short-circuits stop word", "the handleAsk token", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := looksNaturalLanguage(tc.q)
assert.Equal(t, tc.want, got, "q=%q", tc.q)
})
}
}
func TestShouldEngageAssist(t *testing.T) {
// `on` always engages, regardless of shape.
assert.True(t, shouldEngageAssist(assistOn, "Foo"))
assert.True(t, shouldEngageAssist(assistOn, ""))
// `off` never engages.
assert.False(t, shouldEngageAssist(assistOff, "where do we hash"))
assert.False(t, shouldEngageAssist(assistOff, ""))
// `auto` defers to the heuristic.
assert.False(t, shouldEngageAssist(assistAuto, "handleAsk"))
assert.True(t, shouldEngageAssist(assistAuto, "where do we hash"))
// `deep` always engages — its whole purpose is opt-in verification
// for cases the caller knows are NL queries.
assert.True(t, shouldEngageAssist(assistDeep, "Foo"))
assert.True(t, shouldEngageAssist(assistDeep, "where do we hash"))
}
// TestDeferColdLoadForAssist covers the enrichment-aware first-load
// gate: an implicit assist request defers its local-model cold load
// only when the model is not yet loaded AND enrichment is in flight.
func TestDeferColdLoadForAssist(t *testing.T) {
cases := []struct {
name string
engage bool
modelLoaded bool
enrichBusy bool
wantEngage bool
wantDeferred bool
}{
{"not engaging stays off", false, false, true, false, false},
{"loaded model ignores busy", true, true, true, true, false},
{"not busy proceeds", true, false, false, true, false},
{"unloaded + busy defers", true, false, true, false, true},
{"loaded + not busy proceeds", true, true, false, true, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
engage, deferred := deferColdLoadForAssist(tc.engage, tc.modelLoaded, tc.enrichBusy)
assert.Equal(t, tc.wantEngage, engage, "engage")
assert.Equal(t, tc.wantDeferred, deferred, "deferred")
// Deferral and engagement are mutually exclusive.
assert.False(t, engage && deferred, "engage and deferred must never both be true")
})
}
}
func TestTruncateBody(t *testing.T) {
cases := []struct {
name string
src string
maxLines int
maxChars int
want string
}{
{"empty", "", 8, 600, ""},
{
"under both caps",
"a()\nb()\nc()",
8, 600,
"a()\nb()\nc()\n",
},
{
"blank lines skipped from line count",
"a()\n\nb()\n\nc()\n",
3, 600,
"a()\n\nb()\n\nc()\n…\n",
},
{
"line cap fires",
"l1\nl2\nl3\nl4\nl5",
3, 600,
"l1\nl2\nl3\n…\n",
},
{
"char cap fires after line cap",
strings.Repeat("X", 700),
8, 100,
strings.Repeat("X", 100) + "…\n",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := truncateBody(tc.src, tc.maxLines, tc.maxChars)
assert.Equal(t, tc.want, got)
})
}
}
func TestHasMixedCase(t *testing.T) {
assert.False(t, hasMixedCase("lower"))
assert.False(t, hasMixedCase("UPPER"))
assert.False(t, hasMixedCase(""))
assert.False(t, hasMixedCase("123"))
assert.True(t, hasMixedCase("camelCase"))
assert.True(t, hasMixedCase("PascalCase"))
}
func TestHasStopWord(t *testing.T) {
assert.True(t, hasStopWord([]string{"hello", "where", "world"}))
assert.True(t, hasStopWord([]string{"WHERE", "is", "x"}))
assert.False(t, hasStopWord([]string{"validate", "token", "auth"}))
assert.False(t, hasStopWord(nil))
}
// TestFetchAndMergeBM25_DedupesAcrossTerms verifies that when the
// same node matches multiple terms, it appears only once and keeps
// its primary-term position.
func TestFetchAndMergeBM25_DedupesAcrossTerms(t *testing.T) {
srv, _ := setupTestServer(t)
scope := query.QueryOptions{}
// Primary term that hits "helper".
primary := srv.engine.SearchSymbolsScoped("helper", 20, scope)
require.NotEmpty(t, primary)
// Merging with the same term as an "expansion" must produce the
// same list, not duplicates.
merged, primaryCount := fetchAndMergeBM25(srv.engine, "helper", []string{"helper"}, 20, scope)
assert.Equal(t, len(primary), primaryCount)
assert.Equal(t, idsOf(primary), idsOf(merged))
}
// TestFetchAndMergeBM25_CombinedQueryUnionIsSuperset is the load-bearing
// guard for the "combine expansion terms into one BM25 query"
// optimisation. The merged result MUST contain at least every node
// that a per-term fan-out would have returned — otherwise switching
// from N BM25 calls to (primary + combined) drops candidates the
// rerank pipeline used to see. Exact-name rescue (the per-fragment
// FindNodesByNames step) is what makes this hold for tokenisation
// edge cases like PascalCase concatenated names that BM25 misses.
func TestFetchAndMergeBM25_CombinedQueryUnionIsSuperset(t *testing.T) {
srv, _ := setupTestServer(t)
scope := query.QueryOptions{}
// Per-term fan-out (the OLD behaviour). For each fragment, run
// the engine search separately and collect every distinct node ID
// it surfaces — this is the worst-case "no candidate may be
// dropped by collapsing into one query" set.
terms := []string{"helper", "main"}
unionExpected := map[string]bool{}
for _, t := range terms {
for _, n := range srv.engine.SearchSymbolsScoped(t, 20, scope) {
unionExpected[n.ID] = true
}
}
require.NotEmpty(t, unionExpected, "per-term fan-out produced nothing — test corpus drifted")
// New behaviour: primary + combined-OR + per-fragment exact-name
// rescue, all driven by fetchAndMergeBM25.
merged, _ := fetchAndMergeBM25(srv.engine, terms[0], terms[1:], 20, scope)
mergedSet := map[string]bool{}
for _, n := range merged {
mergedSet[n.ID] = true
}
for id := range unionExpected {
require.True(t, mergedSet[id], "merged result missing per-term hit %q", id)
}
}
// TestFetchAndMergeBM25_ExactNameRescuePreserved is the regression
// guard for the soup-mode + PascalCase fragment case that per-term
// fan-out used to handle implicitly. When BM25 tokenisation misses
// a fragment ("BillingInvoice" tokenises to one term `billinginvoice`
// which the camelCase-split index doesn't carry), the per-fragment
// FindNodesByNames rescue MUST still surface its exact-name node.
// This mirrors the failure mode TestSearchSymbols_PathScoping caught
// when soup-split fragments first went through the combined query
// path.
func TestFetchAndMergeBM25_ExactNameRescuePreserved(t *testing.T) {
srv, _ := setupTestServer(t)
// The test corpus carries no PascalCase-concatenated names by
// default, so add three synthetic ones — these never reach BM25
// (we don't re-index it for the test) but they are what the
// rescue step has to surface.
for path, name := range map[string]string{
"svc/billing/Invoice.go": "BillingInvoice",
"svc/auth/Login.go": "AuthLogin",
"libs/money/Amount.go": "MoneyAmount",
} {
id := path + "::" + name
srv.graph.AddNode(&graph.Node{
ID: id, Kind: graph.KindFunction, Name: name,
FilePath: path, StartLine: 1, EndLine: 5, Language: "go",
})
}
terms := []string{"BillingInvoice", "AuthLogin", "MoneyAmount"}
merged, _ := fetchAndMergeBM25(srv.engine, terms[0], terms[1:], 20, query.QueryOptions{})
mergedNames := map[string]bool{}
for _, n := range merged {
mergedNames[n.Name] = true
}
for _, want := range terms {
require.True(t, mergedNames[want], "exact-name rescue dropped %q from merged result", want)
}
}
// TestFetchAndMergeBM25_AppendsNewMatches verifies that expansion
// terms bring in additional candidates the primary term missed.
func TestFetchAndMergeBM25_AppendsNewMatches(t *testing.T) {
srv, _ := setupTestServer(t)
scope := query.QueryOptions{}
primary := srv.engine.SearchSymbolsScoped("helper", 20, scope)
merged, primaryCount := fetchAndMergeBM25(srv.engine, "helper", []string{"main"}, 20, scope)
assert.Equal(t, len(primary), primaryCount)
primaryIDs := idsOf(primary)
mergedIDs := idsOf(merged)
// Every primary ID appears in the merged set, in primary order
// at the head.
require.GreaterOrEqual(t, len(mergedIDs), len(primaryIDs))
for i, id := range primaryIDs {
assert.Equal(t, id, mergedIDs[i], "primary order broken at index %d", i)
}
// The merge brought in at least one "main"-matched node.
assert.Greater(t, len(mergedIDs), len(primaryIDs))
}
// TestSearchSymbols_AssistArgPassThrough verifies the new assist arg
// parses and doesn't break the no-LLM path. Without a service the
// gate always reads as "no engage" regardless of mode, so results
// match the no-assist baseline exactly.
func TestSearchSymbols_AssistArgPassThrough(t *testing.T) {
srv, _ := setupTestServer(t)
for _, mode := range []string{"", "auto", "on", "off"} {
t.Run("assist="+mode, func(t *testing.T) {
args := map[string]any{"query": "helper"}
if mode != "" {
args["assist"] = mode
}
result := callTool(t, srv, "search_symbols", args)
require.False(t, result.IsError, "search failed for mode=%q", mode)
text := result.Content[0].(mcplib.TextContent).Text
var resp map[string]any
require.NoError(t, json.Unmarshal([]byte(text), &resp))
results := resp["results"].([]any)
require.NotEmpty(t, results, "no results for mode=%q", mode)
})
}
}
func TestPrioritizeCallables(t *testing.T) {
// Mixed input: BM25-ranked, with callable kinds interleaved among
// param/field/type nodes. Expected output: callables in their
// original order, then everything else in its original order.
nodes := []*graph.Node{
{ID: "p1", Kind: graph.KindParam},
{ID: "f1", Kind: graph.KindFunction},
{ID: "fld1", Kind: graph.KindField},
{ID: "m1", Kind: graph.KindMethod},
{ID: "t1", Kind: graph.KindType},
{ID: "f2", Kind: graph.KindFunction},
}
got := prioritizeCallables(nodes)
want := []string{"f1", "m1", "f2", "p1", "fld1", "t1"}
gotIDs := idsOf(got)
if len(gotIDs) != len(want) {
t.Fatalf("length mismatch: got=%v want=%v", gotIDs, want)
}
for i := range want {
if gotIDs[i] != want[i] {
t.Errorf("position %d: got=%q want=%q", i, gotIDs[i], want[i])
}
}
}
func TestPrioritizeCallables_AllCallable(t *testing.T) {
nodes := []*graph.Node{
{ID: "a", Kind: graph.KindFunction},
{ID: "b", Kind: graph.KindMethod},
}
got := prioritizeCallables(nodes)
if got[0].ID != "a" || got[1].ID != "b" {
t.Fatalf("order changed when no reordering needed: %v", idsOf(got))
}
}
func TestPrioritizeCallables_NoCallable(t *testing.T) {
nodes := []*graph.Node{
{ID: "a", Kind: graph.KindParam},
{ID: "b", Kind: graph.KindField},
}
got := prioritizeCallables(nodes)
if got[0].ID != "a" || got[1].ID != "b" {
t.Fatalf("order changed when no callables present: %v", idsOf(got))
}
}
func idsOf(nodes []*graph.Node) []string {
out := make([]string, len(nodes))
for i, n := range nodes {
out[i] = n.ID
}
return out
}