@@ -4240,7 +4240,7 @@ func TestTUIPendingAddressedNotClearedByStaleResponse(t *testing.T) {
42404240 }
42414241}
42424242
4243- func TestTUIPendingAddressedClearedByMatchingResponse (t * testing.T ) {
4243+ func TestTUIPendingAddressedNotClearedOnSuccess (t * testing.T ) {
42444244 m := newTuiModel ("http://localhost" )
42454245
42464246 addressedFalse := false
@@ -4251,21 +4251,83 @@ func TestTUIPendingAddressedClearedByMatchingResponse(t *testing.T) {
42514251 // User toggles addressed to true
42524252 m .pendingAddressed [1 ] = pendingState {newState : true , seq : 1 }
42534253
4254- // Response comes back for the correct toggle to true
4255- matchingMsg := tuiAddressedResultMsg {
4254+ // Success response comes back
4255+ successMsg := tuiAddressedResultMsg {
42564256 jobID : 1 ,
42574257 oldState : false ,
4258- newState : true , // This response matches what we requested
4259- seq : 1 , // Matches pending seq
4258+ newState : true ,
4259+ seq : 1 ,
42604260 err : nil ,
42614261 }
42624262
4263- updated , _ := m .Update (matchingMsg )
4263+ updated , _ := m .Update (successMsg )
42644264 m2 := updated .(tuiModel )
42654265
4266- // pendingAddressed should be cleared because newState matches
4266+ // pendingAddressed should NOT be cleared on success - it waits for jobs refresh
4267+ // to confirm the update. This prevents race condition where stale jobs response
4268+ // arrives after success and briefly shows old state.
4269+ if _ , ok := m2 .pendingAddressed [1 ]; ! ok {
4270+ t .Error ("pendingAddressed should NOT be cleared on success response" )
4271+ }
4272+ }
4273+
4274+ func TestTUIPendingAddressedClearedByJobsRefresh (t * testing.T ) {
4275+ m := newTuiModel ("http://localhost" )
4276+
4277+ addressedFalse := false
4278+ m .jobs = []storage.ReviewJob {
4279+ {ID : 1 , Status : storage .JobStatusDone , Addressed : & addressedFalse },
4280+ }
4281+
4282+ // User toggles addressed to true
4283+ m .pendingAddressed [1 ] = pendingState {newState : true , seq : 1 }
4284+
4285+ // Jobs refresh arrives with server data confirming the update
4286+ addressedTrue := true
4287+ jobsMsg := tuiJobsMsg {
4288+ jobs : []storage.ReviewJob {
4289+ {ID : 1 , Status : storage .JobStatusDone , Addressed : & addressedTrue },
4290+ },
4291+ }
4292+
4293+ updated , _ := m .Update (jobsMsg )
4294+ m2 := updated .(tuiModel )
4295+
4296+ // pendingAddressed should be cleared because server data matches pending state
42674297 if _ , ok := m2 .pendingAddressed [1 ]; ok {
4268- t .Error ("pendingAddressed should be cleared when response newState matches pending state" )
4298+ t .Error ("pendingAddressed should be cleared when jobs refresh confirms update" )
4299+ }
4300+ }
4301+
4302+ func TestTUIPendingAddressedNotClearedByStaleJobsRefresh (t * testing.T ) {
4303+ m := newTuiModel ("http://localhost" )
4304+
4305+ addressedFalse := false
4306+ m .jobs = []storage.ReviewJob {
4307+ {ID : 1 , Status : storage .JobStatusDone , Addressed : & addressedFalse },
4308+ }
4309+
4310+ // User toggles addressed to true
4311+ m .pendingAddressed [1 ] = pendingState {newState : true , seq : 1 }
4312+
4313+ // Stale jobs refresh arrives with old data (from request sent before update)
4314+ staleJobsMsg := tuiJobsMsg {
4315+ jobs : []storage.ReviewJob {
4316+ {ID : 1 , Status : storage .JobStatusDone , Addressed : & addressedFalse }, // Still false!
4317+ },
4318+ }
4319+
4320+ updated , _ := m .Update (staleJobsMsg )
4321+ m2 := updated .(tuiModel )
4322+
4323+ // pendingAddressed should NOT be cleared because server data doesn't match
4324+ if _ , ok := m2 .pendingAddressed [1 ]; ! ok {
4325+ t .Error ("pendingAddressed should NOT be cleared when jobs refresh has stale data" )
4326+ }
4327+
4328+ // Job should still show as addressed (pending state re-applied)
4329+ if m2 .jobs [0 ].Addressed == nil || ! * m2 .jobs [0 ].Addressed {
4330+ t .Error ("Job should still show as addressed due to pending state" )
42694331 }
42704332}
42714333
@@ -4632,3 +4694,93 @@ func TestTUIQueueNoScrollIndicatorPads(t *testing.T) {
46324694 t .Errorf ("Queue with few jobs should maintain height, got %d lines for height %d" , len (lines ), m .height )
46334695 }
46344696}
4697+
4698+ func TestTUIPendingReviewAddressedClearedOnSuccess (t * testing.T ) {
4699+ // pendingReviewAddressed (for reviews without jobs) should be cleared on success
4700+ // because the race condition only affects jobs list refresh, not review-only items
4701+ m := newTuiModel ("http://localhost" )
4702+
4703+ // Track a review-only pending state (no job ID)
4704+ m .pendingReviewAddressed [42 ] = pendingState {newState : true , seq : 1 }
4705+
4706+ // Success response for review-only (jobID=0, reviewID=42)
4707+ successMsg := tuiAddressedResultMsg {
4708+ jobID : 0 , // No job - this is review-only
4709+ reviewID : 42 ,
4710+ reviewView : true ,
4711+ oldState : false ,
4712+ newState : true ,
4713+ seq : 1 ,
4714+ err : nil ,
4715+ }
4716+
4717+ updated , _ := m .Update (successMsg )
4718+ m2 := updated .(tuiModel )
4719+
4720+ // pendingReviewAddressed SHOULD be cleared on success (no race condition for review-only)
4721+ if _ , ok := m2 .pendingReviewAddressed [42 ]; ok {
4722+ t .Error ("pendingReviewAddressed should be cleared on success for review-only items" )
4723+ }
4724+ }
4725+
4726+ func TestTUIPendingAddressedClearsWhenServerNilMatchesFalse (t * testing.T ) {
4727+ // When pending newState is false and server Addressed is nil,
4728+ // treat nil as false and clear the pending state
4729+ m := newTuiModel ("http://localhost" )
4730+
4731+ // Job with nil Addressed (e.g., partial payload or non-done status that became done)
4732+ m .jobs = []storage.ReviewJob {
4733+ {ID : 1 , Status : storage .JobStatusDone , Addressed : nil },
4734+ }
4735+
4736+ // User had toggled to false (unaddressed)
4737+ m .pendingAddressed [1 ] = pendingState {newState : false , seq : 1 }
4738+
4739+ // Jobs refresh arrives with nil Addressed (should match false)
4740+ jobsMsg := tuiJobsMsg {
4741+ jobs : []storage.ReviewJob {
4742+ {ID : 1 , Status : storage .JobStatusDone , Addressed : nil },
4743+ },
4744+ }
4745+
4746+ updated , _ := m .Update (jobsMsg )
4747+ m2 := updated .(tuiModel )
4748+
4749+ // pendingAddressed should be cleared because nil == false matches newState=false
4750+ if _ , ok := m2 .pendingAddressed [1 ]; ok {
4751+ t .Error ("pendingAddressed should be cleared when server nil matches pending false" )
4752+ }
4753+ }
4754+
4755+ func TestTUIPendingAddressedNotClearsWhenServerNilMismatchesTrue (t * testing.T ) {
4756+ // When pending newState is true and server Addressed is nil,
4757+ // do NOT clear (nil != true)
4758+ m := newTuiModel ("http://localhost" )
4759+
4760+ m .jobs = []storage.ReviewJob {
4761+ {ID : 1 , Status : storage .JobStatusDone , Addressed : nil },
4762+ }
4763+
4764+ // User toggled to true (addressed)
4765+ m .pendingAddressed [1 ] = pendingState {newState : true , seq : 1 }
4766+
4767+ // Jobs refresh arrives with nil Addressed (doesn't match true)
4768+ jobsMsg := tuiJobsMsg {
4769+ jobs : []storage.ReviewJob {
4770+ {ID : 1 , Status : storage .JobStatusDone , Addressed : nil },
4771+ },
4772+ }
4773+
4774+ updated , _ := m .Update (jobsMsg )
4775+ m2 := updated .(tuiModel )
4776+
4777+ // pendingAddressed should NOT be cleared because nil != true
4778+ if _ , ok := m2 .pendingAddressed [1 ]; ! ok {
4779+ t .Error ("pendingAddressed should NOT be cleared when server nil mismatches pending true" )
4780+ }
4781+
4782+ // Job should show as addressed due to pending state re-applied
4783+ if m2 .jobs [0 ].Addressed == nil || ! * m2 .jobs [0 ].Addressed {
4784+ t .Error ("Job should show as addressed due to pending state" )
4785+ }
4786+ }
0 commit comments