mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +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
358 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|