Files
claude-mnemonic/pkg/models/conflict_test.go
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

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)
}