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
168 lines
6.3 KiB
Go
168 lines
6.3 KiB
Go
package mcp
|
||
|
||
import (
|
||
"path/filepath"
|
||
"slices"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestKeywordTokens_DropsStopWordsAndGenerics(t *testing.T) {
|
||
got := keywordTokens("how do we validate the auth token function")
|
||
// Function words (how, do, we, the) and generic nouns (function)
|
||
// are dropped; the meaningful keywords survive.
|
||
for _, want := range []string{"validate", "auth", "token"} {
|
||
if !slices.Contains(got, want) {
|
||
t.Errorf("keywordTokens missing %q; got %v", want, got)
|
||
}
|
||
}
|
||
for _, drop := range []string{"how", "do", "the", "function", "we"} {
|
||
if slices.Contains(got, drop) {
|
||
t.Errorf("keywordTokens should have dropped %q; got %v", drop, got)
|
||
}
|
||
}
|
||
// camelCase queries split into component tokens.
|
||
cc := keywordTokens("validateAuthToken")
|
||
for _, want := range []string{"validate", "auth", "token"} {
|
||
if !slices.Contains(cc, want) {
|
||
t.Errorf("camelCase keyword split missing %q; got %v", want, cc)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestKeywordBoost_CrossQueryInheritance is the core feature test: a
|
||
// symbol recorded under one phrasing is inherited by a later query
|
||
// that shares keywords but is phrased differently.
|
||
func TestKeywordBoost_CrossQueryInheritance(t *testing.T) {
|
||
cm := &comboManager{now: func() int64 { return 1 }}
|
||
|
||
// Train: "validate auth token" leads to sym-auth, recorded enough
|
||
// times to clear the per-keyword min-hits gate.
|
||
for i := 0; i < 3; i++ {
|
||
cm.Record("validate auth token", "sym-auth")
|
||
}
|
||
|
||
// A differently-phrased query that shares the auth/token keywords.
|
||
// Its whole-query key never matched, so the exact-combo path is
|
||
// silent...
|
||
assert.Nil(t, cm.BoostMap("check auth token here"),
|
||
"a never-seen whole query should have no exact-combo boost")
|
||
|
||
// ...but the per-keyword path inherits the association.
|
||
kw := cm.KeywordBoostMap("check auth token here")
|
||
require.NotNil(t, kw, "keyword boost should inherit across phrasings")
|
||
assert.Greater(t, kw["sym-auth"], 1.0,
|
||
"sym-auth should be boosted via its auth/token keyword associations")
|
||
}
|
||
|
||
// TestKeywordBoost_CoverageScaling confirms a symbol matching more of
|
||
// the query's keywords gets a larger boost than one matching fewer.
|
||
func TestKeywordBoost_CoverageScaling(t *testing.T) {
|
||
cm := &comboManager{now: func() int64 { return 1 }}
|
||
// sym-full is recorded under all three keywords; sym-partial under
|
||
// only one. Record each enough to clear the gate.
|
||
for i := 0; i < 4; i++ {
|
||
cm.Record("parse encode validate", "sym-full")
|
||
cm.Record("validate", "sym-partial")
|
||
}
|
||
kw := cm.KeywordBoostMap("parse encode validate")
|
||
require.NotNil(t, kw)
|
||
require.Contains(t, kw, "sym-full")
|
||
require.Contains(t, kw, "sym-partial")
|
||
assert.Greater(t, kw["sym-full"], kw["sym-partial"],
|
||
"a symbol covering more query keywords should out-boost one covering fewer")
|
||
}
|
||
|
||
// TestKeywordBoost_CoverageIsLinear pins the boost above 1.0 to scale
|
||
// linearly with coverage when per-keyword strength is equal: a symbol
|
||
// matching 1 of 3 keywords earns ~1/3 of the full-coverage boost. Guards
|
||
// against the super-linear (coverage-times-summed-hits) regression.
|
||
func TestKeywordBoost_CoverageIsLinear(t *testing.T) {
|
||
cm := &comboManager{now: func() int64 { return 1 }}
|
||
// Equal per-keyword strength: sym-full under all 3 keywords, sym-partial
|
||
// under 1, each recorded the same number of times.
|
||
for i := 0; i < 4; i++ {
|
||
cm.Record("parse encode validate", "sym-full")
|
||
cm.Record("validate", "sym-partial")
|
||
}
|
||
kw := cm.KeywordBoostMap("parse encode validate")
|
||
require.NotNil(t, kw)
|
||
full := kw["sym-full"] - 1.0
|
||
partial := kw["sym-partial"] - 1.0
|
||
require.Greater(t, full, 0.0)
|
||
require.Greater(t, partial, 0.0)
|
||
// 3 keywords ⇒ full coverage boost is ~3× the 1-of-3 boost.
|
||
assert.InDelta(t, 3.0, full/partial, 0.05,
|
||
"partial-coverage boost should be ~1/N of full coverage (linear, not super-linear)")
|
||
}
|
||
|
||
// TestKeywordBoost_ExactQueryDominates confirms the exact whole-query
|
||
// combo boost out-ranks a keyword-only boost when both fire -- the
|
||
// keyword boost is capped below comboMaxBoost.
|
||
func TestKeywordBoost_ExactQueryDominates(t *testing.T) {
|
||
cm := &comboManager{now: func() int64 { return 1 }}
|
||
// Heavily train an exact query -> exact boost saturates near
|
||
// comboMaxBoost.
|
||
for i := 0; i < 50; i++ {
|
||
cm.Record("auth token flow", "sym-x")
|
||
}
|
||
exact := cm.BoostMap("auth token flow")
|
||
keyword := cm.KeywordBoostMap("auth token flow")
|
||
require.NotNil(t, exact)
|
||
require.NotNil(t, keyword)
|
||
assert.Greater(t, exact["sym-x"], keyword["sym-x"],
|
||
"exact whole-query combo must dominate the keyword-only boost")
|
||
assert.LessOrEqual(t, keyword["sym-x"], keywordMaxBoost+1e-9,
|
||
"keyword boost must stay under its cap")
|
||
}
|
||
|
||
// TestKeywordBoost_BelowGate confirms a keyword association below the
|
||
// min-hits gate does not boost.
|
||
func TestKeywordBoost_BelowGate(t *testing.T) {
|
||
cm := &comboManager{now: func() int64 { return 1 }}
|
||
cm.Record("auth token", "sym-q") // a single hit -- below keywordMinHits
|
||
assert.Nil(t, cm.KeywordBoostMap("auth token"),
|
||
"a single keyword hit should not clear the gate")
|
||
}
|
||
|
||
// TestKeywordBoost_Decay confirms stale keyword associations are
|
||
// reaped on access, just like the exact-combo store.
|
||
func TestKeywordBoost_Decay(t *testing.T) {
|
||
now := int64(1_000_000_000)
|
||
cm := &comboManager{mode: ModeAI, now: func() int64 { return now }}
|
||
for i := 0; i < 3; i++ {
|
||
cm.Record("auth token", "sym-stale")
|
||
}
|
||
require.NotNil(t, cm.KeywordBoostMap("auth token"), "fresh association should boost")
|
||
|
||
// Jump past the AI max-age window.
|
||
now += comboMaxAgeAISec + 1
|
||
assert.Nil(t, cm.KeywordBoostMap("auth token"),
|
||
"a keyword association past the decay window must be reaped")
|
||
}
|
||
|
||
// TestKeywordBoost_Persistence confirms the keyword store survives a
|
||
// manager reload from disk.
|
||
func TestKeywordBoost_Persistence(t *testing.T) {
|
||
cacheDir := t.TempDir()
|
||
repoPath := filepath.Join(t.TempDir(), "repo")
|
||
|
||
cm := newComboManager(cacheDir, repoPath, ModeAI)
|
||
for i := 0; i < 3; i++ {
|
||
cm.Record("validate auth token", "sym-persist")
|
||
}
|
||
|
||
cm2 := newComboManager(cacheDir, repoPath, ModeAI)
|
||
kw := cm2.KeywordBoostMap("check auth token now")
|
||
require.NotNil(t, kw, "keyword associations should survive a reload")
|
||
assert.Greater(t, kw["sym-persist"], 1.0)
|
||
}
|
||
|
||
// TestKeywordBoost_NilSafe confirms a nil manager is safe.
|
||
func TestKeywordBoost_NilSafe(t *testing.T) {
|
||
var cm *comboManager
|
||
assert.Nil(t, cm.KeywordBoostMap("auth token"))
|
||
}
|