Files
claude-mnemonic/pkg/models/pattern_test.go
T
lukaszraczylo 4f4b4ac70f feat(chunking): add AST-aware code chunking for Go, Python, TypeScript
- [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
2026-01-07 13:19:58 +00:00

358 lines
9.9 KiB
Go

package models
import (
"database/sql"
"encoding/json"
"testing"
"time"
)
func TestNewPattern(t *testing.T) {
pattern := NewPattern(
"Test Pattern",
PatternTypeBug,
"A test pattern description",
[]string{"nil", "error", "handling"},
"test-project",
123,
)
if pattern.Name != "Test Pattern" {
t.Errorf("Expected name 'Test Pattern', got '%s'", pattern.Name)
}
if pattern.Type != PatternTypeBug {
t.Errorf("Expected type PatternTypeBug, got '%s'", pattern.Type)
}
if !pattern.Description.Valid || pattern.Description.String != "A test pattern description" {
t.Errorf("Description not set correctly")
}
if len(pattern.Signature) != 3 {
t.Errorf("Expected 3 signature elements, got %d", len(pattern.Signature))
}
if pattern.Frequency != 1 {
t.Errorf("Expected frequency 1, got %d", pattern.Frequency)
}
if len(pattern.Projects) != 1 || pattern.Projects[0] != "test-project" {
t.Errorf("Projects not set correctly")
}
if len(pattern.ObservationIDs) != 1 || pattern.ObservationIDs[0] != 123 {
t.Errorf("ObservationIDs not set correctly")
}
if pattern.Status != PatternStatusActive {
t.Errorf("Expected status Active, got '%s'", pattern.Status)
}
if pattern.Confidence != 0.5 {
t.Errorf("Expected initial confidence 0.5, got %f", pattern.Confidence)
}
}
func TestPattern_AddOccurrence(t *testing.T) {
pattern := NewPattern("Test", PatternTypeBug, "desc", []string{"test"}, "project1", 1)
// Add same project occurrence
pattern.AddOccurrence("project1", 2)
if pattern.Frequency != 2 {
t.Errorf("Expected frequency 2, got %d", pattern.Frequency)
}
if len(pattern.Projects) != 1 {
t.Errorf("Expected 1 project (no duplicates), got %d", len(pattern.Projects))
}
// Add different project occurrence
pattern.AddOccurrence("project2", 3)
if pattern.Frequency != 3 {
t.Errorf("Expected frequency 3, got %d", pattern.Frequency)
}
if len(pattern.Projects) != 2 {
t.Errorf("Expected 2 projects, got %d", len(pattern.Projects))
}
// Add duplicate observation ID - should not duplicate
pattern.AddOccurrence("project2", 3)
if len(pattern.ObservationIDs) != 3 {
t.Errorf("Expected 3 observation IDs (no duplicate), got %d", len(pattern.ObservationIDs))
}
// Check confidence increased
if pattern.Confidence <= 0.5 {
t.Errorf("Expected confidence to increase above 0.5, got %f", pattern.Confidence)
}
}
func TestPattern_ConfidenceCalculation(t *testing.T) {
tests := []struct {
name string
frequency int
projectCount int
minConfidence float64
maxConfidence float64
}{
{"low_frequency", 2, 1, 0.3, 0.5},
{"high_frequency", 10, 1, 0.6, 0.8},
{"multi_project", 3, 3, 0.4, 0.7},
{"high_freq_multi_proj", 10, 5, 0.7, 1.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pattern := NewPattern("Test", PatternTypeBug, "", []string{}, "proj1", 1)
// Simulate occurrences
for i := 1; i < tt.frequency; i++ {
projIdx := i % tt.projectCount
if projIdx == 0 {
projIdx = 1
}
pattern.AddOccurrence("proj"+string(rune('0'+projIdx)), int64(i+1))
}
if pattern.Confidence < tt.minConfidence || pattern.Confidence > tt.maxConfidence {
t.Errorf("Expected confidence between %f and %f, got %f",
tt.minConfidence, tt.maxConfidence, pattern.Confidence)
}
})
}
}
func TestPatternType_Detection(t *testing.T) {
tests := []struct {
title string
narrative string
expected PatternType
concepts []string
}{
{concepts: []string{"anti-pattern"}, title: "", narrative: "", expected: PatternTypeAntiPattern},
{concepts: []string{"best-practice"}, title: "", narrative: "", expected: PatternTypeBestPractice},
{concepts: []string{"architecture"}, title: "", narrative: "", expected: PatternTypeArchitecture},
{concepts: []string{"refactor"}, title: "", narrative: "", expected: PatternTypeRefactor},
{concepts: []string{}, title: "nil pointer bug", narrative: "", expected: PatternTypeBug},
{concepts: []string{}, title: "Deadlock in concurrent code", narrative: "", expected: PatternTypeBug},
{concepts: []string{}, title: "Extract interface", narrative: "", expected: PatternTypeRefactor},
}
for _, tt := range tests {
t.Run(tt.title+"_"+tt.expected.String(), func(t *testing.T) {
result := DetectPatternType(tt.concepts, tt.title, tt.narrative)
if result != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, result)
}
})
}
}
func (pt PatternType) String() string {
return string(pt)
}
func TestExtractSignature(t *testing.T) {
concepts := []string{"error-handling", "security"}
title := "Nil Pointer Validation Pattern"
narrative := "Always validate before dereferencing"
signature := ExtractSignature(concepts, title, narrative)
// Should contain concepts
found := false
for _, s := range signature {
if s == "error-handling" {
found = true
break
}
}
if !found {
t.Errorf("Expected signature to contain concepts, got %v", signature)
}
// Should contain significant words from title
found = false
for _, s := range signature {
if s == "validation" || s == "pattern" || s == "pointer" {
found = true
break
}
}
if !found {
t.Errorf("Expected signature to contain title keywords, got %v", signature)
}
}
func TestCalculateMatchScore(t *testing.T) {
tests := []struct {
name string
sig1 []string
sig2 []string
minScore float64
maxScore float64
}{
{"identical", []string{"a", "b", "c"}, []string{"a", "b", "c"}, 1.0, 1.0},
{"partial", []string{"a", "b", "c"}, []string{"a", "b", "d"}, 0.4, 0.6},
{"no_match", []string{"a", "b", "c"}, []string{"x", "y", "z"}, 0.0, 0.0},
{"empty", []string{}, []string{"a", "b"}, 0.0, 0.0},
{"subset", []string{"a", "b"}, []string{"a", "b", "c", "d"}, 0.4, 0.6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
score := CalculateMatchScore(tt.sig1, tt.sig2)
if score < tt.minScore || score > tt.maxScore {
t.Errorf("Expected score between %f and %f, got %f",
tt.minScore, tt.maxScore, score)
}
})
}
}
func TestPattern_MarshalJSON(t *testing.T) {
pattern := &Pattern{
ID: 1,
Name: "Test Pattern",
Type: PatternTypeBug,
Description: sql.NullString{String: "A description", Valid: true},
Signature: []string{"a", "b"},
Recommendation: sql.NullString{String: "Do this", Valid: true},
Frequency: 5,
Projects: []string{"proj1", "proj2"},
ObservationIDs: []int64{1, 2, 3},
Status: PatternStatusActive,
MergedIntoID: sql.NullInt64{Int64: 0, Valid: false},
Confidence: 0.8,
LastSeenAt: time.Now().Format(time.RFC3339),
LastSeenEpoch: time.Now().UnixMilli(),
CreatedAt: time.Now().Format(time.RFC3339),
CreatedAtEpoch: time.Now().UnixMilli(),
}
data, err := json.Marshal(pattern)
if err != nil {
t.Fatalf("Failed to marshal pattern: %v", err)
}
var result PatternJSON
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal pattern: %v", err)
}
if result.Name != pattern.Name {
t.Errorf("Expected name %s, got %s", pattern.Name, result.Name)
}
if result.Description != pattern.Description.String {
t.Errorf("Expected description %s, got %s", pattern.Description.String, result.Description)
}
if result.Frequency != pattern.Frequency {
t.Errorf("Expected frequency %d, got %d", pattern.Frequency, result.Frequency)
}
if result.MergedIntoID != 0 {
t.Errorf("Expected merged_into_id 0 for invalid NullInt64, got %d", result.MergedIntoID)
}
}
func TestJSONInt64Array_Scan(t *testing.T) {
tests := []struct {
name string
input interface{}
expected JSONInt64Array
wantErr bool
}{
{"string_array", "[1, 2, 3]", JSONInt64Array{1, 2, 3}, false},
{"bytes_array", []byte("[4, 5, 6]"), JSONInt64Array{4, 5, 6}, false},
{"nil", nil, nil, false},
{"empty_string", "", nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var arr JSONInt64Array
err := arr.Scan(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(arr) != len(tt.expected) {
t.Errorf("Expected length %d, got %d", len(tt.expected), len(arr))
}
})
}
}
func TestJSONInt64Array_Value(t *testing.T) {
arr := JSONInt64Array{1, 2, 3}
val, err := arr.Value()
if err != nil {
t.Fatalf("Value() error = %v", err)
}
bytes, ok := val.([]byte)
if !ok {
t.Fatalf("Expected []byte, got %T", val)
}
var result []int64
if err := json.Unmarshal(bytes, &result); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if len(result) != 3 || result[0] != 1 || result[1] != 2 || result[2] != 3 {
t.Errorf("Expected [1, 2, 3], got %v", result)
}
}
func TestPatternSignatureKeywords(t *testing.T) {
// Verify keywords exist for each type
types := []PatternType{
PatternTypeBug,
PatternTypeRefactor,
PatternTypeArchitecture,
PatternTypeAntiPattern,
PatternTypeBestPractice,
}
for _, pt := range types {
keywords := PatternSignatureKeywords[pt]
if len(keywords) == 0 {
t.Errorf("No keywords defined for pattern type %s", pt)
}
}
}
func TestUniqueStrings(t *testing.T) {
tests := []struct {
input []string
expected int
}{
{[]string{"a", "b", "c"}, 3},
{[]string{"a", "a", "b"}, 2},
{[]string{"a", "a", "a"}, 1},
{[]string{}, 0},
}
for _, tt := range tests {
result := uniqueStrings(tt.input)
if len(result) != tt.expected {
t.Errorf("uniqueStrings(%v) = %v (len=%d), expected len=%d",
tt.input, result, len(result), tt.expected)
}
}
}
func TestContainsIgnoreCase(t *testing.T) {
tests := []struct {
text string
substr string
expected bool
}{
{"Hello World", "hello", true},
{"Hello World", "WORLD", true},
{"Hello World", "xyz", false},
{"", "a", false},
{"a", "", true},
}
for _, tt := range tests {
result := containsIgnoreCase(tt.text, tt.substr)
if result != tt.expected {
t.Errorf("containsIgnoreCase(%q, %q) = %v, expected %v",
tt.text, tt.substr, result, tt.expected)
}
}
}