@@ -103,17 +103,23 @@ fn selected_catalog_two_agent_kdl_recording_runner_matrix() {
103103 assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
104104 assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
105105 assert_eq ! ( report. launched, [ "host.owner.work" ] ) ;
106+ assert ! ( report. restarted. is_empty( ) ) ;
107+ assert ! ( report. gc. is_empty( ) ) ;
106108 }
107109 Actual :: Live => {
108110 assert ! ( runner. spawned. borrow( ) . is_empty( ) ) ;
109111 assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
110112 assert_eq ! ( report. adopted, [ "owner" ] ) ;
113+ assert ! ( report. launched. is_empty( ) ) ;
114+ assert ! ( report. restarted. is_empty( ) ) ;
115+ assert ! ( report. gc. is_empty( ) ) ;
111116 }
112117 Actual :: Dead => {
113118 assert_eq ! ( runner. reaped. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
114119 assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
115- assert_eq ! ( report. gc, [ "host.owner.work" ] ) ;
116- assert_eq ! ( report. launched, [ "host.owner.work" ] ) ;
120+ assert ! ( report. launched. is_empty( ) ) ;
121+ assert_eq ! ( report. restarted, [ "host.owner.work" ] ) ;
122+ assert ! ( report. gc. is_empty( ) ) ;
117123 }
118124 }
119125 assert ! (
@@ -349,7 +355,7 @@ fn selected_one_shot_live_adopts_without_actions() {
349355}
350356
351357#[ test]
352- fn selected_one_shot_dead_reaps_and_relaunches_only_selected ( ) {
358+ fn selected_one_shot_reports_a_dead_task_only_as_restarted ( ) {
353359 let runner = FakeRunner {
354360 sessions : vec ! [
355361 dead( "host.agent.work" ) ,
@@ -375,8 +381,9 @@ fn selected_one_shot_dead_reaps_and_relaunches_only_selected() {
375381 assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.agent.work" ] ) ;
376382 assert ! ( runner. killed. borrow( ) . is_empty( ) ) ;
377383 assert ! ( runner. removed. borrow( ) . is_empty( ) ) ;
378- assert_eq ! ( report. gc, [ "host.agent.work" ] ) ;
379- assert_eq ! ( report. launched, [ "host.agent.work" ] ) ;
384+ assert ! ( report. launched. is_empty( ) ) ;
385+ assert_eq ! ( report. restarted, [ "host.agent.work" ] ) ;
386+ assert ! ( report. gc. is_empty( ) ) ;
380387}
381388
382389#[ test]
@@ -444,6 +451,7 @@ struct FakeRunner {
444451 killed : RefCell < Vec < String > > ,
445452 reaped : RefCell < Vec < String > > ,
446453 removed : RefCell < Vec < String > > ,
454+ ops : RefCell < Vec < String > > ,
447455}
448456
449457impl Runner for FakeRunner {
@@ -458,6 +466,9 @@ impl Runner for FakeRunner {
458466 if self . fail_spawn . as_deref ( ) == Some ( target. pty_id . as_str ( ) ) {
459467 anyhow:: bail!( "simulated spawn failure" ) ;
460468 }
469+ self . ops
470+ . borrow_mut ( )
471+ . push ( format ! ( "spawn:{}" , target. pty_id) ) ;
461472 self . spawned . borrow_mut ( ) . push ( target. pty_id . clone ( ) ) ;
462473 self . spawn_dirs
463474 . borrow_mut ( )
@@ -469,6 +480,7 @@ impl Runner for FakeRunner {
469480 Ok ( ( ) )
470481 }
471482 fn reap_for_restart ( & self , pty_id : & str ) -> anyhow:: Result < ( ) > {
483+ self . ops . borrow_mut ( ) . push ( format ! ( "reap:{pty_id}" ) ) ;
472484 self . reaped . borrow_mut ( ) . push ( pty_id. to_string ( ) ) ;
473485 if self . fail_reap . as_deref ( ) == Some ( pty_id) {
474486 anyhow:: bail!( "reap broke" ) ;
@@ -525,6 +537,8 @@ fn up_once_launches_all_tasks_of_a_fresh_agent() {
525537 let mut launched = report. launched . clone ( ) ;
526538 launched. sort ( ) ;
527539 assert_eq ! ( launched, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
540+ assert ! ( report. restarted. is_empty( ) ) ;
541+ assert ! ( report. gc. is_empty( ) ) ;
528542 assert ! ( report. errors. is_empty( ) ) ;
529543 let dirs = runner. spawn_dirs . borrow ( ) ;
530544 assert ! ( dirs. iter( ) . all( |( _, d) | d. ends_with( "agents/hetz/demo" ) ) ) ;
@@ -614,7 +628,7 @@ fn up_once_collects_spawn_errors_without_aborting() {
614628}
615629
616630#[ test]
617- fn up_once_reaps_dead_nonkeep_then_respawns ( ) {
631+ fn up_once_reports_a_successful_replacement_only_as_restarted ( ) {
618632 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
619633 write ( tmp. path ( ) , "agents/hetz/demo/agent.toml" , AGENT ) ;
620634 let runner = FakeRunner {
@@ -627,9 +641,26 @@ fn up_once_reaps_dead_nonkeep_then_respawns() {
627641 assert_eq ! ( reaped, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
628642 assert ! (
629643 runner. removed. borrow( ) . is_empty( ) ,
630- "a crash restart is not final retirement cleanup"
644+ "a restart must not remove final retirement state"
645+ ) ;
646+ assert ! ( report. launched. is_empty( ) ) ;
647+ let mut restarted = report. restarted . clone ( ) ;
648+ restarted. sort ( ) ;
649+ assert_eq ! ( restarted, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
650+ assert ! (
651+ report. gc. is_empty( ) ,
652+ "a successful restart must not be reported as final garbage collection"
653+ ) ;
654+ assert_eq ! (
655+ runner. ops. borrow( ) . as_slice( ) ,
656+ [
657+ "reap:hetz.demo-claude" ,
658+ "spawn:hetz.demo-claude" ,
659+ "reap:hetz.demo.ding" ,
660+ "spawn:hetz.demo.ding" ,
661+ ] ,
662+ "st2 must reap each dead record before it starts the replacement"
631663 ) ;
632- assert_eq ! ( report. launched. len( ) , 2 ) ;
633664}
634665
635666#[ test]
@@ -644,7 +675,9 @@ fn up_once_does_not_restart_a_task_when_diagnostic_reap_fails() {
644675
645676 let report = up_once ( tmp. path ( ) , "hetz" , & runner) . unwrap ( ) ;
646677
647- assert_eq ! ( report. launched, vec![ "hetz.demo.ding" ] ) ;
678+ assert ! ( report. launched. is_empty( ) ) ;
679+ assert_eq ! ( report. restarted, vec![ "hetz.demo.ding" ] ) ;
680+ assert ! ( report. gc. is_empty( ) ) ;
648681 assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "hetz.demo.ding" ] ) ;
649682 assert ! (
650683 report
@@ -654,6 +687,34 @@ fn up_once_does_not_restart_a_task_when_diagnostic_reap_fails() {
654687 ) ;
655688}
656689
690+ #[ test]
691+ fn up_once_does_not_report_failed_replacement_as_restarted ( ) {
692+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
693+ write ( tmp. path ( ) , "agents/hetz/demo/agent.toml" , AGENT ) ;
694+ let runner = FakeRunner {
695+ sessions : vec ! [ dead( "hetz.demo-claude" ) , live( "hetz.demo.ding" ) ] ,
696+ fail_spawn : Some ( "hetz.demo-claude" . into ( ) ) ,
697+ ..Default :: default ( )
698+ } ;
699+
700+ let report = up_once ( tmp. path ( ) , "hetz" , & runner) . unwrap ( ) ;
701+
702+ assert_eq ! (
703+ runner. reaped. borrow( ) . as_slice( ) ,
704+ [ "hetz.demo-claude" ] ,
705+ "st2 must reap the stale record before it starts a replacement"
706+ ) ;
707+ assert ! ( report. launched. is_empty( ) ) ;
708+ assert ! ( report. restarted. is_empty( ) ) ;
709+ assert ! ( report. gc. is_empty( ) ) ;
710+ assert ! (
711+ report
712+ . errors
713+ . iter( )
714+ . any( |error| error == "spawn hetz.demo-claude: simulated spawn failure" )
715+ ) ;
716+ }
717+
657718#[ test]
658719fn up_once_finally_removes_dead_retired_tasks_without_restarting_them ( ) {
659720 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -675,6 +736,7 @@ fn up_once_finally_removes_dead_retired_tasks_without_restarting_them() {
675736 assert_eq ! ( removed, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
676737 assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
677738 assert ! ( report. launched. is_empty( ) ) ;
739+ assert ! ( report. restarted. is_empty( ) ) ;
678740 assert_eq ! ( report. gc. len( ) , 2 ) ;
679741}
680742
0 commit comments