1#[cfg(target_os = "windows")]
27use std::time::Duration;
28
29#[cfg(target_os = "windows")]
30use agentmux_common::ipc::{ClientKind, Command, Event};
31#[cfg(target_os = "windows")]
32use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
33
34#[cfg(target_os = "windows")]
35use tokio::net::windows::named_pipe::ClientOptions;
36
37#[cfg(target_os = "windows")]
38const OBSERVATION_WINDOW: Duration = Duration::from_secs(2);
39
40#[cfg(target_os = "windows")]
44pub async fn run_wrr_diag(launcher_exe_dir: &std::path::Path) -> Result<(), String> {
45 let version = env!("CARGO_PKG_VERSION");
46
47 let paths = crate::data_dir::resolve_paths(launcher_exe_dir, version)
48 .map_err(|e| format!("path resolution failed: {}", e))?;
49 let dir_hash = crate::hash::data_dir_hash16(&paths.data_dir, version);
50 let pipe_path = crate::ipc::pipe_name(&dir_hash);
51
52 println!("AgentMux diagnostic — connecting to {}", pipe_path);
53 println!("Data dir: {}", paths.data_dir.display());
54
55 let client = ClientOptions::new()
56 .open(&pipe_path)
57 .map_err(|e| format!(
58 "could not open pipe {}: {} (is AgentMux running for this data dir?)",
59 pipe_path, e
60 ))?;
61 println!("Connected. Registering as Tool client...\n");
62
63 let (read_half, mut write_half) = tokio::io::split(client);
64
65 let register = Command::Register {
66 kind: ClientKind::Tool,
67 pid: std::process::id(),
68 version: version.to_string(),
69 };
70 let mut buf = serde_json::to_vec(®ister)
71 .map_err(|e| format!("serialize Register: {}", e))?;
72 buf.push(b'\n');
73 write_half.write_all(&buf).await
74 .map_err(|e| format!("send Register: {}", e))?;
75 write_half.flush().await
76 .map_err(|e| format!("flush Register: {}", e))?;
77
78 let mut buf = serde_json::to_vec(&Command::GetSnapshot)
82 .map_err(|e| format!("serialize GetSnapshot: {}", e))?;
83 buf.push(b'\n');
84 write_half.write_all(&buf).await
85 .map_err(|e| format!("send GetSnapshot: {}", e))?;
86 write_half.flush().await
87 .map_err(|e| format!("flush GetSnapshot: {}", e))?;
88
89 let mut buf = serde_json::to_vec(&Command::GetEvents { since: 0 })
95 .map_err(|e| format!("serialize GetEvents: {}", e))?;
96 buf.push(b'\n');
97 write_half.write_all(&buf).await
98 .map_err(|e| format!("send GetEvents: {}", e))?;
99 write_half.flush().await
100 .map_err(|e| format!("flush GetEvents: {}", e))?;
101
102 let reader = BufReader::new(read_half);
107 let mut lines = reader.lines();
108 let mut events: Vec<Event> = Vec::new();
109 let deadline = tokio::time::Instant::now() + OBSERVATION_WINDOW;
110
111 loop {
112 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
113 if remaining.is_zero() {
114 break;
115 }
116 match tokio::time::timeout(remaining, lines.next_line()).await {
117 Ok(Ok(Some(line))) if line.trim().is_empty() => continue,
118 Ok(Ok(Some(line))) => {
119 match serde_json::from_str::<Event>(&line) {
120 Ok(evt) => events.push(evt),
121 Err(e) => eprintln!("[warn] could not parse event: {} ({})", e, line),
122 }
123 }
124 Ok(Ok(None)) => {
125 eprintln!("[warn] pipe closed before observation window elapsed");
126 break;
127 }
128 Ok(Err(e)) => {
129 return Err(format!("read error: {}", e));
130 }
131 Err(_) => break, }
133 }
134
135 let goodbye = match serde_json::to_vec(&Command::Goodbye) {
142 Ok(mut b) => { b.push(b'\n'); b }
143 Err(_) => Vec::new(),
144 };
145 if !goodbye.is_empty() {
146 let _ = write_half.write_all(&goodbye).await;
147 let _ = write_half.flush().await;
148 }
149
150 print_summary(&events);
151 Ok(())
152}
153
154#[cfg(not(target_os = "windows"))]
158pub async fn run_wrr_diag(_launcher_exe_dir: &std::path::Path) -> Result<(), String> {
159 Err("--diag wrr is Windows-only (Phase 7 will add Unix domain socket parity)".to_string())
160}
161
162#[cfg(target_os = "windows")]
176pub async fn run_srv_diag(launcher_exe_dir: &std::path::Path) -> Result<(), String> {
177 let version = env!("CARGO_PKG_VERSION");
178
179 let paths = crate::data_dir::resolve_paths(launcher_exe_dir, version)
180 .map_err(|e| format!("path resolution failed: {}", e))?;
181 let dir_hash = crate::hash::data_dir_hash16(&paths.data_dir, version);
182 let pipe_path = crate::ipc::srv_pipe_name(&dir_hash);
183
184 println!("AgentMux srv diagnostic — connecting to {}", pipe_path);
185 println!("Data dir: {}", paths.data_dir.display());
186
187 let client = ClientOptions::new().open(&pipe_path).map_err(|e| {
188 format!(
189 "could not open srv pipe {}: {} (is AgentMux running for this data dir? \
190 srv may not be bound in `task dev` mode)",
191 pipe_path, e
192 )
193 })?;
194 println!("Connected. Registering as Tool client...\n");
195
196 let (read_half, mut write_half) = tokio::io::split(client);
197
198 let register = Command::Register {
199 kind: ClientKind::Tool,
200 pid: std::process::id(),
201 version: version.to_string(),
202 };
203 let mut buf = serde_json::to_vec(®ister)
204 .map_err(|e| format!("serialize Register: {}", e))?;
205 buf.push(b'\n');
206 write_half
207 .write_all(&buf)
208 .await
209 .map_err(|e| format!("send Register: {}", e))?;
210 write_half
211 .flush()
212 .await
213 .map_err(|e| format!("flush Register: {}", e))?;
214
215 let mut buf = serde_json::to_vec(&Command::GetSrvSnapshot)
217 .map_err(|e| format!("serialize GetSrvSnapshot: {}", e))?;
218 buf.push(b'\n');
219 write_half
220 .write_all(&buf)
221 .await
222 .map_err(|e| format!("send GetSrvSnapshot: {}", e))?;
223 write_half
224 .flush()
225 .await
226 .map_err(|e| format!("flush GetSrvSnapshot: {}", e))?;
227
228 let mut buf = serde_json::to_vec(&Command::GetEvents { since: 0 })
234 .map_err(|e| format!("serialize GetEvents: {}", e))?;
235 buf.push(b'\n');
236 write_half
237 .write_all(&buf)
238 .await
239 .map_err(|e| format!("send GetEvents: {}", e))?;
240 write_half
241 .flush()
242 .await
243 .map_err(|e| format!("flush GetEvents: {}", e))?;
244
245 let reader = BufReader::new(read_half);
246 let mut lines = reader.lines();
247 let mut events: Vec<Event> = Vec::new();
248 let deadline = tokio::time::Instant::now() + OBSERVATION_WINDOW;
249
250 loop {
251 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
252 if remaining.is_zero() {
253 break;
254 }
255 match tokio::time::timeout(remaining, lines.next_line()).await {
256 Ok(Ok(Some(line))) if line.trim().is_empty() => continue,
257 Ok(Ok(Some(line))) => match serde_json::from_str::<Event>(&line) {
258 Ok(evt) => events.push(evt),
259 Err(e) => eprintln!("[warn] could not parse event: {} ({})", e, line),
260 },
261 Ok(Ok(None)) => {
262 eprintln!("[warn] srv pipe closed before observation window elapsed");
263 break;
264 }
265 Ok(Err(e)) => {
266 return Err(format!("read error: {}", e));
267 }
268 Err(_) => break,
269 }
270 }
271
272 let goodbye = match serde_json::to_vec(&Command::Goodbye) {
275 Ok(mut b) => {
276 b.push(b'\n');
277 b
278 }
279 Err(_) => Vec::new(),
280 };
281 if !goodbye.is_empty() {
282 let _ = write_half.write_all(&goodbye).await;
283 let _ = write_half.flush().await;
284 }
285
286 print_srv_summary(&events);
287 Ok(())
288}
289
290#[cfg(not(target_os = "windows"))]
291pub async fn run_srv_diag(_launcher_exe_dir: &std::path::Path) -> Result<(), String> {
292 Err("--diag srv is Windows-only (Phase 7 will add Unix domain socket parity)".to_string())
293}
294
295#[cfg(target_os = "windows")]
296fn print_srv_summary(events: &[Event]) {
297 let snapshot: Vec<&Event> = events
298 .iter()
299 .filter(|e| matches!(e, Event::SrvSnapshot { .. }))
300 .collect();
301 let replay: Vec<&Event> = events
302 .iter()
303 .filter(|e| matches!(e, Event::EventList { .. }))
304 .collect();
305 let stream: Vec<&Event> = events
306 .iter()
307 .filter(|e| !matches!(e, Event::SrvSnapshot { .. } | Event::EventList { .. }))
308 .collect();
309
310 if let Some(Event::SrvSnapshot {
311 version,
312 lifecycle,
313 workspaces,
314 tabs,
315 active_tabs,
316 blocks,
317 }) = snapshot.last().copied()
318 {
319 println!("=== SrvSnapshot (event_version={}) ===", version);
320 println!("Lifecycle: {:?}", lifecycle);
321 println!();
322 println!("Workspaces ({}):", workspaces.len());
323 if workspaces.is_empty() {
324 println!(" (none)");
325 } else {
326 for (id, name) in workspaces {
327 let active = active_tabs
328 .iter()
329 .find(|(ws, _)| ws == id)
330 .map(|(_, t)| t.as_str())
331 .unwrap_or("—");
332 let tab_count = tabs.iter().filter(|(_, ws, _)| ws == id).count();
333 println!(
334 " {:36} name={:<20} tabs={} active={}",
335 id, name, tab_count, active
336 );
337 }
338 }
339 println!();
340 println!("Tabs ({}):", tabs.len());
341 if tabs.is_empty() {
342 println!(" (none)");
343 } else {
344 for (tab_id, ws_id, name) in tabs {
345 let block_count = blocks.iter().filter(|(_, t)| t == tab_id).count();
346 println!(
347 " {:36} ws={:36} name={:<16} blocks={}",
348 tab_id, ws_id, name, block_count
349 );
350 }
351 }
352 println!();
353 println!("Blocks ({}):", blocks.len());
354 if blocks.is_empty() {
355 println!(" (none)");
356 } else {
357 for (block_id, tab_id) in blocks {
358 println!(" {:36} tab={}", block_id, tab_id);
359 }
360 }
361 println!();
362 } else {
363 println!("(SrvSnapshot not received — srv pipe may not be bound, or older srv build)");
364 println!();
365 }
366
367 if let Some(Event::EventList {
368 events: replay_events,
369 version,
370 }) = replay.last().copied()
371 {
372 println!(
373 "=== EventList replay (event_version={}, {} event(s)) ===",
374 version,
375 replay_events.len()
376 );
377 if replay_events.is_empty() {
378 println!("(empty — srv ring + event log contained no events)");
379 } else {
380 let to_show = replay_events.iter().rev().take(20).collect::<Vec<_>>();
382 let n = replay_events.len();
383 let skipped = n.saturating_sub(to_show.len());
384 if skipped > 0 {
385 println!("(showing last 20 of {})", n);
386 }
387 for (i, evt) in to_show.iter().rev().enumerate() {
388 println!(" [{}] {}", skipped + i, format_srv_event(evt));
389 }
390 }
391 println!();
392 }
393
394 use std::collections::BTreeMap;
396 let mut saga_counts: BTreeMap<&'static str, u32> = BTreeMap::new();
397 let stream_or_replay = events
398 .iter()
399 .filter(|e| !matches!(e, Event::SrvSnapshot { .. }))
400 .flat_map(|e| match e {
401 Event::EventList { events: inner, .. } => inner.iter().collect::<Vec<_>>(),
402 other => vec![other],
403 });
404 for evt in stream_or_replay {
405 match evt {
406 Event::SagaStarted { .. } => *saga_counts.entry("SagaStarted").or_insert(0) += 1,
407 Event::SagaCompleted { .. } => {
408 *saga_counts.entry("SagaCompleted").or_insert(0) += 1
409 }
410 Event::SagaFailed { .. } => *saga_counts.entry("SagaFailed").or_insert(0) += 1,
411 _ => {}
412 }
413 }
414 if !saga_counts.is_empty() {
415 println!("=== Saga lifecycle (across snapshot + replay + stream) ===");
416 for (kind, count) in &saga_counts {
417 println!(" {:>4}× {}", count, kind);
418 }
419 println!();
420 }
421
422 println!(
423 "=== Live stream observed in {}s ===",
424 OBSERVATION_WINDOW.as_secs()
425 );
426 if stream.is_empty() {
427 println!("(no events — srv reducer is idle.)");
428 return;
429 }
430 let mut counts: BTreeMap<&'static str, u32> = BTreeMap::new();
431 for evt in &stream {
432 *counts.entry(srv_event_kind_name(evt)).or_insert(0) += 1;
433 }
434 println!("By kind:");
435 for (kind, count) in &counts {
436 println!(" {:>4}× {}", count, kind);
437 }
438 println!();
439 println!("Full stream (oldest first):");
440 for (i, evt) in stream.iter().enumerate() {
441 println!(" [{}] {}", i, format_srv_event(evt));
442 }
443}
444
445#[cfg(target_os = "windows")]
446fn srv_event_kind_name(e: &Event) -> &'static str {
447 match e {
448 Event::Registered { .. } => "Registered",
449 Event::ProcessSpawned { .. } => "ProcessSpawned",
450 Event::ProcessExited { .. } => "ProcessExited",
451 Event::LifecyclePhaseChanged { .. } => "LifecyclePhaseChanged",
452 Event::SagaStarted { .. } => "SagaStarted",
453 Event::SagaCompleted { .. } => "SagaCompleted",
454 Event::SagaFailed { .. } => "SagaFailed",
455 Event::WorkspaceCreated { .. } => "WorkspaceCreated",
456 Event::WorkspaceDeleted { .. } => "WorkspaceDeleted",
457 Event::WorkspaceRenamed { .. } => "WorkspaceRenamed",
458 Event::WorkspaceMetaUpdated { .. } => "WorkspaceMetaUpdated",
459 Event::TabCreated { .. } => "TabCreated",
460 Event::TabDeleted { .. } => "TabDeleted",
461 Event::TabRenamed { .. } => "TabRenamed",
462 Event::TabReordered { .. } => "TabReordered",
463 Event::TabsReorderedBulk { .. } => "TabsReorderedBulk",
464 Event::TabMoved { .. } => "TabMoved",
465 Event::TabMetaUpdated { .. } => "TabMetaUpdated",
466 Event::ActiveTabChanged { .. } => "ActiveTabChanged",
467 Event::BlockCreated { .. } => "BlockCreated",
468 Event::BlockDeleted { .. } => "BlockDeleted",
469 Event::BlockMoved { .. } => "BlockMoved",
470 Event::BlockMetaUpdated { .. } => "BlockMetaUpdated",
471 Event::SrvWindowOpened { .. } => "SrvWindowOpened",
472 Event::SrvWindowClosed { .. } => "SrvWindowClosed",
473 Event::SrvWindowWorkspaceChanged { .. } => "SrvWindowWorkspaceChanged",
474 Event::SrvSnapshot { .. } => "SrvSnapshot",
475 Event::EventList { .. } => "EventList",
476 Event::Error { .. } => "Error",
477 _ => "Other",
478 }
479}
480
481#[cfg(target_os = "windows")]
482fn format_srv_event(e: &Event) -> String {
483 match e {
484 Event::SagaStarted { saga_id, name, version } => {
485 format!("v={:>3} SagaStarted id={} name={}", version, saga_id, name)
486 }
487 Event::SagaCompleted { saga_id, version } => {
488 format!("v={:>3} SagaCompleted id={}", version, saga_id)
489 }
490 Event::SagaFailed { saga_id, reason, version } => {
491 format!("v={:>3} SagaFailed id={} reason={}", version, saga_id, reason)
492 }
493 Event::WorkspaceCreated { workspace_id, name, version } => {
494 format!("v={:>3} WorkspaceCreated id={} name={}", version, workspace_id, name)
495 }
496 Event::WorkspaceDeleted { workspace_id, version, .. } => {
497 format!("v={:>3} WorkspaceDeleted id={}", version, workspace_id)
498 }
499 Event::TabCreated { workspace_id, tab_id, name, version } => {
500 format!("v={:>3} TabCreated tab={} ws={} name={}", version, tab_id, workspace_id, name)
501 }
502 Event::TabMoved {
503 tab_id,
504 src_workspace_id,
505 dst_workspace_id,
506 dst_index,
507 version,
508 ..
509 } => format!(
510 "v={:>3} TabMoved tab={} {} → {} idx={}",
511 version, tab_id, src_workspace_id, dst_workspace_id, dst_index
512 ),
513 Event::BlockMoved { block_id, src_tab_id, dst_tab_id, dst_index, version } => format!(
514 "v={:>3} BlockMoved blk={} {} → {} idx={}",
515 version, block_id, src_tab_id, dst_tab_id, dst_index
516 ),
517 Event::Error { code, message, version, .. } => {
518 format!("v={:>3} Error code={:?} msg={}", version, code, message)
519 }
520 other => serde_json::to_string(other).unwrap_or_else(|_| format!("{:?}", other)),
521 }
522}
523
524#[cfg(target_os = "windows")]
525fn print_summary(events: &[Event]) {
526 let snapshot: Vec<&Event> = events
531 .iter()
532 .filter(|e| matches!(e, Event::Snapshot { .. }))
533 .collect();
534 let replay: Vec<&Event> = events
535 .iter()
536 .filter(|e| matches!(e, Event::EventList { .. }))
537 .collect();
538 let stream: Vec<&Event> = events
539 .iter()
540 .filter(|e| {
541 !matches!(
542 e,
543 Event::Snapshot { .. } | Event::EventList { .. }
544 )
545 })
546 .collect();
547
548 if let Some(Event::Snapshot {
556 version,
557 lifecycle,
558 windows,
559 pool,
560 instance_registry,
561 backend_window_ids,
562 monitors,
563 }) = snapshot.last().copied()
564 {
565 println!("=== Snapshot (event_version={}) ===", version);
566 println!("Lifecycle: {:?}", lifecycle);
567 println!("Monitors: {} ({:?})", monitors.len(), monitors);
568 println!();
569 println!("Windows ({}):", windows.len());
570 if windows.is_empty() {
571 println!(" (none)");
572 } else {
573 for w in windows {
574 let inst = instance_registry
575 .iter()
576 .find(|(l, _)| l == &w.label)
577 .map(|(_, n)| format!("#{}", n))
578 .unwrap_or_else(|| "—".to_string());
579 let backend = backend_window_ids
580 .iter()
581 .find(|(l, _)| l == &w.label)
582 .map(|(_, b)| b.as_str())
583 .unwrap_or("—");
584 println!(
585 " {:>4} {:30} kind={:?} hwnd={:?} visible={} iconic={} fg_seen={} backend={}",
586 inst, w.label, w.kind, w.hwnd, w.visible, w.iconic,
587 w.foregrounded_since_open, backend
588 );
589 }
590 }
591 println!();
592 println!("Pool ({}): {:?}", pool.len(), pool);
593 println!();
594 } else {
595 println!("(snapshot not received — server may be older than D.1)");
596 println!();
597 }
598
599 if let Some(Event::EventList { events: replay_events, version }) = replay.last().copied() {
604 println!("=== EventList replay (event_version={}, {} event(s)) ===", version, replay_events.len());
605 if replay_events.is_empty() {
606 println!("(empty — launcher's in-memory ring contained no events with version > 0)");
607 } else {
608 for (i, evt) in replay_events.iter().enumerate() {
609 println!(" [{}] {}", i, format_event(evt));
610 }
611 }
612 println!();
613 }
614
615 println!("=== Live stream observed in {}s ===", OBSERVATION_WINDOW.as_secs());
616 if stream.is_empty() {
617 println!("(no events — instance is idle.)");
618 return;
619 }
620 use std::collections::BTreeMap;
621 let mut counts: BTreeMap<&'static str, u32> = BTreeMap::new();
622 for evt in &stream {
623 *counts.entry(event_kind_name(evt)).or_insert(0) += 1;
624 }
625 println!("By kind:");
626 for (kind, count) in &counts {
627 println!(" {:>4}× {}", count, kind);
628 }
629 println!();
630 println!("Full stream (oldest first):");
631 for (i, evt) in stream.iter().enumerate() {
632 println!(" [{}] {}", i, format_event(evt));
633 }
634}
635
636#[cfg(target_os = "windows")]
637fn event_kind_name(e: &Event) -> &'static str {
638 match e {
639 Event::Registered { .. } => "Registered",
640 Event::Pong { .. } => "Pong",
641 Event::ProcessSpawned { .. } => "ProcessSpawned",
642 Event::ProcessExited { .. } => "ProcessExited",
643 Event::LifecyclePhaseChanged { .. } => "LifecyclePhaseChanged",
644 Event::WindowOpened { .. } => "WindowOpened",
645 Event::WindowClosed { .. } => "WindowClosed",
646 Event::WindowInstanceAssigned { .. } => "WindowInstanceAssigned",
647 Event::WindowInstanceReleased { .. } => "WindowInstanceReleased",
648 Event::PoolWindowAdded { .. } => "PoolWindowAdded",
649 Event::PoolWindowRemoved { .. } => "PoolWindowRemoved",
650 Event::PoolWindowPromoted { .. } => "PoolWindowPromoted",
651 Event::BackendWindowIdRegistered { .. } => "BackendWindowIdRegistered",
652 Event::BackendWindowIdUnregistered { .. } => "BackendWindowIdUnregistered",
653 Event::DriftDetected { .. } => "DriftDetected",
654 Event::HwndDriftDetected { .. } => "HwndDriftDetected",
655 Event::CorrectiveWindowMove { .. } => "CorrectiveWindowMove",
656 Event::HostShouldQuit { .. } => "HostShouldQuit",
657 Event::Snapshot { .. } => "Snapshot",
658 Event::EventList { .. } => "EventList",
659 Event::SagaStarted { .. } => "SagaStarted",
660 Event::SagaCompleted { .. } => "SagaCompleted",
661 Event::SagaFailed { .. } => "SagaFailed",
662 _ => "Other",
663 }
664}
665
666#[cfg(target_os = "windows")]
667fn format_event(e: &Event) -> String {
668 match e {
672 Event::WindowOpened { label, kind, parent_label, version } => format!(
673 "v={:>3} WindowOpened label={} kind={:?} parent={:?}",
674 version, label, kind, parent_label
675 ),
676 Event::WindowClosed { label, version, crash_detected } => format!(
677 "v={:>3} WindowClosed label={} crash_detected={}", version, label, crash_detected
678 ),
679 Event::WindowInstanceAssigned { label, num, version } => format!(
680 "v={:>3} InstanceAssigned label={} num={}", version, label, num
681 ),
682 Event::HwndDriftDetected { kind, label, hwnd, severity, detail, version } => format!(
683 "v={:>3} HwndDriftDetected kind={:?} label={:?} hwnd={:?} severity={:?} — {}",
684 version, kind, label, hwnd, severity, detail
685 ),
686 Event::HostShouldQuit { version } => format!(
687 "v={:>3} HostShouldQuit", version
688 ),
689 other => serde_json::to_string(other).unwrap_or_else(|_| format!("{:?}", other)),
690 }
691}
692
693#[cfg(target_os = "windows")]
712pub async fn run_sagas_diag(launcher_exe_dir: &std::path::Path) -> Result<(), String> {
713 run_sagas_diag_impl(launcher_exe_dir).await
714}
715
716#[cfg(not(target_os = "windows"))]
717pub async fn run_sagas_diag(launcher_exe_dir: &std::path::Path) -> Result<(), String> {
718 run_sagas_diag_impl(launcher_exe_dir).await
725}
726
727async fn run_sagas_diag_impl(launcher_exe_dir: &std::path::Path) -> Result<(), String> {
728 let _ = launcher_exe_dir;
736 println!("AgentMux launcher saga diagnostic");
737 println!();
738 println!("The durable saga log was removed (Pillar 1 Step 6, 2026-07-16).");
739 println!("Launcher sagas now live in an in-memory registry inside the running");
740 println!("launcher; nothing is persisted for offline inspection.");
741 println!();
742 println!("To inspect saga activity, read the launcher log's [saga] lines:");
743 println!(" muxlog launcher | grep saga");
744 println!();
745 println!("Historical sagas.db files from older versions are deleted at first");
746 println!("startup of this version; any remaining copies are stale.");
747 Ok(())
748}
749
750#[cfg(test)]
751mod sagas_diag_tests {
752 use crate::saga::{LauncherSagaLog, PipeTarget};
753 use agentmux_common::ipc::{Command, Event};
754
755 #[test]
761 fn sagas_diag_fixture_log_has_expected_summary_fields() {
762 let log = LauncherSagaLog::open_in_memory().unwrap();
763
764 log.start_saga(
766 1,
767 "window_cleanup_cascade",
768 &serde_json::json!({"label": "win-1"}),
769 )
770 .unwrap();
771 log.start_step(
772 1,
773 0,
774 "issue_cmd_host_reap_panes",
775 PipeTarget::Host,
776 &Command::Ping { nonce: 1 },
777 )
778 .unwrap();
779 log.finish_step(1, 0, &Event::Pong { nonce: 1, version: 1 })
780 .unwrap();
781 log.terminate_saga(1, crate::saga::log::SagaOutcome::Completed)
782 .unwrap();
783
784 log.start_saga(
787 2,
788 "window_cleanup_cascade",
789 &serde_json::json!({"label": "win-3"}),
790 )
791 .unwrap();
792 log.start_step(
793 2,
794 0,
795 "issue_cmd_host_reap_panes",
796 PipeTarget::Host,
797 &Command::Ping { nonce: 2 },
798 )
799 .unwrap();
800 log.finish_step(2, 0, &Event::Pong { nonce: 2, version: 1 })
801 .unwrap();
802 log.start_step(
803 2,
804 1,
805 "issue_cmd_host_drain_pool",
806 PipeTarget::Host,
807 &Command::Ping { nonce: 3 },
808 )
809 .unwrap();
810 log.mark_failed_compensation(2, "launcher restarted while saga in state 'running'")
813 .unwrap();
814
815 let snapshot = log.snapshot_recent(50).unwrap();
816 assert_eq!(snapshot.len(), 2);
817
818 let s2 = &snapshot[0];
820 let s1 = &snapshot[1];
821 assert_eq!(s1.saga_id, 1);
822 assert_eq!(s1.state, "completed");
823 assert_eq!(s1.step_count, 1);
824
825 assert_eq!(s2.saga_id, 2);
826 assert_eq!(s2.state, "failed_compensation");
827 assert_eq!(s2.step_count, 1);
831 assert!(s2
832 .failure_reason
833 .as_deref()
834 .unwrap_or("")
835 .contains("launcher restarted"));
836
837 let unresolved = log.unresolved_sagas().unwrap();
845 assert!(unresolved.iter().all(|u| u.saga_id != 2));
846 }
847}