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
211 lines
7.5 KiB
Go
211 lines
7.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func TestParseFieldQuery(t *testing.T) {
|
|
cases := []struct {
|
|
raw string
|
|
want fieldQuery
|
|
}{
|
|
{"validate token", fieldQuery{Text: "validate token"}},
|
|
{"kind:function auth", fieldQuery{Text: "auth", Kind: "function"}},
|
|
{"lang:rust path:src/ parse", fieldQuery{Text: "parse", Lang: "rust", Path: "src/"}},
|
|
{"repo:gortex project:web Handler", fieldQuery{Text: "Handler", Repo: "gortex", Project: "web"}},
|
|
{"language:go kind:method,function run", fieldQuery{Text: "run", Lang: "go", Kind: "method,function"}},
|
|
// A non-field token with a colon stays in the free text.
|
|
{"pkg::Type lookup", fieldQuery{Text: "pkg::Type lookup"}},
|
|
{"https://example.com client", fieldQuery{Text: "https://example.com client"}},
|
|
// An unknown field name is left in the text verbatim.
|
|
{"author:alice fix", fieldQuery{Text: "author:alice fix"}},
|
|
// A field with an empty value is treated as plain text.
|
|
{"kind: thing", fieldQuery{Text: "kind: thing"}},
|
|
// A repeated field keeps the last value.
|
|
{"kind:function kind:method x", fieldQuery{Text: "x", Kind: "method"}},
|
|
// flavor: lifts out like kind:.
|
|
{"flavor:struct Foo", fieldQuery{Text: "Foo", Flavor: "struct"}},
|
|
// A pkg::Type token is not mis-parsed as a flavor clause.
|
|
{"pkg::Type flavor:enum", fieldQuery{Text: "pkg::Type", Flavor: "enum"}},
|
|
}
|
|
for _, tc := range cases {
|
|
got := parseFieldQuery(tc.raw)
|
|
if got != tc.want {
|
|
t.Errorf("parseFieldQuery(%q) = %+v, want %+v", tc.raw, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeLang(t *testing.T) {
|
|
cases := map[string]string{
|
|
"ts": "typescript",
|
|
"JS": "javascript",
|
|
" py ": "python",
|
|
"rs": "rust",
|
|
"go": "go",
|
|
"typescript": "typescript",
|
|
"": "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := normalizeLang(in); got != want {
|
|
t.Errorf("normalizeLang(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFieldQueryHasFieldFilters(t *testing.T) {
|
|
if (fieldQuery{Text: "x"}).hasFieldFilters() {
|
|
t.Errorf("plain text query must report no field filters")
|
|
}
|
|
if (fieldQuery{Project: "web"}).hasFieldFilters() {
|
|
t.Errorf("project: alone is scope, not a post-filter")
|
|
}
|
|
if (fieldQuery{Repo: "*"}).hasFieldFilters() {
|
|
t.Errorf("repo:* is a scope sentinel, not a post-filter")
|
|
}
|
|
for _, fq := range []fieldQuery{{Kind: "function"}, {Flavor: "struct"}, {Lang: "go"}, {Path: "src/"}, {Repo: "gortex"}} {
|
|
if !fq.hasFieldFilters() {
|
|
t.Errorf("%+v must report a field filter", fq)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplyFieldFilters(t *testing.T) {
|
|
nodes := []*graph.Node{
|
|
{ID: "a", Language: "go", FilePath: "internal/auth/token.go", RepoPrefix: "gortex"},
|
|
{ID: "b", Language: "rust", FilePath: "src/main.rs", RepoPrefix: "gortex"},
|
|
{ID: "c", Language: "go", FilePath: "cmd/main.go", RepoPrefix: "cloud"},
|
|
}
|
|
ids := func(ns []*graph.Node) []string {
|
|
out := make([]string, len(ns))
|
|
for i, n := range ns {
|
|
out[i] = n.ID
|
|
}
|
|
return out
|
|
}
|
|
|
|
if got := ids(applyFieldFilters(nodes, fieldQuery{Lang: "go"})); !equalStrings(got, []string{"a", "c"}) {
|
|
t.Errorf("lang:go = %v, want [a c]", got)
|
|
}
|
|
if got := ids(applyFieldFilters(nodes, fieldQuery{Lang: "rs"})); !equalStrings(got, []string{"b"}) {
|
|
t.Errorf("lang:rs (alias) = %v, want [b]", got)
|
|
}
|
|
if got := ids(applyFieldFilters(nodes, fieldQuery{Path: "internal/"})); !equalStrings(got, []string{"a"}) {
|
|
t.Errorf("path:internal/ = %v, want [a]", got)
|
|
}
|
|
if got := ids(applyFieldFilters(nodes, fieldQuery{Repo: "cloud"})); !equalStrings(got, []string{"c"}) {
|
|
t.Errorf("repo:cloud = %v, want [c]", got)
|
|
}
|
|
if got := ids(applyFieldFilters(nodes, fieldQuery{Lang: "go", Path: "cmd/"})); !equalStrings(got, []string{"c"}) {
|
|
t.Errorf("lang:go path:cmd/ = %v, want [c]", got)
|
|
}
|
|
if got := applyFieldFilters(nodes, fieldQuery{}); len(got) != 3 {
|
|
t.Errorf("no clauses must keep all nodes, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
// TestNameClauseFilter proves the name: clause both parses out of the query and
|
|
// post-filters nodes by a case-insensitive substring of the symbol name —
|
|
// "search for X but only nodes whose name contains Y".
|
|
func TestNameClauseFilter(t *testing.T) {
|
|
fq := parseFieldQuery("auth name:Handler kind:function")
|
|
if fq.Name != "Handler" {
|
|
t.Errorf("name clause not parsed: fq.Name=%q, want Handler", fq.Name)
|
|
}
|
|
if fq.Kind != "function" {
|
|
t.Errorf("kind clause lost: fq.Kind=%q", fq.Kind)
|
|
}
|
|
if fq.Text != "auth" {
|
|
t.Errorf("residual text=%q, want auth", fq.Text)
|
|
}
|
|
if !fq.hasFieldFilters() {
|
|
t.Error("hasFieldFilters should be true when name: is set")
|
|
}
|
|
|
|
nodes := []*graph.Node{
|
|
{ID: "a", Name: "AuthHandler", Kind: graph.KindType},
|
|
{ID: "b", Name: "authMiddleware", Kind: graph.KindFunction},
|
|
{ID: "c", Name: "RequestHandler", Kind: graph.KindType},
|
|
}
|
|
got := applyFieldFilters(nodes, fieldQuery{Name: "handler"})
|
|
ids := map[string]bool{}
|
|
for _, n := range got {
|
|
ids[n.ID] = true
|
|
}
|
|
if !ids["a"] || !ids["c"] {
|
|
t.Errorf("name:handler should keep AuthHandler + RequestHandler, got %v", ids)
|
|
}
|
|
if ids["b"] {
|
|
t.Error("name:handler should have dropped authMiddleware")
|
|
}
|
|
}
|
|
|
|
func TestApplyFlavorFilter(t *testing.T) {
|
|
nodes := []*graph.Node{
|
|
{ID: "s", Meta: map[string]any{"type_flavor": "struct"}},
|
|
{ID: "e", Meta: map[string]any{"type_flavor": "enum"}},
|
|
{ID: "react", Kind: graph.KindFunction, Meta: map[string]any{"ui_component": "react"}},
|
|
{ID: "svelte", Meta: map[string]any{"type_flavor": "component", "ui_component": "svelte"}},
|
|
{ID: "plain", Meta: map[string]any{}},
|
|
{ID: "nilmeta"},
|
|
}
|
|
ids := func(ns []*graph.Node) []string {
|
|
out := make([]string, len(ns))
|
|
for i, n := range ns {
|
|
out[i] = n.ID
|
|
}
|
|
return out
|
|
}
|
|
if got := ids(applyFlavorFilter(nodes, "struct")); !equalStrings(got, []string{"s"}) {
|
|
t.Errorf("flavor:struct = %v, want [s]", got)
|
|
}
|
|
// Case-insensitive on both sides.
|
|
if got := ids(applyFlavorFilter(nodes, "Struct")); !equalStrings(got, []string{"s"}) {
|
|
t.Errorf("flavor:Struct (case) = %v, want [s]", got)
|
|
}
|
|
// Comma union.
|
|
if got := ids(applyFlavorFilter(nodes, "struct,enum")); !equalStrings(got, []string{"s", "e"}) {
|
|
t.Errorf("flavor:struct,enum = %v, want [s e]", got)
|
|
}
|
|
// component bridges ui_component != "" and type_flavor == component.
|
|
if got := ids(applyFlavorFilter(nodes, "component")); !equalStrings(got, []string{"react", "svelte"}) {
|
|
t.Errorf("flavor:component = %v, want [react svelte]", got)
|
|
}
|
|
// Empty arg is a no-op.
|
|
if got := applyFlavorFilter(nodes, ""); len(got) != len(nodes) {
|
|
t.Errorf("empty flavor must keep all nodes, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
// TestReclassifyKindFlavor pins the codegraph-compat shim: a kind: value
|
|
// that is a flavor but not a real node kind routes to the flavor filter.
|
|
func TestReclassifyKindFlavor(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
wantKinds string
|
|
wantFlavors string
|
|
}{
|
|
{"function", "function", ""},
|
|
{"class", "", "class"},
|
|
{"struct", "", "struct"},
|
|
{"enum", "", "enum"},
|
|
{"component", "", "component"},
|
|
// interface / table / module are BOTH — keep the node-kind meaning.
|
|
{"interface", "interface", ""},
|
|
{"table", "table", ""},
|
|
{"module", "module", ""},
|
|
// Mixed list partitions into kinds and flavors.
|
|
{"function,class", "function", "class"},
|
|
// An unknown value (neither kind nor flavor) stays a kind (graceful).
|
|
{"widget", "widget", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
ks, fs := reclassifyKindFlavor(tc.in)
|
|
if ks != tc.wantKinds || fs != tc.wantFlavors {
|
|
t.Errorf("reclassifyKindFlavor(%q) = (%q,%q), want (%q,%q)", tc.in, ks, fs, tc.wantKinds, tc.wantFlavors)
|
|
}
|
|
}
|
|
}
|