@@ -205,6 +205,162 @@ fn terminal_review_form_continuation_rejects_generic_failed_and_cancelled_runs()
205205 } ) ;
206206}
207207
208+ #[ test]
209+ fn run_next_skips_persisted_test_detached_recipe_and_executes_eligible_work ( ) {
210+ homeboy_core:: test_support:: with_isolated_home ( |_| {
211+ let options = batch_cook_options (
212+ "run-next-ineligible-test-detached" ,
213+ Arc :: new ( AcceptedDetachedAttemptDispatcher ) ,
214+ ) ;
215+ persist_initial_recipe ( & options) . expect ( "persisted test-detached recipe" ) ;
216+ agent_task_lifecycle:: submit_plan ( & options. initial_plan , Some ( & options. initial_run_id ) )
217+ . expect ( "declared Cook attempt submitted" ) ;
218+ agent_task_lifecycle:: rewrite_record_for_test ( & options. initial_run_id , |record| {
219+ record. submitted_at = "2000-01-01T00:00:00+00:00" . to_string ( ) ;
220+ } )
221+ . expect ( "deterministic durable submission timestamp" ) ;
222+ agent_task_lifecycle:: cancel_run ( & options. initial_run_id , Some ( "fixture terminal" ) )
223+ . expect ( "declared Cook attempt terminal" ) ;
224+ super :: super :: enqueue_terminal_continuation ( & options. cook_id , & options. initial_run_id )
225+ . expect ( "durable Cook continuation queued" ) ;
226+
227+ agent_task_lifecycle:: submit_plan (
228+ & batch_cook_options (
229+ "run-next-eligible-work" ,
230+ Arc :: new ( AcceptedDetachedAttemptDispatcher ) ,
231+ )
232+ . initial_plan ,
233+ Some ( "run-next-eligible-work" ) ,
234+ )
235+ . expect ( "eligible work queued" ) ;
236+
237+ let result =
238+ super :: super :: run_next_with_cook_dispatcher ( ImmediateSuccessExecutor , |_| Ok ( None ) )
239+ . expect ( "ineligible continuation does not block eligible work" ) ;
240+
241+ assert_eq ! (
242+ result. value. expect( "eligible aggregate" ) . plan_id,
243+ "run-next-eligible-work"
244+ ) ;
245+ assert_eq ! ( result. skipped. len( ) , 1 ) ;
246+ assert_eq ! ( result. skipped[ 0 ] . run_id, options. initial_run_id) ;
247+ assert_eq ! (
248+ result. skipped[ 0 ] . submitted_at. as_deref( ) ,
249+ Some ( "2000-01-01T00:00:00+00:00" )
250+ ) ;
251+ assert ! ( result. skipped[ 0 ]
252+ . age_seconds
253+ . is_some_and( |age_seconds| age_seconds > 800_000_000 ) ) ;
254+ assert_eq ! (
255+ result. skipped[ 0 ] . dispatcher_kind. as_deref( ) ,
256+ Some ( "test-detached" )
257+ ) ;
258+ assert_eq ! (
259+ result. skipped[ 0 ] . category,
260+ "cook_continuation_preflight_failed"
261+ ) ;
262+ assert_eq ! ( result. skipped[ 0 ] . error_code, "validation.invalid_argument" ) ;
263+ assert ! ( result. skipped[ 0 ] . remediation. contains( "agent-task status" ) ) ;
264+ } ) ;
265+ }
266+
267+ #[ test]
268+ fn run_next_redacts_poisoned_recipe_dispatcher_kind ( ) {
269+ homeboy_core:: test_support:: with_isolated_home ( |_| {
270+ const POISONED_KIND : & str = "LEAK_RECIPE_DISPATCHER_SECRET" ;
271+ let options = batch_cook_options (
272+ "run-next-poisoned-recipe" ,
273+ Arc :: new ( AcceptedDetachedAttemptDispatcher ) ,
274+ ) ;
275+ persist_initial_recipe ( & options) . expect ( "persisted recipe" ) ;
276+ agent_task_lifecycle:: submit_plan ( & options. initial_plan , Some ( & options. initial_run_id ) )
277+ . expect ( "declared Cook attempt submitted" ) ;
278+ agent_task_lifecycle:: cancel_run ( & options. initial_run_id , Some ( "fixture terminal" ) )
279+ . expect ( "declared Cook attempt terminal" ) ;
280+ super :: super :: enqueue_terminal_continuation ( & options. cook_id , & options. initial_run_id )
281+ . expect ( "durable Cook continuation queued" ) ;
282+
283+ let recipe_path = homeboy_core:: paths:: homeboy_data ( )
284+ . expect ( "data path" )
285+ . join ( "agent-task-cooks" )
286+ . join ( & options. cook_id )
287+ . join ( "recipe.json" ) ;
288+ let mut recipe: Value =
289+ serde_json:: from_slice ( & std:: fs:: read ( & recipe_path) . expect ( "persisted recipe bytes" ) )
290+ . expect ( "persisted recipe JSON" ) ;
291+ recipe[ "promotion_transport" ] [ "attempt_dispatch" ] [ "kind" ] =
292+ serde_json:: json!( POISONED_KIND ) ;
293+ std:: fs:: write (
294+ & recipe_path,
295+ serde_json:: to_vec ( & recipe) . expect ( "poisoned recipe JSON" ) ,
296+ )
297+ . expect ( "poisoned recipe persisted" ) ;
298+
299+ agent_task_lifecycle:: submit_plan (
300+ & batch_cook_options (
301+ "run-next-after-poisoned-recipe" ,
302+ Arc :: new ( AcceptedDetachedAttemptDispatcher ) ,
303+ )
304+ . initial_plan ,
305+ Some ( "run-next-after-poisoned-recipe" ) ,
306+ )
307+ . expect ( "eligible work queued" ) ;
308+
309+ let result =
310+ super :: super :: run_next_with_cook_dispatcher ( ImmediateSuccessExecutor , |_| Ok ( None ) )
311+ . expect ( "poisoned continuation does not block eligible work" ) ;
312+ let status = agent_task_lifecycle:: status ( & options. initial_run_id )
313+ . expect ( "continuation record status" ) ;
314+ let logs =
315+ agent_task_lifecycle:: logs ( & options. initial_run_id ) . expect ( "continuation record logs" ) ;
316+ let rendered = serde_json:: to_string ( & serde_json:: json!( {
317+ "queue_skips" : result. skipped,
318+ "status" : status,
319+ "logs" : logs,
320+ } ) )
321+ . expect ( "queue projections serialize" ) ;
322+
323+ assert_eq ! (
324+ result. value. expect( "eligible aggregate" ) . plan_id,
325+ "run-next-after-poisoned-recipe"
326+ ) ;
327+ assert ! ( !rendered. contains( POISONED_KIND ) ) ;
328+ assert ! ( rendered. contains( "cook_continuation_unsupported_dispatcher" ) ) ;
329+ assert ! ( rendered. contains( "agent-task status <run-id> --exact --full" ) ) ;
330+ } ) ;
331+ }
332+
333+ #[ test]
334+ fn malformed_continuation_does_not_head_of_line_block_run_next ( ) {
335+ homeboy_core:: test_support:: with_isolated_home ( |_| {
336+ let queue = homeboy_core:: paths:: homeboy_data ( )
337+ . expect ( "data path" )
338+ . join ( "agent-task-cook-continuations" ) ;
339+ std:: fs:: create_dir_all ( & queue) . expect ( "continuation queue" ) ;
340+ std:: fs:: write ( queue. join ( "000-malformed.pending" ) , "not JSON" )
341+ . expect ( "malformed continuation persisted" ) ;
342+ agent_task_lifecycle:: submit_plan (
343+ & batch_cook_options (
344+ "run-next-after-malformed-continuation" ,
345+ Arc :: new ( AcceptedDetachedAttemptDispatcher ) ,
346+ )
347+ . initial_plan ,
348+ Some ( "run-next-after-malformed-continuation" ) ,
349+ )
350+ . expect ( "eligible work queued" ) ;
351+
352+ let result =
353+ super :: super :: run_next_with_cook_dispatcher ( ImmediateSuccessExecutor , |_| Ok ( None ) )
354+ . expect ( "malformed continuation is skipped" ) ;
355+
356+ assert_eq ! (
357+ result. value. expect( "eligible aggregate" ) . plan_id,
358+ "run-next-after-malformed-continuation"
359+ ) ;
360+ assert ! ( queue. join( "000-malformed.failed" ) . is_file( ) ) ;
361+ } ) ;
362+ }
363+
208364#[ test]
209365fn durable_cook_inspection_reports_an_unsupported_run_schema ( ) {
210366 homeboy_core:: test_support:: with_isolated_home ( |_| {
0 commit comments