mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-06 23:13:50 +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
422 lines
13 KiB
Go
422 lines
13 KiB
Go
// Package models contains domain models for claude-mnemonic.
|
|
package models
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// ConflictSuite is a test suite for conflict detection operations.
|
|
type ConflictSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func TestConflictSuite(t *testing.T) {
|
|
suite.Run(t, new(ConflictSuite))
|
|
}
|
|
|
|
// TestConflictTypeConstants tests conflict type constants.
|
|
func (s *ConflictSuite) TestConflictTypeConstants() {
|
|
s.Equal(ConflictType("superseded"), ConflictSuperseded)
|
|
s.Equal(ConflictType("contradicts"), ConflictContradicts)
|
|
s.Equal(ConflictType("outdated_pattern"), ConflictOutdatedPattern)
|
|
}
|
|
|
|
// TestResolutionConstants tests resolution constants.
|
|
func (s *ConflictSuite) TestResolutionConstants() {
|
|
s.Equal(ConflictResolution("prefer_newer"), ResolutionPreferNewer)
|
|
s.Equal(ConflictResolution("prefer_older"), ResolutionPreferOlder)
|
|
s.Equal(ConflictResolution("manual"), ResolutionManual)
|
|
}
|
|
|
|
// TestNewObservationConflict tests conflict creation.
|
|
func (s *ConflictSuite) TestNewObservationConflict() {
|
|
conflict := NewObservationConflict(2, 1, ConflictSuperseded, ResolutionPreferNewer, "Test reason")
|
|
|
|
s.Equal(int64(2), conflict.NewerObsID)
|
|
s.Equal(int64(1), conflict.OlderObsID)
|
|
s.Equal(ConflictSuperseded, conflict.ConflictType)
|
|
s.Equal(ResolutionPreferNewer, conflict.Resolution)
|
|
s.Equal("Test reason", conflict.Reason)
|
|
s.False(conflict.Resolved)
|
|
s.NotEmpty(conflict.DetectedAt)
|
|
s.Greater(conflict.DetectedAtEpoch, int64(0))
|
|
}
|
|
|
|
// TestDetectExplicitCorrection_TableDriven tests explicit correction detection.
|
|
func (s *ConflictSuite) TestDetectExplicitCorrection_TableDriven() {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
expectPattern string
|
|
expectMatch bool
|
|
}{
|
|
{
|
|
name: "actually that was wrong",
|
|
text: "Actually, that was wrong - we should use a different approach",
|
|
expectMatch: true,
|
|
expectPattern: "actually, that was wrong",
|
|
},
|
|
{
|
|
name: "correction prefix",
|
|
text: "Correction: the previous implementation had a bug",
|
|
expectMatch: true,
|
|
expectPattern: "correction:",
|
|
},
|
|
{
|
|
name: "ignore previous",
|
|
text: "Please ignore the previous recommendation",
|
|
expectMatch: true,
|
|
expectPattern: "ignore",
|
|
},
|
|
{
|
|
name: "disregard earlier",
|
|
text: "Disregard the earlier suggestion, it was flawed",
|
|
expectMatch: true,
|
|
expectPattern: "disregard",
|
|
},
|
|
{
|
|
name: "was wrong",
|
|
text: "The original approach was wrong",
|
|
expectMatch: true,
|
|
expectPattern: "was wrong",
|
|
},
|
|
{
|
|
name: "no longer valid",
|
|
text: "This method is no longer valid after the refactor",
|
|
expectMatch: true,
|
|
expectPattern: "no longer valid",
|
|
},
|
|
{
|
|
name: "deprecated approach",
|
|
text: "This is a deprecated approach that should not be used",
|
|
expectMatch: true,
|
|
expectPattern: "deprecated approach",
|
|
},
|
|
{
|
|
name: "better approach is",
|
|
text: "A better approach is to use the new API",
|
|
expectMatch: true,
|
|
expectPattern: "better approach is",
|
|
},
|
|
{
|
|
name: "normal text - no correction",
|
|
text: "This is a normal observation about the code",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
name: "empty text",
|
|
text: "",
|
|
expectMatch: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
found, reason := DetectExplicitCorrection(tt.text)
|
|
s.Equal(tt.expectMatch, found)
|
|
if tt.expectMatch {
|
|
s.Contains(reason, "Explicit correction detected")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectOpposingFileChanges_TableDriven tests opposing file change detection.
|
|
func (s *ConflictSuite) TestDetectOpposingFileChanges_TableDriven() {
|
|
tests := []struct {
|
|
newerObs *Observation
|
|
olderObs *Observation
|
|
name string
|
|
expectConflict bool
|
|
}{
|
|
{
|
|
name: "add then remove - conflict",
|
|
newerObs: &Observation{
|
|
Title: sql.NullString{String: "Remove authentication middleware", Valid: true},
|
|
Narrative: sql.NullString{String: "Removed the auth middleware from handlers", Valid: true},
|
|
FilesModified: []string{"middleware.go", "handler.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Title: sql.NullString{String: "Add authentication middleware", Valid: true},
|
|
Narrative: sql.NullString{String: "Added auth middleware to secure endpoints", Valid: true},
|
|
FilesModified: []string{"middleware.go", "handler.go"},
|
|
},
|
|
expectConflict: true,
|
|
},
|
|
{
|
|
name: "enable then disable - conflict",
|
|
newerObs: &Observation{
|
|
Title: sql.NullString{String: "Disable caching feature", Valid: true},
|
|
Narrative: sql.NullString{String: "Disabled caching due to issues", Valid: true},
|
|
FilesModified: []string{"cache.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Title: sql.NullString{String: "Enable caching feature", Valid: true},
|
|
Narrative: sql.NullString{String: "Enabled caching for performance", Valid: true},
|
|
FilesModified: []string{"cache.go"},
|
|
},
|
|
expectConflict: true,
|
|
},
|
|
{
|
|
name: "different files - no conflict",
|
|
newerObs: &Observation{
|
|
Title: sql.NullString{String: "Remove old code", Valid: true},
|
|
Narrative: sql.NullString{String: "Removed deprecated functions", Valid: true},
|
|
FilesModified: []string{"old.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Title: sql.NullString{String: "Add new feature", Valid: true},
|
|
Narrative: sql.NullString{String: "Added new functions", Valid: true},
|
|
FilesModified: []string{"new.go"},
|
|
},
|
|
expectConflict: false,
|
|
},
|
|
{
|
|
name: "same files but no opposing keywords - no conflict",
|
|
newerObs: &Observation{
|
|
Title: sql.NullString{String: "Update handler logic", Valid: true},
|
|
Narrative: sql.NullString{String: "Updated the handler implementation", Valid: true},
|
|
FilesModified: []string{"handler.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Title: sql.NullString{String: "Fix handler bug", Valid: true},
|
|
Narrative: sql.NullString{String: "Fixed a bug in handler", Valid: true},
|
|
FilesModified: []string{"handler.go"},
|
|
},
|
|
expectConflict: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
found, _ := DetectOpposingFileChanges(tt.newerObs, tt.olderObs)
|
|
s.Equal(tt.expectConflict, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectConceptTagMismatch_TableDriven tests concept tag mismatch detection.
|
|
func (s *ConflictSuite) TestDetectConceptTagMismatch_TableDriven() {
|
|
tests := []struct {
|
|
newerObs *Observation
|
|
olderObs *Observation
|
|
name string
|
|
expectConflict bool
|
|
}{
|
|
{
|
|
name: "same concepts and files - conflict",
|
|
newerObs: &Observation{
|
|
Concepts: []string{"security", "authentication"},
|
|
FilesModified: []string{"auth.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Concepts: []string{"security", "authentication"},
|
|
FilesModified: []string{"auth.go"},
|
|
},
|
|
expectConflict: true,
|
|
},
|
|
{
|
|
name: "overlapping concepts different files - no conflict",
|
|
newerObs: &Observation{
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"new_auth.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"old_auth.go"},
|
|
},
|
|
expectConflict: false,
|
|
},
|
|
{
|
|
name: "different concepts same files - no conflict",
|
|
newerObs: &Observation{
|
|
Concepts: []string{"performance"},
|
|
FilesModified: []string{"handler.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Concepts: []string{"testing"},
|
|
FilesModified: []string{"handler.go"},
|
|
},
|
|
expectConflict: false,
|
|
},
|
|
{
|
|
name: "no overlapping concepts or files - no conflict",
|
|
newerObs: &Observation{
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"auth.go"},
|
|
},
|
|
olderObs: &Observation{
|
|
Concepts: []string{"testing"},
|
|
FilesModified: []string{"test.go"},
|
|
},
|
|
expectConflict: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
found, _ := DetectConceptTagMismatch(tt.newerObs, tt.olderObs)
|
|
s.Equal(tt.expectConflict, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectConflict tests comprehensive conflict detection.
|
|
func (s *ConflictSuite) TestDetectConflict() {
|
|
// Test explicit correction takes precedence
|
|
newer := &Observation{
|
|
ID: 2,
|
|
Project: "test",
|
|
Narrative: sql.NullString{String: "Actually, that was wrong. We should use a different approach.", Valid: true},
|
|
}
|
|
older := &Observation{
|
|
ID: 1,
|
|
Project: "test",
|
|
}
|
|
|
|
result := DetectConflict(newer, older)
|
|
s.True(result.HasConflict)
|
|
s.Equal(ConflictContradicts, result.Type)
|
|
s.Equal(ResolutionPreferNewer, result.Resolution)
|
|
s.Contains(result.Reason, "Explicit correction")
|
|
}
|
|
|
|
// TestDetectConflictsWithExisting tests conflict detection against multiple observations.
|
|
func (s *ConflictSuite) TestDetectConflictsWithExisting() {
|
|
newer := &Observation{
|
|
ID: 3,
|
|
Project: "test",
|
|
Narrative: sql.NullString{String: "Actually, that was wrong", Valid: true},
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"auth.go"},
|
|
}
|
|
|
|
existing := []*Observation{
|
|
{
|
|
ID: 1,
|
|
Project: "test",
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"auth.go"},
|
|
},
|
|
{
|
|
ID: 2,
|
|
Project: "test",
|
|
Concepts: []string{"testing"},
|
|
FilesModified: []string{"test.go"},
|
|
},
|
|
{
|
|
ID: 3, // Same as newer - should be skipped
|
|
Project: "test",
|
|
},
|
|
}
|
|
|
|
results := DetectConflictsWithExisting(newer, existing)
|
|
|
|
// Should find conflicts with obs 1 (concepts + files overlap + correction language)
|
|
// but not with obs 2 (different concepts/files) or obs 3 (same ID)
|
|
s.GreaterOrEqual(len(results), 1)
|
|
|
|
// At least one result should reference obs 1
|
|
foundObs1 := false
|
|
for _, r := range results {
|
|
for _, id := range r.OlderObsIDs {
|
|
if id == 1 {
|
|
foundObs1 = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
s.True(foundObs1)
|
|
}
|
|
|
|
// TestDetectConflictsWithExisting_DifferentProjects tests that different projects don't conflict.
|
|
func (s *ConflictSuite) TestDetectConflictsWithExisting_DifferentProjects() {
|
|
newer := &Observation{
|
|
ID: 2,
|
|
Project: "project-a",
|
|
Scope: ScopeProject,
|
|
Narrative: sql.NullString{String: "Actually, that was wrong", Valid: true},
|
|
}
|
|
|
|
existing := []*Observation{
|
|
{
|
|
ID: 1,
|
|
Project: "project-b",
|
|
Scope: ScopeProject,
|
|
},
|
|
}
|
|
|
|
results := DetectConflictsWithExisting(newer, existing)
|
|
s.Empty(results) // Different projects should not conflict
|
|
}
|
|
|
|
// TestDetectConflictsWithExisting_GlobalScope tests that global observations can conflict across projects.
|
|
func (s *ConflictSuite) TestDetectConflictsWithExisting_GlobalScope() {
|
|
newer := &Observation{
|
|
ID: 2,
|
|
Project: "project-a",
|
|
Scope: ScopeGlobal,
|
|
Narrative: sql.NullString{String: "Actually, that was wrong", Valid: true},
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"auth.go"},
|
|
}
|
|
|
|
existing := []*Observation{
|
|
{
|
|
ID: 1,
|
|
Project: "project-b",
|
|
Scope: ScopeGlobal, // Global scope allows cross-project conflict detection
|
|
Concepts: []string{"security"},
|
|
FilesModified: []string{"auth.go"},
|
|
},
|
|
}
|
|
|
|
results := DetectConflictsWithExisting(newer, existing)
|
|
s.GreaterOrEqual(len(results), 1) // Global scope allows conflict detection
|
|
}
|
|
|
|
// TestCorrectionPatterns_Compiled ensures all patterns compile correctly.
|
|
func TestCorrectionPatterns_Compiled(t *testing.T) {
|
|
// This test verifies that all correction patterns are valid regexps
|
|
// If any pattern fails to compile, the package won't load
|
|
assert.NotEmpty(t, CorrectionPatterns)
|
|
for i, pattern := range CorrectionPatterns {
|
|
assert.NotNil(t, pattern, "Pattern %d should not be nil", i)
|
|
}
|
|
}
|
|
|
|
// TestOpposingChangePatterns tests the opposing change pattern map.
|
|
func TestOpposingChangePatterns(t *testing.T) {
|
|
assert.NotEmpty(t, OpposingChangePatterns)
|
|
assert.Equal(t, "remove", OpposingChangePatterns["add"])
|
|
assert.Equal(t, "removed", OpposingChangePatterns["added"])
|
|
assert.Equal(t, "delete", OpposingChangePatterns["create"])
|
|
assert.Equal(t, "disable", OpposingChangePatterns["enable"])
|
|
}
|
|
|
|
// TestObservationConflict_Fields tests field access.
|
|
func TestObservationConflict_Fields(t *testing.T) {
|
|
conflict := &ObservationConflict{
|
|
ID: 1,
|
|
NewerObsID: 10,
|
|
OlderObsID: 5,
|
|
ConflictType: ConflictSuperseded,
|
|
Resolution: ResolutionPreferNewer,
|
|
Reason: "Test reason", //nolint:govet
|
|
DetectedAt: "2024-01-01T00:00:00Z", //nolint:govet
|
|
DetectedAtEpoch: 1704067200000, //nolint:govet
|
|
Resolved: false,
|
|
}
|
|
|
|
assert.Equal(t, int64(1), conflict.ID)
|
|
assert.Equal(t, int64(10), conflict.NewerObsID)
|
|
assert.Equal(t, int64(5), conflict.OlderObsID)
|
|
assert.Equal(t, ConflictSuperseded, conflict.ConflictType)
|
|
assert.Equal(t, ResolutionPreferNewer, conflict.Resolution)
|
|
assert.False(t, conflict.Resolved)
|
|
}
|