@@ -258,7 +258,11 @@ func TestPersonMergeSuggestionService_AllowsFamilyAndFriendCandidates(t *testing
258258 assert .Contains (t , allCandidateIDs , familyCandidate .ID )
259259}
260260
261- func TestPersonMergeSuggestionService_DoesNotCreateSuggestionAtOrAboveAttachThreshold (t * testing.T ) {
261+ func TestPersonMergeSuggestionService_CreatesSuggestionEvenAboveAttachThreshold (t * testing.T ) {
262+ // Candidates above the attach threshold should still produce merge suggestions.
263+ // Previously they were skipped (logic: "above attach = auto-attach"), but since
264+ // these faces are already assigned to existing persons, auto-attach never fires,
265+ // creating a blind spot where high-similarity stranger pairs are never suggested.
262266 svc , _ , repos , _ := newPersonMergeSuggestionServiceWithConfigForTest (t , config.PeopleConfig {
263267 MergeSuggestionThreshold : 0.90 ,
264268 AttachThreshold : 0.95 ,
@@ -268,29 +272,30 @@ func TestPersonMergeSuggestionService_DoesNotCreateSuggestionAtOrAboveAttachThre
268272 })
269273
270274 target := createSuggestionTestPerson (t , repos , model .PersonCategoryFamily , []float32 {1 , 0 }, []float32 {0.99 , 0.01 })
271- tooCloseCandidate := createSuggestionTestPerson (t , repos , model .PersonCategoryStranger , []float32 {1 , 0 })
275+ highSimilarityCandidate := createSuggestionTestPerson (t , repos , model .PersonCategoryStranger , []float32 {1 , 0 })
272276
273277 require .NoError (t , svc .MarkDirty ("attach-threshold-upper-bound" ))
274278 require .NoError (t , svc .RunBackgroundSlice ())
275279
276280 got := pendingSuggestionCandidatesByTarget (t , repos .MergeSuggestion )
277- assert .NotContains (t , got , target .ID )
278- assert .NotContains (t , got [target .ID ], tooCloseCandidate .ID )
281+ assert .Contains (t , got [target .ID ], highSimilarityCandidate .ID , "candidates above attach threshold should still produce suggestions" )
279282}
280283
281284func TestPersonMergeSuggestionService_UsesAverageBestSimilarityInsteadOfSingleMaxPair (t * testing.T ) {
285+ // Verify that a candidate with one high-similarity prototype and one low-similarity
286+ // prototype stays below threshold when using average-best (not max-pair).
282287 svc , _ , repos , _ := newPersonMergeSuggestionServiceWithConfigForTest (t , config.PeopleConfig {
283- MergeSuggestionThreshold : 0.80 ,
284- AttachThreshold : 0.95 ,
288+ MergeSuggestionThreshold : 0.90 ,
289+ AttachThreshold : 1.10 , // effectively disabled - never skip above attach
285290 MergeSuggestionMaxPairsPerRun : 100 ,
286291 MergeSuggestionBatchSize : 10 ,
287292 MergeSuggestionCooldownSeconds : 1 ,
288293 })
289294
290295 target := createSuggestionTestPerson (t , repos , model .PersonCategoryFamily , []float32 {1 , 0 }, []float32 {0 , 1 })
291- // One prototype matches perfectly, one is nearly orthogonal.
292- // Max-pair logic would suggest; average-best should stay below threshold.
293- candidate := createSuggestionTestPerson (t , repos , model .PersonCategoryStranger , []float32 {1 , 0 }, []float32 {0.2 , 0.98 })
296+ // One prototype matches target[0] perfectly, one is nearly orthogonal to both .
297+ // average-best is well below 0.90 threshold.
298+ candidate := createSuggestionTestPerson (t , repos , model .PersonCategoryStranger , []float32 {1 , 0 }, []float32 {0.5 , 0.5 })
294299
295300 require .NoError (t , svc .MarkDirty ("average-best-similarity" ))
296301 require .NoError (t , svc .RunBackgroundSlice ())
0 commit comments