@@ -288,6 +288,8 @@ mod tests {
288288 use super :: * ;
289289 use shepherd_core:: yolo:: rules:: RuleSet ;
290290
291+ // ---- ServerInfo serde tests ----
292+
291293 #[ test]
292294 fn server_info_serde_roundtrip ( ) {
293295 let info = ServerInfo {
@@ -302,6 +304,17 @@ mod tests {
302304 assert_eq ! ( parsed. started_at, "2026-03-19T10:00:00Z" ) ;
303305 }
304306
307+ #[ test]
308+ fn server_info_deserializes_from_json_string ( ) {
309+ let json = r#"{"pid":42,"port":3000,"started_at":"2026-01-01T00:00:00Z"}"# ;
310+ let info: ServerInfo = serde_json:: from_str ( json) . unwrap ( ) ;
311+ assert_eq ! ( info. pid, 42 ) ;
312+ assert_eq ! ( info. port, 3000 ) ;
313+ assert_eq ! ( info. started_at, "2026-01-01T00:00:00Z" ) ;
314+ }
315+
316+ // ---- ServerInfo write_to / read_from tests ----
317+
305318 #[ test]
306319 fn server_info_write_to_and_read_from ( ) {
307320 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -311,9 +324,7 @@ mod tests {
311324 port : 8080 ,
312325 started_at : "2026-03-19T12:00:00Z" . into ( ) ,
313326 } ;
314- // Use the actual write_to method (creates parent dirs)
315327 info. write_to ( & path) . unwrap ( ) ;
316- // Use the actual read_from method
317328 let parsed = ServerInfo :: read_from ( & path) . unwrap ( ) ;
318329 assert_eq ! ( parsed. pid, 999 ) ;
319330 assert_eq ! ( parsed. port, 8080 ) ;
@@ -344,19 +355,18 @@ mod tests {
344355 }
345356
346357 #[ test]
347- fn server_info_path_ends_with_server_json ( ) {
348- let path = ServerInfo :: path ( ) ;
349- assert ! ( path. ends_with( "server.json" ) ) ;
350- assert ! ( path. to_string_lossy( ) . contains( ".shepherd" ) ) ;
351- }
352-
353- #[ test]
354- fn server_info_deserializes_from_json_string ( ) {
355- let json = r#"{"pid":42,"port":3000,"started_at":"2026-01-01T00:00:00Z"}"# ;
356- let info: ServerInfo = serde_json:: from_str ( json) . unwrap ( ) ;
357- assert_eq ! ( info. pid, 42 ) ;
358- assert_eq ! ( info. port, 3000 ) ;
359- assert_eq ! ( info. started_at, "2026-01-01T00:00:00Z" ) ;
358+ fn server_info_write_to_creates_nested_dirs ( ) {
359+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
360+ let path = tmp. path ( ) . join ( "a/b/c/server.json" ) ;
361+ let info = ServerInfo {
362+ pid : 1 ,
363+ port : 3000 ,
364+ started_at : "2026-03-19T00:00:00Z" . into ( ) ,
365+ } ;
366+ info. write_to ( & path) . unwrap ( ) ;
367+ assert ! ( path. exists( ) ) ;
368+ let parsed = ServerInfo :: read_from ( & path) . unwrap ( ) ;
369+ assert_eq ! ( parsed. port, 3000 ) ;
360370 }
361371
362372 #[ test]
@@ -383,34 +393,32 @@ mod tests {
383393 assert ! ( ServerInfo :: read_from( & path) . is_none( ) ) ;
384394 }
385395
396+ // ---- ServerInfo path() tests ----
397+
386398 #[ test]
387- fn server_info_write_to_creates_nested_dirs ( ) {
388- let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
389- let path = tmp. path ( ) . join ( "a/b/c/server.json" ) ;
390- let info = ServerInfo {
391- pid : 1 ,
392- port : 3000 ,
393- started_at : "2026-03-19T00:00:00Z" . into ( ) ,
394- } ;
395- info. write_to ( & path) . unwrap ( ) ;
396- assert ! ( path. exists( ) ) ;
397- let parsed = ServerInfo :: read_from ( & path) . unwrap ( ) ;
398- assert_eq ! ( parsed. port, 3000 ) ;
399+ fn server_info_path_ends_with_server_json ( ) {
400+ let path = ServerInfo :: path ( ) ;
401+ assert ! ( path. ends_with( "server.json" ) ) ;
399402 }
400403
404+ #[ test]
405+ fn server_info_path_contains_shepherd_dir ( ) {
406+ let path = ServerInfo :: path ( ) ;
407+ assert ! ( path. to_string_lossy( ) . contains( ".shepherd" ) ) ;
408+ }
409+
410+ // ---- ServerInfo remove() test ----
411+
401412 #[ test]
402413 fn server_info_remove_is_best_effort ( ) {
403- // Calling remove() when the file doesn't exist should not panic
404- // We can't easily test this without touching the global path,
405- // but we can verify it doesn't panic by calling it.
406- // (This at least covers the remove() method body.)
414+ // Calling remove() when the file doesn't exist should not panic.
407415 ServerInfo :: remove ( ) ;
408416 }
409417
418+ // ---- ServerInfo write/read global path tests ----
419+
410420 #[ test]
411421 fn server_info_write_and_read_global_path ( ) {
412- // Exercise write() and read() through the global path delegates.
413- // These call write_to(path()) and read_from(path()) internally.
414422 let info = ServerInfo {
415423 pid : std:: process:: id ( ) ,
416424 port : 9999 ,
@@ -425,6 +433,8 @@ mod tests {
425433 assert ! ( ServerInfo :: read( ) . is_none( ) ) ;
426434 }
427435
436+ // ---- pty_output_to_event tests ----
437+
428438 #[ test]
429439 fn pty_output_to_event_converts_correctly ( ) {
430440 let output = PtyOutput {
@@ -451,15 +461,32 @@ mod tests {
451461 match event {
452462 ServerEvent :: TerminalOutput { task_id, data } => {
453463 assert_eq ! ( task_id, 7 ) ;
454- // String::from_utf8_lossy replaces invalid bytes with U+FFFD
455464 assert ! ( data. contains( "Hi" ) ) ;
456465 }
457466 other => panic ! ( "Expected TerminalOutput, got {:?}" , other) ,
458467 }
459468 }
460469
470+ #[ test]
471+ fn pty_output_to_event_empty_data ( ) {
472+ let output = PtyOutput {
473+ task_id : 0 ,
474+ data : vec ! [ ] ,
475+ } ;
476+ let event = super :: pty_output_to_event ( & output) ;
477+ match event {
478+ ServerEvent :: TerminalOutput { task_id, data } => {
479+ assert_eq ! ( task_id, 0 ) ;
480+ assert_eq ! ( data, "" ) ;
481+ }
482+ other => panic ! ( "Expected TerminalOutput, got {:?}" , other) ,
483+ }
484+ }
485+
486+ // ---- forward_pty_to_dispatcher tests ----
487+
461488 #[ tokio:: test]
462- async fn forward_pty_to_dispatcher_handles_output ( ) {
489+ async fn forward_pty_to_dispatcher_no_panic_for_unknown_task ( ) {
463490 use shepherd_core:: coordination:: LockManager ;
464491 use shepherd_core:: dispatch:: TaskDispatcher ;
465492
@@ -480,18 +507,19 @@ mod tests {
480507 task_id : 99 ,
481508 data : b"some agent output" . to_vec ( ) ,
482509 } ;
483- // Should not panic — no monitor for task 99, returns None internally
510+ // Should not panic -- no monitor for task 99, returns None internally
484511 super :: forward_pty_to_dispatcher ( & dispatcher, & output) . await ;
485512 }
486513
514+ // ---- replay recording test ----
515+
487516 #[ tokio:: test]
488517 async fn pty_output_records_to_replay ( ) {
489518 use shepherd_core:: db:: open_memory;
490519 use shepherd_core:: replay;
491520
492521 let conn = open_memory ( ) . unwrap ( ) ;
493522
494- // Record an event
495523 let id = replay:: record_event (
496524 & conn,
497525 1 , // task_id
@@ -504,10 +532,30 @@ mod tests {
504532 . unwrap ( ) ;
505533 assert ! ( id > 0 ) ;
506534
507- // Verify it shows in timeline
508535 let events = replay:: get_timeline ( & conn, 1 ) . unwrap ( ) ;
509536 assert_eq ! ( events. len( ) , 1 ) ;
510537 assert_eq ! ( events[ 0 ] . content, "hello world" ) ;
511538 assert_eq ! ( events[ 0 ] . event_type, replay:: EventType :: Output ) ;
512539 }
540+
541+ // ---- ServerInfo write_to produces pretty JSON ----
542+
543+ #[ test]
544+ fn server_info_write_to_produces_pretty_json ( ) {
545+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
546+ let path = tmp. path ( ) . join ( "server.json" ) ;
547+ let info = ServerInfo {
548+ pid : 100 ,
549+ port : 5000 ,
550+ started_at : "2026-03-31T00:00:00Z" . into ( ) ,
551+ } ;
552+ info. write_to ( & path) . unwrap ( ) ;
553+ let content = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
554+ // Pretty JSON should contain newlines and indentation
555+ assert ! ( content. contains( '\n' ) ) ;
556+ assert ! ( content. contains( " " ) ) ;
557+ // Verify it still parses correctly
558+ let parsed: ServerInfo = serde_json:: from_str ( & content) . unwrap ( ) ;
559+ assert_eq ! ( parsed. pid, 100 ) ;
560+ }
513561}
0 commit comments