11use std:: fs;
22use std:: path:: { Path , PathBuf } ;
33use std:: process:: { Command , Stdio } ;
4- use std:: sync:: { Arc , Barrier , Mutex } ;
4+ use std:: sync:: { Arc , Barrier , Mutex , mpsc} ;
5+ use std:: time:: Duration ;
56
67use sha2:: { Digest as _, Sha256 } ;
78use st2:: event:: { self , EventReceiptStatus , RING_CAPACITY } ;
@@ -412,7 +413,155 @@ fn supersede_skips_an_archived_head_and_retires_the_latest_unread_predecessor()
412413}
413414
414415#[ test]
415- fn failed_predecessor_archive_leaves_the_successor_unread_and_replay_completes ( ) {
416+ fn supersede_skips_an_archive_shadowed_inbox_candidate ( ) {
417+ let catalog = tempfile:: tempdir ( ) . unwrap ( ) ;
418+ let agent = declare_agent ( catalog. path ( ) , "\" running\" " , " stream \" gh-ci\" {}\n " ) ;
419+ let older = emit ( catalog. path ( ) , "pr1-queued" , Some ( "pr-1" ) , false ) ;
420+ let shadowed = emit ( catalog. path ( ) , "pr1-running" , Some ( "pr-1" ) , false ) ;
421+ let inbox = message:: inbox_dir ( & agent) ;
422+ let archive = message:: archive_dir ( & agent) ;
423+ fs:: create_dir_all ( & archive) . unwrap ( ) ;
424+ fs:: copy (
425+ inbox. join ( & shadowed. filename ) ,
426+ archive. join ( & shadowed. filename ) ,
427+ )
428+ . unwrap ( ) ;
429+
430+ let successor = emit ( catalog. path ( ) , "pr1-pass" , Some ( "pr-1" ) , true ) ;
431+
432+ assert_eq ! (
433+ successor. superseded. as_deref( ) ,
434+ Some ( older. filename. as_str( ) )
435+ ) ;
436+ assert ! ( inbox. join( & shadowed. filename) . is_file( ) ) ;
437+ assert ! ( archive. join( & shadowed. filename) . is_file( ) ) ;
438+ assert ! ( archive. join( & older. filename) . is_file( ) ) ;
439+ }
440+
441+ #[ test]
442+ fn initial_supersession_authenticates_predecessor_immediately_before_archive ( ) {
443+ let catalog = tempfile:: tempdir ( ) . unwrap ( ) ;
444+ let agent = declare_agent ( catalog. path ( ) , "\" running\" " , " stream \" gh-ci\" {}\n " ) ;
445+ let predecessor = emit ( catalog. path ( ) , "running" , Some ( "pr-1" ) , false ) ;
446+ let inbox = message:: inbox_dir ( & agent) ;
447+ fs:: write ( inbox. join ( & predecessor. filename ) , "forged predecessor" ) . unwrap ( ) ;
448+
449+ let error = event:: emit (
450+ catalog. path ( ) ,
451+ "hetz" ,
452+ "hetz.worker" ,
453+ "gh-ci" ,
454+ "passed" ,
455+ Some ( "pr-1" ) ,
456+ Some ( "passed" ) ,
457+ "passed" ,
458+ true ,
459+ )
460+ . unwrap_err ( ) ;
461+
462+ assert ! ( error. to_string( ) . contains( "different bytes" ) , "{error:#}" ) ;
463+ assert ! ( inbox. join( & predecessor. filename) . is_file( ) ) ;
464+ assert ! (
465+ !message:: archive_dir( & agent)
466+ . join( & predecessor. filename)
467+ . exists( )
468+ ) ;
469+ assert ! (
470+ message:: list_inbox( & inbox)
471+ . unwrap( )
472+ . iter( )
473+ . any( |message| { message. event_id. as_deref( ) == Some ( "passed" ) } )
474+ ) ;
475+ }
476+
477+ #[ test]
478+ fn catalog_authoring_and_emit_linearize_without_deadlock ( ) {
479+ let catalog = tempfile:: tempdir ( ) . unwrap ( ) ;
480+ declare_agent ( catalog. path ( ) , "\" running\" " , " stream \" gh-ci\" {}\n " ) ;
481+ let admission = st2:: CatalogLock :: shared ( catalog. path ( ) ) . unwrap ( ) ;
482+ let root = catalog. path ( ) . to_path_buf ( ) ;
483+ let ( done_tx, done_rx) = mpsc:: channel ( ) ;
484+ let author = std:: thread:: spawn ( move || {
485+ let result = st2:: agent_author:: remove_stream ( & root, "hetz.worker" , "hetz" , None , "gh-ci" ) ;
486+ done_tx. send ( result) . unwrap ( ) ;
487+ } ) ;
488+ assert ! ( done_rx. recv_timeout( Duration :: from_millis( 100 ) ) . is_err( ) ) ;
489+
490+ let receipt = emit ( catalog. path ( ) , "before-remove" , None , false ) ;
491+ assert_eq ! ( receipt. status, EventReceiptStatus :: Created ) ;
492+ drop ( admission) ;
493+ done_rx
494+ . recv_timeout ( Duration :: from_secs ( 5 ) )
495+ . unwrap ( )
496+ . unwrap ( ) ;
497+ author. join ( ) . unwrap ( ) ;
498+
499+ let error = event:: emit (
500+ catalog. path ( ) ,
501+ "hetz" ,
502+ "hetz.worker" ,
503+ "gh-ci" ,
504+ "after-remove" ,
505+ None ,
506+ None ,
507+ "after" ,
508+ false ,
509+ )
510+ . unwrap_err ( ) ;
511+ assert ! (
512+ error. to_string( ) . contains( "does not declare stream" ) ,
513+ "{error:#}"
514+ ) ;
515+ }
516+
517+ #[ test]
518+ fn desired_state_authoring_and_emit_linearize_without_deadlock ( ) {
519+ let catalog = tempfile:: tempdir ( ) . unwrap ( ) ;
520+ declare_agent ( catalog. path ( ) , "\" running\" " , " stream \" gh-ci\" {}\n " ) ;
521+ let admission = st2:: CatalogLock :: shared ( catalog. path ( ) ) . unwrap ( ) ;
522+ let root = catalog. path ( ) . to_path_buf ( ) ;
523+ let ( done_tx, done_rx) = mpsc:: channel ( ) ;
524+ let author = std:: thread:: spawn ( move || {
525+ let result = st2:: agent_author:: set_desired_state (
526+ & root,
527+ "hetz.worker" ,
528+ "hetz" ,
529+ None ,
530+ st2:: agent_author:: DesiredStateValue :: Suspended ,
531+ Some ( "maintenance" ) ,
532+ ) ;
533+ done_tx. send ( result) . unwrap ( ) ;
534+ } ) ;
535+ assert ! ( done_rx. recv_timeout( Duration :: from_millis( 100 ) ) . is_err( ) ) ;
536+
537+ assert_eq ! (
538+ emit( catalog. path( ) , "before-suspend" , None , false ) . status,
539+ EventReceiptStatus :: Created
540+ ) ;
541+ drop ( admission) ;
542+ done_rx
543+ . recv_timeout ( Duration :: from_secs ( 5 ) )
544+ . unwrap ( )
545+ . unwrap ( ) ;
546+ author. join ( ) . unwrap ( ) ;
547+
548+ let error = event:: emit (
549+ catalog. path ( ) ,
550+ "hetz" ,
551+ "hetz.worker" ,
552+ "gh-ci" ,
553+ "after-suspend" ,
554+ None ,
555+ None ,
556+ "after" ,
557+ false ,
558+ )
559+ . unwrap_err ( ) ;
560+ assert ! ( error. to_string( ) . contains( "is suspended" ) , "{error:#}" ) ;
561+ }
562+
563+ #[ test]
564+ fn invalid_archive_receipt_blocks_supersession_before_successor_publication ( ) {
416565 let catalog = tempfile:: tempdir ( ) . unwrap ( ) ;
417566 let agent = declare_agent ( catalog. path ( ) , "\" running\" " , " stream \" gh-ci\" {}\n " ) ;
418567 let predecessor = emit ( catalog. path ( ) , "pr1-running" , Some ( "pr-1" ) , false ) ;
@@ -438,17 +587,17 @@ fn failed_predecessor_archive_leaves_the_successor_unread_and_replay_completes()
438587 "{error:#}"
439588 ) ;
440589 let unread = message:: list_inbox ( & inbox) . unwrap ( ) ;
441- assert_eq ! ( unread. len( ) , 2 ) ;
590+ assert_eq ! ( unread. len( ) , 1 ) ;
442591 assert ! (
443592 unread
444593 . iter( )
445- . any ( |message| message. event_id. as_deref( ) = = Some ( "pr1-pass" ) )
594+ . all ( |message| message. event_id. as_deref( ) ! = Some ( "pr1-pass" ) )
446595 ) ;
447596 assert ! ( inbox. join( & predecessor. filename) . exists( ) ) ;
448597
449598 fs:: remove_dir ( archive. join ( & predecessor. filename ) ) . unwrap ( ) ;
450599 let replay = emit ( catalog. path ( ) , "pr1-pass" , Some ( "pr-1" ) , true ) ;
451- assert_eq ! ( replay. status, EventReceiptStatus :: Deduplicated ) ;
600+ assert_eq ! ( replay. status, EventReceiptStatus :: Created ) ;
452601 assert_eq ! (
453602 replay. superseded. as_deref( ) ,
454603 Some ( predecessor. filename. as_str( ) )
0 commit comments