2026-08-09 16:00:01 +00:00
package semanticreconcile
import (
"reflect"
2026-08-28 18:25:00 +00:00
"strings"
2026-08-09 16:00:01 +00:00
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
2026-08-28 18:25:00 +00:00
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
2026-08-09 16:00:01 +00:00
)
func TestAssessProducesAStableOriginalPositionPlan ( t * testing . T ) {
preparation := proposalPreparation ( t )
response := ProposalResponse { DuplicateGroups : [ ] DuplicateGroup {
2026-08-29 03:16:24 +00:00
{ CandidateNumbers : [ ] int { 5 , 4 } , CanonicalCandidateNumber : 5 } ,
{ CandidateNumbers : [ ] int { 2 , 1 } , CanonicalCandidateNumber : 2 } ,
2026-08-09 16:00:01 +00:00
} }
assessment := preparation . Assess ( response )
want := [ ] planGroupSnapshot {
{ members : [ ] int { 0 , 2 } , canonical : 2 } ,
{ members : [ ] int { 5 , 6 } , canonical : 6 } ,
}
if got := snapshotPlan ( assessment . Plan ( ) ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "plan = %#v, want %#v" , got , want )
}
if assessment . DiscardedGroupCount ( ) != 0 || assessment . RetryRequired ( ) || len ( assessment . Issues ( ) ) != 0 {
t . Fatalf ( "assessment diagnostics = discarded %d, retry %t, issues %#v" , assessment . DiscardedGroupCount ( ) , assessment . RetryRequired ( ) , assessment . Issues ( ) )
}
reordered := preparation . Assess ( ProposalResponse { DuplicateGroups : [ ] DuplicateGroup {
2026-08-29 03:16:24 +00:00
{ CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 2 } ,
{ CandidateNumbers : [ ] int { 4 , 5 } , CanonicalCandidateNumber : 5 } ,
2026-08-09 16:00:01 +00:00
} } )
if got := snapshotPlan ( reordered . Plan ( ) ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "reordered plan = %#v, want %#v" , got , want )
}
empty := preparation . Assess ( ProposalResponse { } )
if len ( empty . Plan ( ) . Groups ( ) ) != 0 || len ( empty . Issues ( ) ) != 0 || empty . DiscardedGroupCount ( ) != 0 || empty . RetryRequired ( ) {
t . Fatalf ( "empty assessment = plan %#v, issues %#v, discarded %d, retry %t" , empty . Plan ( ) . Groups ( ) , empty . Issues ( ) , empty . DiscardedGroupCount ( ) , empty . RetryRequired ( ) )
}
}
2026-08-09 16:57:44 +00:00
func TestIssueDetailsPreservesIssueOrder ( t * testing . T ) {
issues := [ ] Issue {
{ GroupIndex : 3 , Category : IssueCanonicalUnknown } ,
{ GroupIndex : 1 , Category : IssueMemberUnknown } ,
}
want := [ ] string { "group 3: canonical_unknown" , "group 1: member_unknown" }
if got := IssueDetails ( issues ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "IssueDetails() = %#v, want %#v" , got , want )
}
}
2026-08-28 18:25:00 +00:00
func TestCorrectionGuidanceCoversEveryIssueCategoryWithoutExposingInternalLabels ( t * testing . T ) {
2026-08-29 03:16:24 +00:00
numberRange := CandidateNumberRange { First : 1 , Last : 8 }
2026-08-28 18:25:00 +00:00
if len ( issueCorrectionProse ) != len ( allIssueCategories ) {
t . Fatalf ( "correction prose entries = %d, categories = %d" , len ( issueCorrectionProse ) , len ( allIssueCategories ) )
}
issues := make ( [ ] Issue , len ( allIssueCategories ) )
seen := make ( map [ IssueCategory ] struct { } , len ( allIssueCategories ) )
for index , category := range allIssueCategories {
if _ , duplicate := seen [ category ] ; duplicate {
t . Fatalf ( "duplicate authoritative issue category %q" , category )
}
seen [ category ] = struct { } { }
if strings . TrimSpace ( issueCorrectionProse [ category ] ) == "" {
t . Fatalf ( "issue category %q has no model-facing prose" , category )
}
issues [ index ] = Issue { GroupIndex : index , Category : category }
}
2026-08-29 03:16:24 +00:00
details , err := CorrectionDetails ( issues , numberRange )
2026-08-28 18:25:00 +00:00
if err != nil {
t . Fatalf ( "CorrectionDetails() error = %v" , err )
}
2026-08-29 03:16:24 +00:00
guidance , err := CorrectionGuidance ( issues , numberRange )
2026-08-28 18:25:00 +00:00
if err != nil {
t . Fatalf ( "CorrectionGuidance() error = %v" , err )
}
2026-08-29 03:16:24 +00:00
if len ( details ) != len ( allIssueCategories ) + 1 || ! strings . Contains ( guidance , "Duplicate group 1" ) || ! strings . Contains ( guidance , "candidate-number range 1 through 8" ) || ! strings . Contains ( guidance , "start_unit_id" ) || ! strings . Contains ( guidance , "complete corrected JSON response" ) || len ( guidance ) > contracts . MaxNormalizeRetryCorrectionGuidanceBytes {
2026-08-28 18:25:00 +00:00
t . Fatalf ( "correction details = %#v guidance = %q" , details , guidance )
}
for _ , category := range allIssueCategories {
if strings . Contains ( guidance , string ( category ) ) {
t . Fatalf ( "model guidance exposed internal category %q: %q" , category , guidance )
}
}
}
func TestCorrectionDetailsDeduplicatesAndBoundsAffectedGroupLists ( t * testing . T ) {
2026-08-29 03:16:24 +00:00
numberRange := CandidateNumberRange { First : 1 , Last : 256 }
2026-08-28 18:25:00 +00:00
issues := make ( [ ] Issue , 0 , 257 )
for group := 0 ; group < 256 ; group ++ {
issues = append ( issues , Issue { GroupIndex : group , Category : IssueMemberUnknown } )
}
issues = append ( issues , Issue { GroupIndex : 0 , Category : IssueMemberUnknown } )
2026-08-29 03:16:24 +00:00
details , err := CorrectionDetails ( issues , numberRange )
2026-08-28 18:25:00 +00:00
if err != nil {
t . Fatalf ( "CorrectionDetails() error = %v" , err )
}
2026-08-29 03:16:24 +00:00
if len ( details ) != 2 || ! strings . Contains ( details [ 0 ] , "additional affected group" ) || strings . Count ( details [ 0 ] , "Duplicate groups" ) != 1 || ! strings . Contains ( details [ 1 ] , "source positions" ) {
t . Fatalf ( "CorrectionDetails() = %#v, want one bounded grouped detail and one namespace reminder" , details )
2026-08-28 18:25:00 +00:00
}
2026-08-29 03:16:24 +00:00
guidance , err := CorrectionGuidance ( issues , numberRange )
2026-08-28 18:25:00 +00:00
if err != nil || len ( guidance ) > contracts . MaxNormalizeRetryCorrectionGuidanceBytes {
t . Fatalf ( "CorrectionGuidance() = %q, %v" , guidance , err )
}
}
func TestCorrectionDetailsRejectsUnknownOrInvalidIssues ( t * testing . T ) {
for _ , issues := range [ ] [ ] Issue {
{ { GroupIndex : 0 , Category : "future_unmapped_category" } } ,
{ { GroupIndex : - 1 , Category : IssueMemberUnknown } } ,
} {
2026-08-29 03:16:24 +00:00
if details , err := CorrectionDetails ( issues , CandidateNumberRange { First : 1 , Last : 2 } ) ; err == nil || details != nil {
2026-08-28 18:25:00 +00:00
t . Fatalf ( "CorrectionDetails(%#v) = %#v, %v; want fail-closed error" , issues , details , err )
}
}
2026-08-29 03:16:24 +00:00
for _ , numberRange := range [ ] CandidateNumberRange { { } , { First : 0 , Last : 2 } , { First : 1 , Last : 1 } } {
if details , err := CorrectionDetails ( [ ] Issue { { GroupIndex : 0 , Category : IssueMemberUnknown } } , numberRange ) ; err == nil || details != nil {
t . Fatalf ( "CorrectionDetails(range %#v) = %#v, %v; want fail-closed error" , numberRange , details , err )
}
}
2026-08-28 18:25:00 +00:00
}
2026-08-09 16:00:01 +00:00
func TestAssessRejectsEveryUnsafeLocalGroupShape ( t * testing . T ) {
preparation := proposalPreparation ( t )
tests := [ ] struct {
name string
group DuplicateGroup
category IssueCategory
} {
2026-08-29 03:16:24 +00:00
{ name : "zero member" , group : DuplicateGroup { CandidateNumbers : [ ] int { 0 , 2 } , CanonicalCandidateNumber : 2 } , category : IssueMemberNonPositive } ,
{ name : "negative member" , group : DuplicateGroup { CandidateNumbers : [ ] int { - 1 , 2 } , CanonicalCandidateNumber : 2 } , category : IssueMemberNonPositive } ,
{ name : "unknown member" , group : DuplicateGroup { CandidateNumbers : [ ] int { 99 , 2 } , CanonicalCandidateNumber : 2 } , category : IssueMemberUnknown } ,
{ name : "repeated member" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 , 1 } , CanonicalCandidateNumber : 1 } , category : IssueRepeatedMember } ,
{ name : "too small" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 } , CanonicalCandidateNumber : 1 } , category : IssueFewerThanTwoMembers } ,
{ name : "zero canonical" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 0 } , category : IssueCanonicalNonPositive } ,
{ name : "negative canonical" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : - 1 } , category : IssueCanonicalNonPositive } ,
{ name : "unknown canonical" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 99 } , category : IssueCanonicalUnknown } ,
{ name : "canonical not member" , group : DuplicateGroup { CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 3 } , category : IssueCanonicalNotMember } ,
2026-08-09 16:00:01 +00:00
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
assessment := preparation . Assess ( ProposalResponse { DuplicateGroups : [ ] DuplicateGroup { test . group } } )
if len ( assessment . Plan ( ) . Groups ( ) ) != 0 || assessment . DiscardedGroupCount ( ) != 1 || ! assessment . RetryRequired ( ) {
t . Fatalf ( "assessment = plan %#v, discarded %d, retry %t" , assessment . Plan ( ) . Groups ( ) , assessment . DiscardedGroupCount ( ) , assessment . RetryRequired ( ) )
}
if ! hasIssue ( assessment . Issues ( ) , 0 , test . category ) {
t . Fatalf ( "issues = %#v, want category %q" , assessment . Issues ( ) , test . category )
}
} )
}
}
func TestAssessDiscardsEveryOverlappingGroupAndRetainsIndependentGroups ( t * testing . T ) {
preparation := proposalPreparation ( t )
assessment := preparation . Assess ( ProposalResponse { DuplicateGroups : [ ] DuplicateGroup {
2026-08-29 03:16:24 +00:00
{ CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 1 } ,
{ CandidateNumbers : [ ] int { 2 , 3 } , CanonicalCandidateNumber : 2 } ,
{ CandidateNumbers : [ ] int { 4 , 5 } , CanonicalCandidateNumber : 5 } ,
2026-08-09 16:00:01 +00:00
} } )
wantPlan := [ ] planGroupSnapshot { { members : [ ] int { 5 , 6 } , canonical : 6 } }
if got := snapshotPlan ( assessment . Plan ( ) ) ; ! reflect . DeepEqual ( got , wantPlan ) {
t . Fatalf ( "plan = %#v, want %#v" , got , wantPlan )
}
if assessment . DiscardedGroupCount ( ) != 2 || ! assessment . RetryRequired ( ) {
t . Fatalf ( "discarded = %d, retry = %t" , assessment . DiscardedGroupCount ( ) , assessment . RetryRequired ( ) )
}
issues := assessment . Issues ( )
if len ( issues ) != 2 || ! hasIssue ( issues , 0 , IssueOverlappingMember ) || ! hasIssue ( issues , 1 , IssueOverlappingMember ) {
t . Fatalf ( "issues = %#v, want both conflicting group indexes" , issues )
}
invalidAndSafe := preparation . Assess ( ProposalResponse { DuplicateGroups : [ ] DuplicateGroup {
2026-08-29 03:16:24 +00:00
{ CandidateNumbers : [ ] int { 1 , 99 } , CanonicalCandidateNumber : 1 } ,
{ CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 2 } ,
2026-08-09 16:00:01 +00:00
} } )
wantPlan = [ ] planGroupSnapshot { { members : [ ] int { 0 , 2 } , canonical : 2 } }
if got := snapshotPlan ( invalidAndSafe . Plan ( ) ) ; ! reflect . DeepEqual ( got , wantPlan ) {
t . Fatalf ( "plan with independent invalid group = %#v, want %#v" , got , wantPlan )
}
if invalidAndSafe . DiscardedGroupCount ( ) != 1 || ! invalidAndSafe . RetryRequired ( ) || ! hasIssue ( invalidAndSafe . Issues ( ) , 0 , IssueMemberUnknown ) {
t . Fatalf ( "invalid-and-safe assessment = discarded %d, retry %t, issues %#v" , invalidAndSafe . DiscardedGroupCount ( ) , invalidAndSafe . RetryRequired ( ) , invalidAndSafe . Issues ( ) )
}
}
func TestAssessmentAccessorsAndInputsDoNotShareRetainedState ( t * testing . T ) {
preparation := proposalPreparation ( t )
response := ProposalResponse { DuplicateGroups : [ ] DuplicateGroup {
2026-08-29 03:16:24 +00:00
{ CandidateNumbers : [ ] int { 1 , 2 } , CanonicalCandidateNumber : 2 } ,
2026-08-09 16:00:01 +00:00
} }
assessment := preparation . Assess ( response )
want := snapshotPlan ( assessment . Plan ( ) )
2026-08-29 03:16:24 +00:00
response . DuplicateGroups [ 0 ] . CandidateNumbers [ 0 ] = 99
response . DuplicateGroups [ 0 ] . CanonicalCandidateNumber = 99
2026-08-09 16:00:01 +00:00
plan := assessment . Plan ( )
groups := plan . Groups ( )
members := groups [ 0 ] . MemberPositions ( )
members [ 0 ] = 99
groups [ 0 ] = PlanGroup { }
if got := snapshotPlan ( assessment . Plan ( ) ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "assessment plan changed through returned or input data: %#v" , got )
}
invalid := preparation . Assess ( ProposalResponse { DuplicateGroups : [ ] DuplicateGroup { {
2026-08-29 03:16:24 +00:00
CandidateNumbers : [ ] int { 1 , 99 } , CanonicalCandidateNumber : 1 ,
2026-08-09 16:00:01 +00:00
} } } )
issues := invalid . Issues ( )
issues [ 0 ] . GroupIndex = 99
issues [ 0 ] . Category = IssueOverlappingMember
if retained := invalid . Issues ( ) ; retained [ 0 ] . GroupIndex == 99 || retained [ 0 ] . Category == IssueOverlappingMember {
t . Fatalf ( "Issues() exposed retained state: %#v" , retained )
}
}
type planGroupSnapshot struct {
members [ ] int
canonical int
}
func snapshotPlan ( plan Plan ) [ ] planGroupSnapshot {
groups := plan . Groups ( )
snapshot := make ( [ ] planGroupSnapshot , len ( groups ) )
for index , group := range groups {
snapshot [ index ] = planGroupSnapshot {
members : group . MemberPositions ( ) ,
canonical : group . CanonicalPosition ( ) ,
}
}
return snapshot
}
func hasIssue ( issues [ ] Issue , groupIndex int , category IssueCategory ) bool {
for _ , issue := range issues {
if issue . GroupIndex == groupIndex && issue . Category == category {
return true
}
}
return false
}
func proposalPreparation ( t * testing . T ) Preparation {
t . Helper ( )
document := & source . SourceDocument { ID : "session" , Units : make ( [ ] source . SourceUnit , 7 ) }
candidates := make ( [ ] Candidate , len ( document . Units ) )
for position := range document . Units {
unitID := position + 1
document . Units [ position ] = source . SourceUnit { ID : unitID , Text : "unit" }
candidates [ position ] = Candidate {
Label : "candidate" ,
SourceRefs : [ ] source . SourceRef { {
SourceID : document . ID ,
StartUnitID : unitID ,
EndUnitID : unitID ,
} } ,
}
}
for _ , position := range [ ] int { 1 , 4 } {
candidates [ position ] . SourceRefs [ 0 ] . SourceID = "other"
}
preparation , err := Prepare ( document , candidates , Limits {
ContextRadius : 0 ,
MaximumCandidates : len ( candidates ) ,
MaximumMaterialBytes : 10000 ,
} )
if err != nil || preparation . Disposition ( ) != Ready {
t . Fatalf ( "Prepare() disposition = %v, error = %v" , preparation . Disposition ( ) , err )
}
return preparation
}