@@ -435,7 +435,7 @@ func TestThreeWayMergeNoChange(t *testing.T) {
435435 RemoteOptionIDBase : "OPT_PROG" ,
436436 }
437437
438- decision := threeWayMerge (StageActive , "OPT_PROG" , base )
438+ decision := threeWayMerge (StageActive , "OPT_PROG" , "" , base )
439439
440440 if decision .Action != "none" {
441441 t .Errorf ("action = %q, want %q" , decision .Action , "none" )
@@ -452,7 +452,7 @@ func TestThreeWayMergeLocalChanged(t *testing.T) {
452452 RemoteOptionIDBase : "OPT_PROG" ,
453453 }
454454
455- decision := threeWayMerge (StageComplete , "OPT_PROG" , base )
455+ decision := threeWayMerge (StageComplete , "OPT_PROG" , "" , base )
456456
457457 if decision .Action != "push-local" {
458458 t .Errorf ("action = %q, want %q" , decision .Action , "push-local" )
@@ -475,7 +475,7 @@ func TestThreeWayMergeRemoteChanged(t *testing.T) {
475475
476476 // Remote changed (human moved card from "In progress" to "Done")
477477 // but local didn't change
478- decision := threeWayMerge (StageActive , "OPT_DONE" , base )
478+ decision := threeWayMerge (StageActive , "OPT_DONE" , "" , base )
479479
480480 if decision .Action != "report-remote-move" {
481481 t .Errorf ("action = %q, want %q (human move detection)" , decision .Action , "report-remote-move" )
@@ -496,7 +496,7 @@ func TestThreeWayMergeConflict(t *testing.T) {
496496 }
497497
498498 // Both changed: local to complete, remote to blocked
499- decision := threeWayMerge (StageComplete , "OPT_BLOCKED" , base )
499+ decision := threeWayMerge (StageComplete , "OPT_BLOCKED" , "" , base )
500500
501501 if decision .Action != "report-conflict" {
502502 t .Errorf ("action = %q, want %q" , decision .Action , "report-conflict" )
@@ -509,6 +509,38 @@ func TestThreeWayMergeConflict(t *testing.T) {
509509 }
510510}
511511
512+ // TestThreeWayMergeConvergenceCompleteToDone verifies that when both sides changed but
513+ // converged to the same state, no conflict is reported.
514+ func TestThreeWayMergeConvergenceCompleteToDone (t * testing.T ) {
515+ base := BoardBinding {
516+ LocalStageBase : StageActive ,
517+ RemoteOptionIDBase : "OPT_PROG" ,
518+ }
519+
520+ // Both changed: local to complete, remote to "Done" (which is what complete maps to)
521+ decision := threeWayMerge (StageComplete , "OPT_DONE" , "OPT_DONE" , base )
522+
523+ if decision .Action != "converged" {
524+ t .Errorf ("action = %q, want %q" , decision .Action , "converged" )
525+ }
526+ }
527+
528+ // TestThreeWayMergeConflictWithExpected verifies that when both sides changed and didn't converge,
529+ // conflict is still reported even with expectedRemote set.
530+ func TestThreeWayMergeConflictWithExpected (t * testing.T ) {
531+ base := BoardBinding {
532+ LocalStageBase : StageActive ,
533+ RemoteOptionIDBase : "OPT_PROG" ,
534+ }
535+
536+ // Both changed: local to complete (expects OPT_DONE), but remote moved to BLOCKED
537+ decision := threeWayMerge (StageComplete , "OPT_BLOCKED" , "OPT_DONE" , base )
538+
539+ if decision .Action != "report-conflict" {
540+ t .Errorf ("action = %q, want %q" , decision .Action , "report-conflict" )
541+ }
542+ }
543+
512544// TestThreeWayMergeHumanMoveToBacklog verifies a specific real scenario.
513545func TestThreeWayMergeHumanMoveToBacklog (t * testing.T ) {
514546 base := BoardBinding {
@@ -517,7 +549,7 @@ func TestThreeWayMergeHumanMoveToBacklog(t *testing.T) {
517549 }
518550
519551 // Human moved card back to backlog/todo
520- decision := threeWayMerge (StageActive , "OPT_TODO" , base )
552+ decision := threeWayMerge (StageActive , "OPT_TODO" , "" , base )
521553
522554 if decision .Action != "report-remote-move" {
523555 t .Errorf ("action = %q, want %q" , decision .Action , "report-remote-move" )
@@ -772,7 +804,7 @@ func TestSaveBoardStateAtomicWrite(t *testing.T) {
772804// TestArchivedStageOnBoard verifies archived changes map to terminal status.
773805func TestArchivedStageOnBoard (t * testing.T ) {
774806 target := BoardTarget {
775- Owner : "owner" ,
807+ Owner : "owner" ,
776808 Number : 5 ,
777809 }
778810
@@ -794,9 +826,25 @@ func TestThreeWayMergeConvergence(t *testing.T) {
794826 }
795827
796828 // Both changed: local from backlog→active, remote from OPT_TODO→OPT_PROG
797- // This is a "both changed" case — currently reported as conflict.
798- decision := threeWayMerge (StageActive , "OPT_PROG" , base )
829+ // This is a "both changed" case — reported as conflict.
830+ decision := threeWayMerge (StageActive , "OPT_PROG" , "" , base )
799831 if decision .Action != "report-conflict" {
800832 t .Errorf ("action = %q, want %q" , decision .Action , "report-conflict" )
801833 }
802834}
835+
836+ // TestThreeWayMergeConvergenceBothChanged verifies that when both sides changed
837+ // but the local stage maps to the same option ID as the remote, it converges.
838+ func TestThreeWayMergeConvergenceBothChanged (t * testing.T ) {
839+ base := BoardBinding {
840+ LocalStageBase : StageBacklog ,
841+ RemoteOptionIDBase : "OPT_TODO" ,
842+ }
843+
844+ // Both changed: local from backlog→active, remote from OPT_TODO→OPT_PROG
845+ // Local stage (active) maps to OPT_PROG, which matches the remote.
846+ decision := threeWayMerge (StageActive , "OPT_PROG" , "OPT_PROG" , base )
847+ if decision .Action != "converged" {
848+ t .Errorf ("action = %q, want %q" , decision .Action , "converged" )
849+ }
850+ }
0 commit comments