@@ -242,8 +242,9 @@ func TestHandler_getHistory_ExpireIfStale(t *testing.T) {
242242 Return ([]config.PurchaseExecution {freshExec (), staleExec ("pending" )}, nil )
243243 mockStore .On ("GetGlobalConfig" , ctx ).Return (& config.GlobalConfig {NotificationEmail : & approverEmail }, nil )
244244 // The goroutine uses context.Background(); context.Background() == ctx in
245- // this test, so the matcher fires correctly.
246- mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" ).
245+ // this test, so the matcher fires correctly. The trailing mock.Anything
246+ // matches the actor *string (nil for the system-initiated async expire).
247+ mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" , mock .Anything ).
247248 Run (waitForCall (done )).
248249 Return (& expired , nil ).Once ()
249250
@@ -263,7 +264,7 @@ func TestHandler_getHistory_ExpireIfStale(t *testing.T) {
263264
264265 // Exactly one Transition call, only for the stale row.
265266 mockStore .AssertNumberOfCalls (t , "TransitionExecutionStatus" , 1 )
266- mockStore .AssertCalled (t , "TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" )
267+ mockStore .AssertCalled (t , "TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" , mock . Anything )
267268
268269 historyResp := result .(HistoryResponse )
269270 require .Len (t , historyResp .Purchases , 2 , "both executions must render as history rows" )
@@ -300,7 +301,7 @@ func TestHandler_getHistory_ExpireIfStale(t *testing.T) {
300301 mockStore .On ("GetExecutionsByStatuses" , ctx , mock .Anything , mock .Anything ).
301302 Return ([]config.PurchaseExecution {staleExec ("notified" )}, nil )
302303 mockStore .On ("GetGlobalConfig" , ctx ).Return (& config.GlobalConfig {NotificationEmail : & approverEmail }, nil )
303- mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" ).
304+ mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" , mock . Anything ).
304305 Run (waitForCall (done )).
305306 Return (& expired , nil ).Once ()
306307
@@ -337,7 +338,7 @@ func TestHandler_getHistory_ExpireIfStale(t *testing.T) {
337338 mockStore .On ("GetExecutionsByStatuses" , ctx , mock .Anything , mock .Anything ).
338339 Return ([]config.PurchaseExecution {staleExec ("pending" )}, nil )
339340 mockStore .On ("GetGlobalConfig" , ctx ).Return (& config.GlobalConfig {NotificationEmail : & approverEmail }, nil )
340- mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" ).
341+ mockStore .On ("TransitionExecutionStatus" , mock .Anything , staleID , []string {"pending" , "notified" }, "expired" , mock . Anything ).
341342 Run (waitForCall (done )).
342343 Return (nil , errors .New ("simulated store failure" )).Once ()
343344
@@ -540,6 +541,38 @@ func TestHandler_getHistory_ScopedUserSeesEmptyAccountRows(t *testing.T) {
540541 assert .Equal (t , 1 , resp .Summary .TotalPending )
541542}
542543
544+ // TestHandler_expireIfStale_SystemActorIsNil asserts that the expireIfStale
545+ // helper passes nil as the actor param to TransitionExecutionStatus.
546+ // expireIfStale is a system-initiated path (no human session), so transitioned_by
547+ // must be NULL on the affected row (issue #1009).
548+ func TestHandler_expireIfStale_SystemActorIsNil (t * testing.T ) {
549+ ctx := context .Background ()
550+ mockStore := new (MockConfigStore )
551+ approverEmail := "ops@example.com"
552+ t .Cleanup (func () { mockStore .AssertExpectations (t ) })
553+
554+ staleID := "actor-nil-stale-exec"
555+ expired := config.PurchaseExecution {ExecutionID : staleID , Status : "expired" }
556+
557+ mockStore .On ("GetAllPurchaseHistory" , ctx , 100 ).Return ([]config.PurchaseHistoryRecord {}, nil )
558+ mockStore .On ("GetExecutionsByStatuses" , ctx , mock .Anything , mock .Anything ).
559+ Return ([]config.PurchaseExecution {{
560+ ExecutionID : staleID ,
561+ Status : "pending" ,
562+ ScheduledDate : time .Now ().Add (- 8 * 24 * time .Hour ),
563+ }}, nil )
564+ mockStore .On ("GetGlobalConfig" , ctx ).Return (& config.GlobalConfig {NotificationEmail : & approverEmail }, nil )
565+ // System path: expireIfStale must pass nil actor so transitioned_by = NULL.
566+ mockStore .On ("TransitionExecutionStatus" , ctx , staleID , []string {"pending" , "notified" }, "expired" ,
567+ (* string )(nil ),
568+ ).Return (& expired , nil ).Once ()
569+
570+ mockAuth , req := adminHistoryReq (ctx )
571+ handler := & Handler {auth : mockAuth , config : mockStore }
572+ _ , err := handler .getHistory (ctx , req , map [string ]string {})
573+ require .NoError (t , err )
574+ }
575+
543576// TestHandler_getHistory_PermissionDenied asserts that a non-admin user without
544577// view:purchases gets 403 and never reaches the store.
545578func TestHandler_getHistory_PermissionDenied (t * testing.T ) {
0 commit comments