@@ -21,6 +21,7 @@ struct FakeRunner {
2121 killed : RefCell < Vec < String > > ,
2222 reaped : RefCell < Vec < String > > ,
2323 removed : RefCell < Vec < String > > ,
24+ ops : RefCell < Vec < String > > ,
2425}
2526
2627impl Runner for FakeRunner {
@@ -34,6 +35,9 @@ impl Runner for FakeRunner {
3435 if self . fail_spawn . as_deref ( ) == Some ( target. pty_id . as_str ( ) ) {
3536 anyhow:: bail!( "simulated spawn failure" ) ;
3637 }
38+ self . ops
39+ . borrow_mut ( )
40+ . push ( format ! ( "spawn:{}" , target. pty_id) ) ;
3741 self . spawned . borrow_mut ( ) . push ( target. pty_id . clone ( ) ) ;
3842 self . spawn_dirs
3943 . borrow_mut ( )
@@ -45,6 +49,7 @@ impl Runner for FakeRunner {
4549 Ok ( ( ) )
4650 }
4751 fn reap_for_restart ( & self , pty_id : & str ) -> anyhow:: Result < ( ) > {
52+ self . ops . borrow_mut ( ) . push ( format ! ( "reap:{pty_id}" ) ) ;
4853 self . reaped . borrow_mut ( ) . push ( pty_id. to_string ( ) ) ;
4954 if self . fail_reap . as_deref ( ) == Some ( pty_id) {
5055 anyhow:: bail!( "reap broke" ) ;
@@ -93,6 +98,8 @@ fn up_once_launches_all_tasks_of_a_fresh_agent() {
9398 let mut launched = report. launched . clone ( ) ;
9499 launched. sort ( ) ;
95100 assert_eq ! ( launched, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
101+ assert ! ( report. restarted. is_empty( ) ) ;
102+ assert ! ( report. gc. is_empty( ) ) ;
96103 assert ! ( report. errors. is_empty( ) ) ;
97104 let dirs = runner. spawn_dirs . borrow ( ) ;
98105 assert ! ( dirs. iter( ) . all( |( _, d) | d. ends_with( "agents/hetz/demo" ) ) ) ;
@@ -168,7 +175,7 @@ fn up_once_collects_spawn_errors_without_aborting() {
168175}
169176
170177#[ test]
171- fn up_once_reaps_dead_nonkeep_then_respawns ( ) {
178+ fn up_once_reports_dead_active_reap_spawn_as_restart_not_gc_or_launch ( ) {
172179 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
173180 write ( tmp. path ( ) , "agents/hetz/demo/agent.toml" , AGENT ) ;
174181 let runner = FakeRunner {
@@ -183,7 +190,24 @@ fn up_once_reaps_dead_nonkeep_then_respawns() {
183190 runner. removed. borrow( ) . is_empty( ) ,
184191 "a crash restart is not final retirement cleanup"
185192 ) ;
186- assert_eq ! ( report. launched. len( ) , 2 ) ;
193+ assert ! ( report. launched. is_empty( ) ) ;
194+ let mut restarted = report. restarted . clone ( ) ;
195+ restarted. sort ( ) ;
196+ assert_eq ! ( restarted, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
197+ assert ! (
198+ report. gc. is_empty( ) ,
199+ "a successful restart must not be reported as final GC"
200+ ) ;
201+ assert_eq ! (
202+ runner. ops. borrow( ) . as_slice( ) ,
203+ [
204+ "reap:hetz.demo-claude" ,
205+ "spawn:hetz.demo-claude" ,
206+ "reap:hetz.demo.ding" ,
207+ "spawn:hetz.demo.ding" ,
208+ ] ,
209+ "report taxonomy must not change reap-before-spawn execution ordering"
210+ ) ;
187211}
188212
189213#[ test]
@@ -198,7 +222,9 @@ fn up_once_does_not_restart_a_task_when_diagnostic_reap_fails() {
198222
199223 let report = up_once ( tmp. path ( ) , "hetz" , & runner) . unwrap ( ) ;
200224
201- assert_eq ! ( report. launched, vec![ "hetz.demo.ding" ] ) ;
225+ assert ! ( report. launched. is_empty( ) ) ;
226+ assert_eq ! ( report. restarted, vec![ "hetz.demo.ding" ] ) ;
227+ assert ! ( report. gc. is_empty( ) ) ;
202228 assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "hetz.demo.ding" ] ) ;
203229 assert ! (
204230 report
@@ -208,6 +234,34 @@ fn up_once_does_not_restart_a_task_when_diagnostic_reap_fails() {
208234 ) ;
209235}
210236
237+ #[ test]
238+ fn up_once_does_not_report_a_failed_replacement_as_restarted ( ) {
239+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
240+ write ( tmp. path ( ) , "agents/hetz/demo/agent.toml" , AGENT ) ;
241+ let runner = FakeRunner {
242+ sessions : vec ! [ dead( "hetz.demo-claude" ) , live( "hetz.demo.ding" ) ] ,
243+ fail_spawn : Some ( "hetz.demo-claude" . into ( ) ) ,
244+ ..Default :: default ( )
245+ } ;
246+
247+ let report = up_once ( tmp. path ( ) , "hetz" , & runner) . unwrap ( ) ;
248+
249+ assert_eq ! (
250+ runner. reaped. borrow( ) . as_slice( ) ,
251+ [ "hetz.demo-claude" ] ,
252+ "the stale record is still reaped before the replacement attempt"
253+ ) ;
254+ assert ! ( report. launched. is_empty( ) ) ;
255+ assert ! ( report. restarted. is_empty( ) ) ;
256+ assert ! ( report. gc. is_empty( ) ) ;
257+ assert ! (
258+ report
259+ . errors
260+ . iter( )
261+ . any( |error| error == "spawn hetz.demo-claude: simulated spawn failure" )
262+ ) ;
263+ }
264+
211265#[ test]
212266fn up_once_finally_removes_dead_retired_tasks_without_restarting_them ( ) {
213267 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -229,6 +283,7 @@ fn up_once_finally_removes_dead_retired_tasks_without_restarting_them() {
229283 assert_eq ! ( removed, vec![ "hetz.demo-claude" , "hetz.demo.ding" ] ) ;
230284 assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
231285 assert ! ( report. launched. is_empty( ) ) ;
286+ assert ! ( report. restarted. is_empty( ) ) ;
232287 assert_eq ! ( report. gc. len( ) , 2 ) ;
233288}
234289
0 commit comments