#![allow(clippy::field_reassign_with_default)] use std::fs; use std::io::Cursor; use serde_json::json; use super::*; #[test] fn reads_standard_dap_content_length_frame_from_generic_client() { let payload = json!({ "seq": 7, "type": "request", "command": "threads" }) .to_string(); let frame = format!("Content-Length: {}\r\n\r\n{payload}", payload.len()); let mut reader = Cursor::new(frame.into_bytes()); let message = read_message(&mut reader) .unwrap() .expect("DAP frame should contain a request"); assert_eq!(message["seq"], 7); assert_eq!(message["type"], "request"); assert_eq!(message["command"], "threads"); } #[test] fn initialize_capabilities_use_standard_dap_flags() { let capabilities = initialize_capabilities(); let capabilities = capabilities .as_object() .expect("initialize capabilities should be an object"); assert_eq!( capabilities.get("supportsConfigurationDoneRequest"), Some(&json!(true)) ); assert_eq!(capabilities.get("supportsRestartFrame"), Some(&json!(true))); assert_eq!(capabilities.get("supportsStepBack"), Some(&json!(false))); assert!(capabilities .keys() .all(|key| key.starts_with("supports") && !key.contains("clusterflux"))); } #[test] fn runtime_backend_defaults_to_local_services_and_requires_explicit_demo() { assert_eq!( runtime_backend_from_launch_arg(None).unwrap(), RuntimeBackend::LocalServices ); assert_eq!( runtime_backend_from_launch_arg(Some("simulated")).unwrap(), RuntimeBackend::Simulated ); assert_eq!( runtime_backend_from_launch_arg(Some("demo")).unwrap(), RuntimeBackend::Simulated ); assert!(runtime_backend_from_launch_arg(Some("unknown")).is_err()); } #[test] fn live_dap_uses_matching_stored_cli_session_and_omits_claimed_scope() { let temp = tempfile::tempdir().unwrap(); let project = temp.path().join("examples/demo"); fs::create_dir_all(temp.path().join(".clusterflux")).unwrap(); fs::create_dir_all(&project).unwrap(); fs::write( temp.path().join(".clusterflux/session.json"), serde_json::to_vec(&json!({ "coordinator": "https://hosted.example.test", "tenant": "tenant-session", "project": "project-session", "user": "user-session", "session_secret": "dap-session-secret" })) .unwrap(), ) .unwrap(); let mut state = AdapterState::default(); state .configure_launch( project.to_string_lossy().into_owned(), "build".to_owned(), RuntimeBackend::LiveServices, Some("https://hosted.example.test/".to_owned()), None, ) .unwrap(); assert_eq!(state.tenant.as_str(), "tenant-session"); assert_eq!(state.project_id.as_str(), "project-session"); assert_eq!(state.actor_user.as_str(), "user-session"); assert_eq!( state.client_session_secret.as_deref(), Some("dap-session-secret") ); let request = client_user_request( &state, json!({ "type": "debug_attach", "tenant": "forged-tenant", "project": "forged-project", "actor_user": "forged-user", "process": "vp" }), ); assert_eq!(request["type"], "authenticated"); assert_eq!(request["session_secret"], "dap-session-secret"); assert_eq!(request["request"]["type"], "debug_attach"); assert!(request["request"].get("tenant").is_none()); assert!(request["request"].get("project").is_none()); assert!(request["request"].get("actor_user").is_none()); let mismatch = state.configure_launch( project.to_string_lossy().into_owned(), "build".to_owned(), RuntimeBackend::LiveServices, Some("https://different.example.test".to_owned()), None, ); assert!(mismatch .unwrap_err() .to_string() .contains("does not match the authenticated CLI session")); } #[test] fn compatible_restart_frame_does_not_require_whole_process_restart() { let request = json!({ "command": "restartFrame", "arguments": { "frameId": 1, "sourceEdit": { "compatibility": "compatible" } } }); assert!(!restart_requires_whole_process(&request)); } #[test] fn incompatible_source_edit_requires_whole_process_restart() { for arguments in [ json!({ "sourceCompatibility": "incompatible" }), json!({ "sourceEdit": { "compatibility": "whole-process" } }), json!({ "requiresWholeProcessRestart": true }), ] { let request = json!({ "command": "restartFrame", "arguments": arguments, }); assert!(restart_requires_whole_process(&request)); } } #[test] fn task_restart_response_preserves_coordinator_boundary_decision() { let record = parse_task_restart_response(json!({ "type": "task_restart", "accepted": false, "clean_boundary_available": false, "requires_whole_process_restart": true, "active_task": false, "completed_event_observed": true, "message": "selected task has only terminal event metadata; restart requires a captured checkpoint boundary" })) .unwrap(); assert!(!record.accepted); assert!(!record.clean_boundary_available); assert!(record.requires_whole_process_restart); assert!(!record.active_task); assert!(record.completed_event_observed); assert!(record.message.contains("checkpoint boundary")); } #[test] fn nonzero_local_runtime_record_marks_linux_task_failed() { let mut state = AdapterState::default(); state.runtime_backend = RuntimeBackend::LocalServices; state.apply_runtime_record(RuntimeLaunchRecord { coordinator: "127.0.0.1:7999".to_owned(), node: "node".to_owned(), node_report: json!({ "terminal_event": { "task": "compile-linux-1", "task_definition": "compile-linux" } }), task_events: json!({ "events": [] }), placed_task_launched: true, status_code: Some(1), stdout_bytes: 0, stderr_bytes: 12, stdout_tail: String::new(), stderr_tail: "failed".to_owned(), stdout_truncated: false, stderr_truncated: false, artifact_path: None, event_count: 1, debug_epoch: None, stopped_task: None, stopped_probe_symbol: None, all_participants_frozen: false, }); let exact_runtime_thread = state .threads .values() .find(|thread| thread.task == TaskInstanceId::from("compile-linux-1")) .expect("the exact terminal task instance should have its own thread"); assert!(state.last_task_failed); assert!(state .command_status .contains("failed through local services")); assert!(matches!( exact_runtime_thread.state, DebugRuntimeState::Failed(_) )); assert!(exact_runtime_thread .recent_output .iter() .any(|line| line.contains("task failed"))); assert_eq!( state.threads[&LINUX_THREAD].state, DebugRuntimeState::Running ); } #[test] fn freeze_all_reports_failure_without_claiming_all_stop() { let mut state = AdapterState::default(); state .threads .get_mut(&PACKAGE_THREAD) .expect("package thread should exist") .freeze_supported = false; let error = freeze_all(&mut state, LINUX_THREAD, None).unwrap_err(); assert_eq!(error.thread_id, PACKAGE_THREAD); assert_eq!(error.task, TaskInstanceId::from("package-release")); assert!(error.message().contains("could not freeze")); assert_eq!(state.epoch, 0); assert!(state .threads .values() .all(|thread| thread.state == DebugRuntimeState::Running)); } #[test] fn freeze_all_success_freezes_every_running_participant() { let mut state = AdapterState::default(); freeze_all(&mut state, LINUX_THREAD, None).unwrap(); assert_eq!(state.epoch, 1); assert!(state .threads .values() .all(|thread| thread.state == DebugRuntimeState::Frozen)); } #[test] fn breakpoint_line_selects_main_or_spawned_virtual_thread() { let mut state = AdapterState::default(); state.breakpoints = vec![12]; assert_eq!(stopped_thread_for_breakpoint(&state), MAIN_THREAD); state.breakpoints = vec![42]; assert_eq!(stopped_thread_for_breakpoint(&state), LINUX_THREAD); } #[test] fn breakpoint_resolution_uses_bundle_debug_probe_metadata() { let mut state = AdapterState::default(); state.debug_probes = vec![BundleDebugProbe { id: "probe-linux".to_owned(), source_path: "src/build.rs".to_owned(), line_start: 20, line_end: 30, function: "compile_linux".to_owned(), task: clusterflux_core::TaskDefinitionId::from("compile-linux"), }]; let resolved = resolve_breakpoints(&mut state, vec![22, 80]); assert_eq!(resolved.len(), 2); assert!(resolved[0].verified); assert!(resolved[0].message.contains("probe-linux")); assert!(!resolved[1].verified); assert!(resolved[1].message.contains("No Clusterflux debug probe")); assert_eq!(state.breakpoints, vec![22]); assert_eq!(stopped_thread_for_breakpoint(&state), LINUX_THREAD); } #[test] fn breakpoint_updates_from_another_source_do_not_replace_configured_breakpoints() { let project = tempfile::tempdir().unwrap(); let source_dir = project.path().join("src"); fs::create_dir_all(&source_dir).unwrap(); fs::write(source_dir.join("build.rs"), "pub fn build() {}\n").unwrap(); fs::write(source_dir.join("main.rs"), "fn main() {}\n").unwrap(); let mut state = AdapterState::default(); state.project = project.path().to_string_lossy().into_owned(); state.source_path = "src/build.rs".to_owned(); state.breakpoints = vec![1]; let requested_source = source_dir.join("main.rs"); let resolved = resolve_breakpoints_for_source(&mut state, requested_source.to_str(), vec![1]); assert_eq!(resolved.len(), 1); assert!(!resolved[0].verified); assert!(resolved[0] .message .contains("configured for `src/build.rs`")); assert_eq!(state.breakpoints, vec![1]); } #[test] fn launch_threads_include_windows_task_in_same_virtual_process() { let state = AdapterState::default(); let windows = state .threads .get(&WINDOWS_THREAD) .expect("windows task should be debugger-visible"); assert_eq!(state.threads.len(), 4); assert_eq!(windows.id, WINDOWS_THREAD); assert_eq!(windows.task, TaskInstanceId::from("compile-windows")); assert_eq!(windows.name, "compile windows"); assert_eq!(windows.line, 52); assert_eq!(process_id(&state.project, &state.entry), state.process); } #[test] fn vscode_and_dap_share_stable_workspace_process_identity() { assert_eq!( process_id("/workspace/app", "build"), clusterflux_core::ProcessId::from("vp-e4bd6ef50539") ); } #[test] fn windows_thread_runtime_variables_share_virtual_process() { let state = AdapterState::default(); let windows = state .threads .get(&WINDOWS_THREAD) .expect("windows task should be debugger-visible"); let variables = variables_response(&state, windows.runtime_ref); let variables = variables["variables"] .as_array() .expect("variables response should be an array"); assert!(variables.iter().any(|variable| { variable["name"] == "virtual_process_id" && variable["value"] == state.process.to_string() })); assert!(variables.iter().any(|variable| { variable["name"] == "virtual_thread" && variable["value"] == "compile-windows" })); assert!(variables.iter().any(|variable| { variable["name"] == "task_attempt_id" && variable["value"] == format!("demo-attempt-{WINDOWS_THREAD}") })); } #[test] fn variables_expose_mvp_runtime_handles_and_output_tails() { let mut state = AdapterState::default(); state.runtime_backend = RuntimeBackend::LocalServices; state.apply_runtime_record(RuntimeLaunchRecord { coordinator: "127.0.0.1:7999".to_owned(), node: "node".to_owned(), node_report: json!({ "terminal_event": { "task": "compile-linux-1", "task_definition": "compile-linux" } }), task_events: json!({ "events": [] }), placed_task_launched: true, status_code: Some(0), stdout_bytes: 11, stderr_bytes: 7, stdout_tail: "hello tail\n".to_owned(), stderr_tail: "warn\n".to_owned(), stdout_truncated: false, stderr_truncated: false, artifact_path: Some("/vfs/artifacts/dap-output.txt".to_owned()), event_count: 1, debug_epoch: None, stopped_task: None, stopped_probe_symbol: None, all_participants_frozen: false, }); let exact_thread_id = state .threads .values() .find(|thread| thread.task == TaskInstanceId::from("compile-linux-1")) .map(|thread| thread.id) .expect("the exact terminal task instance should have its own thread"); { let runtime_thread = state.threads.get_mut(&exact_thread_id).unwrap(); runtime_thread.runtime_task_args = vec![("source".to_owned(), "source://checkout".to_owned())]; runtime_thread.runtime_handles = vec![( "artifact".to_owned(), "artifact://runtime/output".to_owned(), )]; runtime_thread.runtime_command_status = Some("frozen native command pid 42".to_owned()); } let runtime_thread = state .threads .get(&exact_thread_id) .expect("exact runtime thread should exist"); let args = variables_response(&state, runtime_thread.args_ref); let args = args["variables"] .as_array() .expect("variables response should be an array"); assert!(args.iter().any(|variable| { variable["name"] == "source" && variable["value"] == "source://checkout" && variable["type"] == "task-argument" })); assert!(args.iter().any(|variable| { variable["name"] == "artifact" && variable["value"] == "artifact://runtime/output" && variable["type"] == "runtime-handle" })); let runtime = variables_response(&state, runtime_thread.runtime_ref); let runtime = runtime["variables"].as_array().unwrap(); let command_ref = runtime .iter() .find(|variable| variable["name"] == "command_spec") .and_then(|variable| variable["variablesReference"].as_i64()) .expect("command spec should have child variables"); assert!(runtime .iter() .any(|variable| variable["name"] == "stdout_tail" && variable["value"] == "hello tail\n")); assert!(runtime .iter() .any(|variable| variable["name"] == "stderr_tail" && variable["value"] == "warn\n")); let command = variables_response(&state, command_ref); let command = command["variables"].as_array().unwrap(); assert!(command.iter().any(|variable| { variable["name"] == "status" && variable["value"] == "frozen native command pid 42" })); assert!(!command.iter().any(|variable| { variable["name"] == "program" || variable["name"] == "required_capability" })); } #[test] fn attach_record_replaces_demo_threads_with_authoritative_task_snapshots() { let mut state = AdapterState::default(); state.runtime_backend = RuntimeBackend::LiveServices; state.apply_attach_record(RuntimeLaunchRecord { coordinator: "127.0.0.1:9443".to_owned(), node: "coordinator-debug-attach".to_owned(), node_report: json!({ "debug_attach": { "authorization": { "allowed": true, "reason": "debug attach allowed" } }, "process_status": { "main_task_definition": "sha256:main-definition", "main_task_instance": format!("ti:{}:main", state.process), "main_debug_epoch": null, "coordinator_epoch": 1 }, "task_snapshots": { "snapshots": [{ "task": "compile-linux-1", "task_definition": "compile-linux", "attempt_id": "attempt-compile-linux-1", "display_name": "compile linux", "current": true, "state": "running", "node": "worker-linux" }] } }), task_events: json!({ "events": [{ "tenant": "tenant", "project": "project", "process": state.process.to_string(), "node": "worker-linux", "executor": "node", "task_definition": "compile-linux", "task": "compile-linux-1", "terminal_state": "completed", "status_code": 0, "stdout_bytes": 4, "stderr_bytes": 0, "stdout_tail": "done", "stderr_tail": "", "stdout_truncated": false, "stderr_truncated": false, "artifact_path": "/vfs/artifacts/from-attach.txt", "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact_size_bytes": 4 }] }), placed_task_launched: true, status_code: Some(0), stdout_bytes: 4, stderr_bytes: 0, stdout_tail: "done".to_owned(), stderr_tail: String::new(), stdout_truncated: false, stderr_truncated: false, artifact_path: Some("/vfs/artifacts/from-attach.txt".to_owned()), event_count: 1, debug_epoch: None, stopped_task: None, stopped_probe_symbol: None, all_participants_frozen: false, }); assert_eq!(state.threads.len(), 2); assert!(state.threads.contains_key(&MAIN_THREAD)); assert!(!state.threads.contains_key(&WINDOWS_THREAD)); let linux = state .threads .get(&LINUX_THREAD) .expect("current coordinator task snapshot should become debugger thread"); assert_eq!(linux.task, TaskInstanceId::from("compile-linux-1")); assert_eq!( linux.task_definition, clusterflux_core::TaskDefinitionId::from("compile-linux") ); assert!(linux.stdout_tail.is_empty()); assert_eq!(linux.state, DebugRuntimeState::Running); assert_eq!(state.runtime_artifact_path, None); assert!(state .command_status .contains("attached to existing virtual process")); assert!(state.command_status.contains("coordinator task event")); } #[test] fn attach_projects_active_coordinator_main_without_inventing_a_worker_task() { let mut state = AdapterState::default(); state.runtime_backend = RuntimeBackend::LiveServices; let main_instance = format!("ti:{}:main", state.process); state.apply_attach_record(RuntimeLaunchRecord { coordinator: "127.0.0.1:9443".to_owned(), node: "coordinator-main".to_owned(), node_report: json!({ "debug_attach": { "authorization": { "allowed": true } }, "process_status": { "process": state.process.to_string(), "state": "running", "main_task_definition": "sha256:main-definition", "main_task_instance": main_instance, "main_state": "running", "main_debug_epoch": null, "connected_nodes": [], "coordinator_epoch": 1 } }), task_events: json!({ "events": [] }), placed_task_launched: true, status_code: None, stdout_bytes: 0, stderr_bytes: 0, stdout_tail: String::new(), stderr_tail: String::new(), stdout_truncated: false, stderr_truncated: false, artifact_path: None, event_count: 0, debug_epoch: None, stopped_task: None, stopped_probe_symbol: None, all_participants_frozen: false, }); assert_eq!(state.threads.len(), 1); let main = state.threads.get(&MAIN_THREAD).unwrap(); assert_eq!(main.task, TaskInstanceId::from(main_instance.as_str())); assert_eq!( main.task_definition, clusterflux_core::TaskDefinitionId::from("sha256:main-definition") ); assert_eq!(main.state, DebugRuntimeState::Running); assert!(main.name.contains("coordinator main")); } #[test] fn terminal_events_do_not_resurrect_threads_absent_from_current_snapshots() { let mut state = AdapterState::default(); state.runtime_backend = RuntimeBackend::LiveServices; let main_instance = format!("ti:{}:main", state.process); state.apply_attach_record(RuntimeLaunchRecord { coordinator: "127.0.0.1:9443".to_owned(), node: "coordinator-main".to_owned(), node_report: json!({ "debug_attach": { "authorization": { "allowed": true } }, "task_snapshots": { "snapshots": [] } }), task_events: json!({ "events": [ { "node": "worker-linux", "executor": "node", "task_definition": "compile_linux", "task": "ti:child:1", "terminal_state": "completed", "status_code": 0 }, { "node": "coordinator-main", "executor": "coordinator_main", "task_definition": "sha256:main-definition", "task": main_instance, "terminal_state": "completed", "status_code": 0 } ] }), placed_task_launched: true, status_code: Some(0), stdout_bytes: 0, stderr_bytes: 0, stdout_tail: String::new(), stderr_tail: String::new(), stdout_truncated: false, stderr_truncated: false, artifact_path: None, event_count: 2, debug_epoch: None, stopped_task: None, stopped_probe_symbol: None, all_participants_frozen: false, }); assert!(state.threads.is_empty()); } #[test] fn source_locals_infer_clusterflux_api_values_from_runtime_state() { let mut state = AdapterState::default(); let project = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../examples/launch-build-demo") .canonicalize() .unwrap(); state.project = project.to_string_lossy().into_owned(); state.source_path = "src/build.rs".to_owned(); let source = fs::read_to_string(project.join(&state.source_path)).unwrap(); let inspection_line = source .lines() .position(|line| line.contains("let package_artifact = package.join()")) .map(|index| index as i64 + 1) .expect("flagship source must expose the post-join locals inspection point"); state.threads.get_mut(&MAIN_THREAD).unwrap().line = inspection_line; let thread = state.threads[&MAIN_THREAD].clone(); let locals = variables_response(&state, thread.locals_ref); let locals = locals["variables"].as_array().unwrap(); assert!(locals.iter().any(|variable| { variable["name"] == "linux" && variable["value"].as_str().is_some_and(|value| { value.contains("TaskHandle") && value.contains("compile-linux") && value.contains("virtual_thread_id = 2") }) })); assert!(locals .iter() .any(|variable| { variable["name"] == "linux_thread" && variable["value"] == "2" })); assert!(locals.iter().any(|variable| { variable["name"] == "linux_artifact" && variable["value"] .as_str() .is_some_and(|value| value.contains("Artifact")) })); assert!(locals.iter().any(|variable| { variable["name"] == "unavailable-local-diagnostic" && variable["type"] == "unavailable-local" })); } #[test] fn source_locals_infer_task_with_arg_values_from_runtime_state() { let project = std::env::temp_dir().join(format!( "clusterflux-dap-task-with-arg-{}", std::process::id() )); let src = project.join("src"); fs::create_dir_all(&src).unwrap(); fs::write( src.join("main.rs"), r#"use clusterflux::{Artifact, EnvRef, SourceSnapshot}; async fn build_release() -> Result<(), clusterflux::TaskArgError> { let source = prepare_source_snapshot(); let compile = clusterflux::spawn::task_with_arg(source.clone(), compile_linux) .name("compile linux") .env(linux_command_env()) .start() .await?; let compile_thread = compile.virtual_thread_id(); let linux_artifact = compile.join().await; let package = clusterflux::spawn::task_with_arg(vec![linux_artifact.clone()], package_release) .name("package release") .env(coordinator_env()) .start() .await?; let package_thread = package.virtual_thread_id(); let release_artifact = package.join().await; Ok(()) } fn prepare_source_snapshot() -> SourceSnapshot { SourceSnapshot { digest: "source://coordinator/quick-test-checkout".to_owned(), } } fn linux_command_env() -> EnvRef { clusterflux::env!("linux-command") } fn coordinator_env() -> EnvRef { clusterflux::env!("coordinator") } fn compile_linux(source: SourceSnapshot) -> Artifact { Artifact { id: source.digest } } fn package_release(inputs: Vec) -> Artifact { inputs.into_iter().next().unwrap() } "#, ) .unwrap(); let mut state = AdapterState::default(); state.project = project.to_string_lossy().into_owned(); state.source_path = "src/main.rs".to_owned(); state.threads.get_mut(&MAIN_THREAD).unwrap().line = 23; let thread = state.threads[&MAIN_THREAD].clone(); let locals = variables_response(&state, thread.locals_ref); let locals = locals["variables"].as_array().unwrap(); assert!(locals.iter().any(|variable| { variable["name"] == "source" && variable["value"] .as_str() .is_some_and(|value| value.contains("source://coordinator/quick-test-checkout")) })); assert!(locals.iter().any(|variable| { variable["name"] == "compile" && variable["value"].as_str().is_some_and(|value| { value.contains("TaskHandle") && value.contains("compile-linux") && value.contains("virtual_thread_id = 2") && value.contains("linux-command") }) })); assert!(locals .iter() .any(|variable| variable["name"] == "compile_thread" && variable["value"] == "2")); assert!(locals.iter().any(|variable| { variable["name"] == "linux_artifact" && variable["value"] .as_str() .is_some_and(|value| value.contains("Artifact")) })); assert!(locals.iter().any(|variable| { variable["name"] == "package" && variable["value"].as_str().is_some_and(|value| { value.contains("TaskHandle") && value.contains("package-release") && value.contains("virtual_thread_id = 4") && value.contains("coordinator") }) })); assert!(locals .iter() .any(|variable| variable["name"] == "package_thread" && variable["value"] == "4")); assert!(locals.iter().any(|variable| { variable["name"] == "release_artifact" && variable["value"] .as_str() .is_some_and(|value| value.contains("Artifact")) })); let _ = fs::remove_dir_all(project); } #[test] fn wasm_frame_locals_expose_only_values_from_the_node_snapshot() { let mut state = AdapterState::default(); state.project = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../examples/launch-build-demo") .to_string_lossy() .into_owned(); state.source_path = "src/build.rs".to_owned(); let source = fs::read_to_string(Path::new(&state.project).join(&state.source_path)).unwrap(); state.threads.get_mut(&MAIN_THREAD).unwrap().line = source .lines() .position(|line| line.contains("input + 1")) .map(|line| line as i64 + 1) .expect("task_add_one source line"); state .threads .get_mut(&MAIN_THREAD) .unwrap() .wasm_local_values = vec![("wasm_local_0".to_owned(), "I32(41)".to_owned())]; let thread = state.threads[&MAIN_THREAD].clone(); let locals = variables_response(&state, thread.wasm_locals_ref); let locals = locals["variables"].as_array().unwrap(); assert!(locals.iter().any(|variable| { variable["name"] == "wasm_local_0" && variable["type"] == "wasm-frame-local" && variable["value"] .as_str() .is_some_and(|value| value.contains("41")) })); assert!(!locals .iter() .any(|variable| variable["name"] == "wasm-local-diagnostic")); } #[test] fn detects_current_failed_attempt_awaiting_operator_action() { let snapshots = json!({ "snapshots": [ { "task": "task-stale", "attempt_id": "attempt-stale", "state": "failed_awaiting_action", "current": false }, { "task": "task-current", "attempt_id": "attempt-current", "state": "failed_awaiting_action", "current": true } ] }); assert_eq!( runtime_client::failed_awaiting_action_snapshot(&snapshots), Some(("task-current", "attempt-current")) ); }