mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-09 23:59:40 +00:00
4f4b4ac70f
- [x] Add language-specific chunkers with AST parsing (Go, Python, TypeScript) - [x] Implement chunking manager to dispatch files to appropriate chunkers - [x] Integrate code chunks into vector sync for semantic search - [x] Add tree-sitter dependency for Python/TypeScript parsing - [x] Reorder struct fields for consistency across codebase - [x] Rename error variables to follow Go conventions (err → unmarshalErr, etc.) - [x] Add code chunk metadata to vector documents (language, symbol name, line ranges) - [x] Update worker service to initialize chunking pipeline with all three languages
519 lines
14 KiB
Go
519 lines
14 KiB
Go
// Package expansion provides context-aware query expansion for improved search recall.
|
|
package expansion
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// ExpanderSuite tests the Expander functionality.
|
|
type ExpanderSuite struct {
|
|
suite.Suite
|
|
expander *Expander
|
|
}
|
|
|
|
func TestExpanderSuite(t *testing.T) {
|
|
suite.Run(t, new(ExpanderSuite))
|
|
}
|
|
|
|
func (s *ExpanderSuite) SetupTest() {
|
|
// Create expander without embedding service for basic tests
|
|
s.expander = NewExpander(nil)
|
|
}
|
|
|
|
// TestNewExpander tests expander creation.
|
|
func (s *ExpanderSuite) TestNewExpander() {
|
|
e := NewExpander(nil)
|
|
s.NotNil(e)
|
|
s.NotNil(e.intentPatterns)
|
|
s.Nil(e.embedSvc)
|
|
}
|
|
|
|
// TestDetectIntent tests intent detection.
|
|
func (s *ExpanderSuite) TestDetectIntent() {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
expected QueryIntent
|
|
}{
|
|
// Question intents
|
|
{"how_question", "how do I implement auth?", IntentQuestion},
|
|
{"why_question", "why does this fail?", IntentError}, // "fail" triggers error first
|
|
{"what_question", "what is the purpose of this function?", IntentQuestion},
|
|
{"question_mark", "the handler for auth?", IntentQuestion},
|
|
{"explain", "explain the architecture", IntentQuestion},
|
|
|
|
// Error intents
|
|
{"error_word", "authentication error in login", IntentError},
|
|
{"bug_word", "bug in user registration", IntentError},
|
|
{"fix_word", "fix the memory leak", IntentError},
|
|
{"not_working", "login not working", IntentError},
|
|
{"crash", "application crash on startup", IntentError},
|
|
|
|
// Implementation intents
|
|
{"implement", "implement user authentication", IntentImplementation},
|
|
{"add_feature", "add new endpoint for users", IntentImplementation},
|
|
{"create", "create a handler for uploads", IntentImplementation},
|
|
{"function", "function to validate input", IntentImplementation},
|
|
|
|
// Architecture intents
|
|
{"architecture", "architecture of the system", IntentArchitecture},
|
|
{"design", "design pattern for observers", IntentArchitecture},
|
|
{"component", "component structure", IntentArchitecture},
|
|
{"flow", "data flow in the pipeline", IntentArchitecture},
|
|
|
|
// General intents
|
|
{"general", "user authentication", IntentGeneral},
|
|
{"empty", "", IntentGeneral},
|
|
{"simple", "database", IntentGeneral},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
result := s.expander.DetectIntent(tt.query)
|
|
s.Equal(tt.expected, result, "Query: %s", tt.query)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExpand tests basic query expansion.
|
|
func (s *ExpanderSuite) TestExpand() {
|
|
ctx := context.Background()
|
|
cfg := DefaultConfig()
|
|
cfg.EnableVocabularyExpansion = false // Disable for unit test
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
expectedIntent QueryIntent
|
|
minExpansions int
|
|
hasOriginal bool
|
|
}{
|
|
{name: "question", query: "how do I implement auth", expectedIntent: IntentQuestion, minExpansions: 1, hasOriginal: true},
|
|
{name: "error", query: "fix the bug in login", expectedIntent: IntentError, minExpansions: 1, hasOriginal: true},
|
|
{name: "implementation", query: "implement user handler", expectedIntent: IntentImplementation, minExpansions: 1, hasOriginal: true},
|
|
{name: "architecture", query: "architecture design", expectedIntent: IntentArchitecture, minExpansions: 1, hasOriginal: true},
|
|
{name: "general", query: "database connection", expectedIntent: IntentGeneral, minExpansions: 1, hasOriginal: true},
|
|
{name: "empty", query: "", expectedIntent: IntentGeneral, minExpansions: 0, hasOriginal: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
expansions := s.expander.Expand(ctx, tt.query, cfg)
|
|
|
|
if tt.minExpansions == 0 {
|
|
s.Empty(expansions)
|
|
return
|
|
}
|
|
|
|
s.GreaterOrEqual(len(expansions), tt.minExpansions)
|
|
|
|
if tt.hasOriginal {
|
|
// First expansion should be the original
|
|
s.Equal(tt.query, expansions[0].Query)
|
|
s.Equal(1.0, expansions[0].Weight)
|
|
s.Equal("original", expansions[0].Source)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExpandWithConfig tests expansion with custom config.
|
|
func (s *ExpanderSuite) TestExpandWithConfig() {
|
|
ctx := context.Background()
|
|
|
|
cfg := Config{
|
|
MaxExpansions: 2,
|
|
MinSimilarity: 0.7,
|
|
EnableVocabularyExpansion: false,
|
|
}
|
|
|
|
expansions := s.expander.Expand(ctx, "how to implement authentication", cfg)
|
|
s.LessOrEqual(len(expansions), cfg.MaxExpansions)
|
|
}
|
|
|
|
// TestExpandDeduplication tests that duplicates are removed.
|
|
func (s *ExpanderSuite) TestExpandDeduplication() {
|
|
ctx := context.Background()
|
|
cfg := DefaultConfig()
|
|
cfg.EnableVocabularyExpansion = false
|
|
|
|
// Query that might generate duplicate expansions
|
|
query := "how to fix authentication"
|
|
expansions := s.expander.Expand(ctx, query, cfg)
|
|
|
|
// Check for duplicates
|
|
seen := make(map[string]bool)
|
|
for _, exp := range expansions {
|
|
normalized := exp.Query
|
|
s.False(seen[normalized], "Duplicate expansion found: %s", exp.Query)
|
|
seen[normalized] = true
|
|
}
|
|
}
|
|
|
|
// TestExtractKeyTerms tests key term extraction.
|
|
func TestExtractKeyTerms(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "simple",
|
|
query: "user authentication handler",
|
|
expected: []string{"user", "authentication", "handler"},
|
|
},
|
|
{
|
|
name: "with_stop_words",
|
|
query: "how to implement the user login",
|
|
expected: []string{"implement", "user", "login"},
|
|
},
|
|
{
|
|
name: "with_punctuation",
|
|
query: "fix the bug, please!",
|
|
expected: []string{"fix", "bug", "please"},
|
|
},
|
|
{
|
|
name: "empty",
|
|
query: "",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "only_stop_words",
|
|
query: "the a an is are",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "short_words_filtered",
|
|
query: "a b c auth",
|
|
expected: []string{"auth"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := extractKeyTerms(tt.query)
|
|
if tt.expected == nil {
|
|
assert.Empty(t, result)
|
|
} else {
|
|
assert.Equal(t, tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMakeDeclarative tests question to declarative conversion.
|
|
func TestMakeDeclarative(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "how_do_i",
|
|
query: "how do I implement auth?",
|
|
expected: "implement auth",
|
|
},
|
|
{
|
|
name: "how_to",
|
|
query: "how to fix the bug",
|
|
expected: "fix the bug",
|
|
},
|
|
{
|
|
name: "what_is",
|
|
query: "what is the purpose of this?",
|
|
expected: "the purpose of this",
|
|
},
|
|
{
|
|
name: "why_does",
|
|
query: "why does this fail?",
|
|
expected: "this fail",
|
|
},
|
|
{
|
|
name: "already_declarative",
|
|
query: "user authentication",
|
|
expected: "user authentication",
|
|
},
|
|
{
|
|
name: "question_mark_only",
|
|
query: "authentication?",
|
|
expected: "authentication",
|
|
},
|
|
{
|
|
name: "case_insensitive",
|
|
query: "How To Fix Auth?",
|
|
expected: "Fix Auth",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := makeDeclarative(tt.query)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeduplicateExpansions tests deduplication.
|
|
func TestDeduplicateExpansions(t *testing.T) {
|
|
expansions := []ExpandedQuery{
|
|
{Query: "auth handler", Weight: 1.0},
|
|
{Query: "AUTH HANDLER", Weight: 0.8}, // Duplicate (case insensitive)
|
|
{Query: "auth handler ", Weight: 0.7}, // Duplicate (whitespace)
|
|
{Query: "user auth", Weight: 0.6},
|
|
}
|
|
|
|
result := deduplicateExpansions(expansions)
|
|
assert.Len(t, result, 2) // "auth handler" and "user auth"
|
|
assert.Equal(t, "auth handler", result[0].Query)
|
|
assert.Equal(t, 1.0, result[0].Weight) // First one preserved
|
|
assert.Equal(t, "user auth", result[1].Query)
|
|
}
|
|
|
|
// TestCosineSimilarity tests cosine similarity calculation.
|
|
func TestCosineSimilarity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
a []float32
|
|
b []float32
|
|
expected float64
|
|
delta float64
|
|
}{
|
|
{
|
|
name: "identical_vectors",
|
|
a: []float32{1, 0, 0},
|
|
b: []float32{1, 0, 0},
|
|
expected: 1.0,
|
|
delta: 0.001,
|
|
},
|
|
{
|
|
name: "orthogonal_vectors",
|
|
a: []float32{1, 0, 0},
|
|
b: []float32{0, 1, 0},
|
|
expected: 0.0,
|
|
delta: 0.001,
|
|
},
|
|
{
|
|
name: "opposite_vectors",
|
|
a: []float32{1, 0, 0},
|
|
b: []float32{-1, 0, 0},
|
|
expected: -1.0,
|
|
delta: 0.001,
|
|
},
|
|
{
|
|
name: "similar_vectors",
|
|
a: []float32{1, 1, 0},
|
|
b: []float32{1, 0, 0},
|
|
expected: 0.707,
|
|
delta: 0.01,
|
|
},
|
|
{
|
|
name: "empty_vectors",
|
|
a: []float32{},
|
|
b: []float32{},
|
|
expected: 0.0,
|
|
delta: 0.001,
|
|
},
|
|
{
|
|
name: "different_lengths",
|
|
a: []float32{1, 0},
|
|
b: []float32{1, 0, 0},
|
|
expected: 0.0,
|
|
delta: 0.001,
|
|
},
|
|
{
|
|
name: "zero_vector",
|
|
a: []float32{0, 0, 0},
|
|
b: []float32{1, 1, 1},
|
|
expected: 0.0,
|
|
delta: 0.001,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := cosineSimilarity(tt.a, tt.b)
|
|
assert.InDelta(t, tt.expected, result, tt.delta)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDefaultConfig tests default configuration.
|
|
func TestDefaultConfig(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
|
|
assert.Equal(t, 4, cfg.MaxExpansions)
|
|
assert.Equal(t, 0.5, cfg.MinSimilarity)
|
|
assert.True(t, cfg.EnableVocabularyExpansion)
|
|
}
|
|
|
|
// TestExpandedQueryStruct tests ExpandedQuery struct.
|
|
func TestExpandedQueryStruct(t *testing.T) {
|
|
eq := ExpandedQuery{
|
|
Query: "test query",
|
|
Weight: 0.85,
|
|
Source: "vocabulary:auth",
|
|
Intent: IntentQuestion,
|
|
}
|
|
|
|
assert.Equal(t, "test query", eq.Query)
|
|
assert.Equal(t, 0.85, eq.Weight)
|
|
assert.Equal(t, "vocabulary:auth", eq.Source)
|
|
assert.Equal(t, IntentQuestion, eq.Intent)
|
|
}
|
|
|
|
// TestVocabEntry tests VocabEntry struct.
|
|
func TestVocabEntry(t *testing.T) {
|
|
ve := VocabEntry{
|
|
Term: "authentication",
|
|
Weight: 0.9,
|
|
Source: "concept",
|
|
}
|
|
|
|
assert.Equal(t, "authentication", ve.Term)
|
|
assert.Equal(t, 0.9, ve.Weight)
|
|
assert.Equal(t, "concept", ve.Source)
|
|
}
|
|
|
|
// TestIntentConstants tests intent constant values.
|
|
func TestIntentConstants(t *testing.T) {
|
|
assert.Equal(t, QueryIntent("question"), IntentQuestion)
|
|
assert.Equal(t, QueryIntent("error"), IntentError)
|
|
assert.Equal(t, QueryIntent("implementation"), IntentImplementation)
|
|
assert.Equal(t, QueryIntent("architecture"), IntentArchitecture)
|
|
assert.Equal(t, QueryIntent("general"), IntentGeneral)
|
|
}
|
|
|
|
// TestTruncate tests the truncate helper.
|
|
func TestTruncate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
maxLen int
|
|
}{
|
|
{name: "short", input: "hello", maxLen: 10, expected: "hello"},
|
|
{name: "exact", input: "hello", maxLen: 5, expected: "hello"},
|
|
{name: "long", input: "hello world", maxLen: 5, expected: "hello..."},
|
|
{name: "empty", input: "", maxLen: 10, expected: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := truncate(tt.input, tt.maxLen)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSqrt tests the sqrt helper.
|
|
func TestSqrt(t *testing.T) {
|
|
tests := []struct {
|
|
input float64
|
|
expected float64
|
|
delta float64
|
|
}{
|
|
{4.0, 2.0, 0.001},
|
|
{9.0, 3.0, 0.001},
|
|
{16.0, 4.0, 0.001},
|
|
{2.0, 1.414, 0.01},
|
|
{0.0, 0.0, 0.001},
|
|
{-1.0, 0.0, 0.001},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
result := sqrt(tt.input)
|
|
assert.InDelta(t, tt.expected, result, tt.delta)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExpandByIntentError tests error intent expansion.
|
|
func (s *ExpanderSuite) TestExpandByIntentError() {
|
|
expansions := s.expander.expandByIntent("fix authentication bug", IntentError)
|
|
s.NotEmpty(expansions)
|
|
|
|
// Should have solution-oriented expansion
|
|
hasSolution := false
|
|
for _, exp := range expansions {
|
|
if exp.Source == "intent:solution" {
|
|
hasSolution = true
|
|
break
|
|
}
|
|
}
|
|
s.True(hasSolution)
|
|
}
|
|
|
|
// TestExpandByIntentQuestion tests question intent expansion.
|
|
func (s *ExpanderSuite) TestExpandByIntentQuestion() {
|
|
expansions := s.expander.expandByIntent("how do I implement auth", IntentQuestion)
|
|
s.NotEmpty(expansions)
|
|
|
|
// Should have declarative expansion
|
|
hasDeclarative := false
|
|
for _, exp := range expansions {
|
|
if exp.Source == "intent:declarative" {
|
|
hasDeclarative = true
|
|
break
|
|
}
|
|
}
|
|
s.True(hasDeclarative)
|
|
}
|
|
|
|
// TestExpandByIntentImplementation tests implementation intent expansion.
|
|
func (s *ExpanderSuite) TestExpandByIntentImplementation() {
|
|
expansions := s.expander.expandByIntent("implement user handler", IntentImplementation)
|
|
s.NotEmpty(expansions)
|
|
|
|
// Should have how expansion
|
|
hasHow := false
|
|
for _, exp := range expansions {
|
|
if exp.Source == "intent:how" {
|
|
hasHow = true
|
|
break
|
|
}
|
|
}
|
|
s.True(hasHow)
|
|
}
|
|
|
|
// TestExpandByIntentArchitecture tests architecture intent expansion.
|
|
func (s *ExpanderSuite) TestExpandByIntentArchitecture() {
|
|
expansions := s.expander.expandByIntent("system architecture design", IntentArchitecture)
|
|
s.NotEmpty(expansions)
|
|
|
|
// Should have design expansion
|
|
hasDesign := false
|
|
for _, exp := range expansions {
|
|
if exp.Source == "intent:design" {
|
|
hasDesign = true
|
|
break
|
|
}
|
|
}
|
|
s.True(hasDesign)
|
|
}
|
|
|
|
// TestExpandByIntentGeneral tests general intent returns no expansions.
|
|
func (s *ExpanderSuite) TestExpandByIntentGeneral() {
|
|
expansions := s.expander.expandByIntent("database", IntentGeneral)
|
|
s.Empty(expansions) // General intent doesn't add intent-based expansions
|
|
}
|
|
|
|
// TestEmptyVocabulary tests expansion with empty vocabulary.
|
|
func (s *ExpanderSuite) TestEmptyVocabulary() {
|
|
ctx := context.Background()
|
|
expansions := s.expander.expandByVocabulary(ctx, "test query", 0.5)
|
|
s.Empty(expansions)
|
|
}
|
|
|
|
// TestIntentPatternsExist tests that all intents have patterns.
|
|
func (s *ExpanderSuite) TestIntentPatternsExist() {
|
|
s.NotEmpty(s.expander.intentPatterns[IntentQuestion])
|
|
s.NotEmpty(s.expander.intentPatterns[IntentError])
|
|
s.NotEmpty(s.expander.intentPatterns[IntentImplementation])
|
|
s.NotEmpty(s.expander.intentPatterns[IntentArchitecture])
|
|
}
|