@@ -10,6 +10,7 @@ import (
1010 "github.com/stretchr/testify/assert"
1111 "github.com/stretchr/testify/mock"
1212 "github.com/stretchr/testify/require"
13+ "google.golang.org/genproto/googleapis/rpc/code"
1314 "google.golang.org/genproto/googleapis/rpc/status"
1415 "google.golang.org/grpc/codes"
1516 "google.golang.org/protobuf/proto"
@@ -55,6 +56,12 @@ func newTestActionUpdate(actionName string) (*executorv1.TaskAction, *ActionUpda
5556 return ta , update
5657}
5758
59+ func acceptedRecordActionResponse () * connect.Response [workflow.RecordActionResponse ] {
60+ return connect .NewResponse (& workflow.RecordActionResponse {
61+ Status : & status.Status {Code : int32 (code .Code_OK )},
62+ })
63+ }
64+
5865func TestNotifyRunService_DeduplicateRecordAction (t * testing.T ) {
5966 ctx := context .Background ()
6067
@@ -73,7 +80,7 @@ func TestNotifyRunService_DeduplicateRecordAction(t *testing.T) {
7380
7481 // Expect RecordAction called exactly once
7582 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
76- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
83+ Return (acceptedRecordActionResponse () , nil ).Once ()
7784
7885 // First Added event — should call RecordAction
7986 c .notifyRunService (ctx , ta , update , watch .Added )
@@ -105,7 +112,7 @@ func TestNotifyRunService_FailedRecordAllowsRetry(t *testing.T) {
105112 Return ((* connect.Response [workflow.RecordActionResponse ])(nil ), fmt .Errorf ("transient error" )).Once ()
106113 // Second call succeeds
107114 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
108- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
115+ Return (acceptedRecordActionResponse () , nil ).Once ()
109116
110117 // First event — RecordAction fails, should NOT add to filter
111118 c .notifyRunService (ctx , ta , update , watch .Added )
@@ -120,6 +127,115 @@ func TestNotifyRunService_FailedRecordAllowsRetry(t *testing.T) {
120127 mockClient .AssertNumberOfCalls (t , "RecordAction" , 2 )
121128}
122129
130+ func TestNotifyRunService_MissingResponseAllowsRetry (t * testing.T ) {
131+ for _ , tc := range []struct {
132+ name string
133+ response * connect.Response [workflow.RecordActionResponse ]
134+ }{
135+ {name : "nil response" },
136+ {name : "nil message" , response : & connect.Response [workflow.RecordActionResponse ]{}},
137+ {name : "nil status" , response : connect .NewResponse (& workflow.RecordActionResponse {})},
138+ } {
139+ t .Run (tc .name , func (t * testing.T ) {
140+ ctx := context .Background ()
141+ mockClient := runmocks .NewInternalRunServiceClient (t )
142+ filter , err := fastcheck .NewOppoBloomFilter (128 , promutils .NewTestScope ())
143+ assert .NoError (t , err )
144+ c := & ActionsClient {
145+ runClient : mockClient ,
146+ recordedFilter : filter ,
147+ subscribers : make (map [string ]map [chan * ActionUpdate ]struct {}),
148+ }
149+ ta , update := newTestActionUpdate ("action-missing-response" )
150+ mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
151+ Return (tc .response , nil ).Twice ()
152+
153+ c .notifyRunService (ctx , ta , update , watch .Added )
154+ c .notifyRunService (ctx , ta , update , watch .Added )
155+
156+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 2 )
157+ })
158+ }
159+ }
160+
161+ func TestNotifyRunService_RejectedRecordAllowsRetry (t * testing.T ) {
162+ ctx := context .Background ()
163+
164+ mockClient := runmocks .NewInternalRunServiceClient (t )
165+
166+ filter , err := fastcheck .NewOppoBloomFilter (128 , promutils .NewTestScope ())
167+ assert .NoError (t , err )
168+
169+ c := & ActionsClient {
170+ runClient : mockClient ,
171+ recordedFilter : filter ,
172+ subscribers : make (map [string ]map [chan * ActionUpdate ]struct {}),
173+ }
174+
175+ ta , update := newTestActionUpdate ("action-rejected" )
176+
177+ // The run service reports rejections in the response body with a nil
178+ // transport error, so the first two calls look like successes to connect.
179+ rejected := connect .NewResponse (& workflow.RecordActionResponse {
180+ ActionId : update .ActionID ,
181+ Status : & status.Status {
182+ Code : int32 (code .Code_INVALID_ARGUMENT ),
183+ Message : "unsupported action spec type: <nil>" ,
184+ },
185+ })
186+ mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
187+ Return (rejected , nil ).Twice ()
188+ mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
189+ Return (connect .NewResponse (& workflow.RecordActionResponse {
190+ ActionId : update .ActionID ,
191+ Status : & status.Status {Code : int32 (code .Code_OK )},
192+ }), nil ).Once ()
193+
194+ // First event — rejected, so the action must stay retryable.
195+ c .notifyRunService (ctx , ta , update , watch .Added )
196+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 1 )
197+
198+ // Second event — still not recorded, so it is sent again.
199+ c .notifyRunService (ctx , ta , update , watch .Added )
200+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 2 )
201+
202+ // Third event — accepted this time.
203+ c .notifyRunService (ctx , ta , update , watch .Added )
204+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 3 )
205+
206+ // Fourth event — now recorded, so it is deduplicated.
207+ c .notifyRunService (ctx , ta , update , watch .Added )
208+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 3 )
209+ }
210+
211+ func TestNotifyRunService_AcceptedRecordDeduplicates (t * testing.T ) {
212+ ctx := context .Background ()
213+
214+ mockClient := runmocks .NewInternalRunServiceClient (t )
215+
216+ filter , err := fastcheck .NewOppoBloomFilter (128 , promutils .NewTestScope ())
217+ assert .NoError (t , err )
218+
219+ c := & ActionsClient {
220+ runClient : mockClient ,
221+ recordedFilter : filter ,
222+ subscribers : make (map [string ]map [chan * ActionUpdate ]struct {}),
223+ }
224+
225+ ta , update := newTestActionUpdate ("action-accepted" )
226+
227+ mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
228+ Return (connect .NewResponse (& workflow.RecordActionResponse {
229+ ActionId : update .ActionID ,
230+ Status : & status.Status {Code : int32 (code .Code_OK )},
231+ }), nil ).Once ()
232+
233+ c .notifyRunService (ctx , ta , update , watch .Added )
234+ c .notifyRunService (ctx , ta , update , watch .Added )
235+
236+ mockClient .AssertNumberOfCalls (t , "RecordAction" , 1 )
237+ }
238+
123239func TestNotifyRunService_UpdateActionStatusIncludesAttemptsAndCacheStatus (t * testing.T ) {
124240 ctx := context .Background ()
125241
@@ -143,7 +259,7 @@ func TestNotifyRunService_UpdateActionStatusIncludesAttemptsAndCacheStatus(t *te
143259 })).Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil ).Once ()
144260 // First-sight MODIFIED now also records (deduped via the mandatory filter).
145261 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
146- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Maybe ()
262+ Return (acceptedRecordActionResponse () , nil ).Maybe ()
147263
148264 c .notifyRunService (ctx , ta , update , watch .Modified )
149265
@@ -476,7 +592,7 @@ func TestNotifyRunService_ChildAddedPromotesParentToRunning(t *testing.T) {
476592
477593 // Expect RecordAction for the child
478594 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
479- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
595+ Return (acceptedRecordActionResponse () , nil ).Once ()
480596
481597 // Expect UpdateActionStatus for the PARENT with RUNNING phase
482598 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .MatchedBy (func (req * connect.Request [workflow.UpdateActionStatusRequest ]) bool {
@@ -527,7 +643,7 @@ func TestNotifyRunService_SkipsTerminalAddedEventsOnlyWhenInBloomFilter(t *testi
527643
528644 // First ADDED event (cold start, not in bloom filter): should process normally
529645 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
530- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
646+ Return (acceptedRecordActionResponse () , nil ).Once ()
531647 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .Anything ).
532648 Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil )
533649 c .notifyRunService (ctx , ta , update , watch .Added )
@@ -583,7 +699,7 @@ func TestNotifyRunService_ProcessesNonTerminalAddedEvents(t *testing.T) {
583699
584700 // Non-terminal ADDED events should be processed normally
585701 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
586- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
702+ Return (acceptedRecordActionResponse () , nil ).Once ()
587703 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .Anything ).
588704 Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil ).Once ()
589705
@@ -611,7 +727,7 @@ func TestNotifyRunService_DuplicateAddedSkipsRecordAction(t *testing.T) {
611727
612728 // First call — should process normally
613729 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
614- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
730+ Return (acceptedRecordActionResponse () , nil ).Once ()
615731 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .Anything ).
616732 Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil )
617733 c .notifyRunService (ctx , ta , update , watch .Added )
@@ -641,7 +757,7 @@ func TestNotifyRunService_TerminalDuplicateRepairsTimestamps(t *testing.T) {
641757
642758 // First call — should process normally (RecordAction + UpdateActionStatus)
643759 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
644- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
760+ Return (acceptedRecordActionResponse () , nil ).Once ()
645761 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .Anything ).
646762 Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil ).Times (2 )
647763 c .notifyRunService (ctx , ta , update , watch .Added )
@@ -668,7 +784,7 @@ func TestNotifyRunService_RootActionAddedDoesNotPromoteParent(t *testing.T) {
668784 ta , update := newTestActionUpdate ("action-root" )
669785
670786 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
671- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
787+ Return (acceptedRecordActionResponse () , nil ).Once ()
672788
673789 c .notifyRunService (ctx , ta , update , watch .Added )
674790
@@ -782,7 +898,7 @@ func TestHandleWatchEvent_CoalescedReadsLatestPhase(t *testing.T) {
782898 }
783899
784900 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
785- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
901+ Return (acceptedRecordActionResponse () , nil ).Once ()
786902 // Accept exactly one status update, and ONLY if it is SUCCEEDED. A RUNNING update
787903 // would be an unexpected call and fail the test.
788904 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .MatchedBy (func (req * connect.Request [workflow.UpdateActionStatusRequest ]) bool {
@@ -827,7 +943,7 @@ func TestHandleWatchEvent_CreateThenDeleteStillRecords(t *testing.T) {
827943
828944 // Step 2: the DELETE tombstone (still carries Spec) must create the row, then abort it.
829945 mockClient .On ("RecordAction" , mock .Anything , mock .Anything ).
830- Return (& connect. Response [workflow. RecordActionResponse ]{} , nil ).Once ()
946+ Return (acceptedRecordActionResponse () , nil ).Once ()
831947 mockClient .On ("UpdateActionStatus" , mock .Anything , mock .MatchedBy (func (req * connect.Request [workflow.UpdateActionStatusRequest ]) bool {
832948 return req .Msg .GetStatus ().GetPhase () == common .ActionPhase_ACTION_PHASE_ABORTED
833949 })).Return (& connect.Response [workflow.UpdateActionStatusResponse ]{}, nil ).Once ()
0 commit comments