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