From ab84fd382da21afe7d359012ae108fb2929914bc Mon Sep 17 00:00:00 2001 From: Michel Paulissen <862400+MichelPaulissen@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:52:44 +0200 Subject: [PATCH] Summarize node readiness in doctor --- DISASMER_PUBLIC_TREE.json | 4 +- crates/disasmer-cli/src/main.rs | 134 ++++++++++++++++++++++++++-- scripts/cli-first-contract-smoke.js | 7 ++ scripts/cli-output-mode-smoke.js | 14 +++ 4 files changed, 148 insertions(+), 11 deletions(-) diff --git a/DISASMER_PUBLIC_TREE.json b/DISASMER_PUBLIC_TREE.json index 59a544e..805c668 100644 --- a/DISASMER_PUBLIC_TREE.json +++ b/DISASMER_PUBLIC_TREE.json @@ -1,7 +1,7 @@ { "kind": "disasmer-filtered-public-tree", - "source_commit": "28a8b4578c0b3fd289b938c1e6f07cc15e6848b2", - "release_name": "dryrun-28a8b4578c0b", + "source_commit": "714ecfe0fc03dec4fa3ca48b8f8760a7aeead338", + "release_name": "dryrun-714ecfe0fc03", "filtered_out": [ "private/**", "experiments/**", diff --git a/crates/disasmer-cli/src/main.rs b/crates/disasmer-cli/src/main.rs index 9d58681..fc3c3b3 100644 --- a/crates/disasmer-cli/src/main.rs +++ b/crates/disasmer-cli/src/main.rs @@ -749,6 +749,16 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result { .and_then(|config| config.coordinator.clone()) }); let coordinator_reachability = coordinator_reachability(coordinator.as_deref()); + let dependencies = json!({ + "cargo": command_available("cargo"), + "git": command_available("git"), + "podman": command_available("podman"), + "disasmer-node": command_available("disasmer-node") || sibling_binary("disasmer-node").is_some(), + "disasmer-coordinator": command_available("disasmer-coordinator") || sibling_binary("disasmer-coordinator").is_some(), + "disasmer-debug-dap": command_available("disasmer-debug-dap") || sibling_binary("disasmer-debug-dap").is_some(), + }); + let node_readiness = NodeCapabilities::detect_current(); + let node_readiness_summary = node_readiness_summary(&node_readiness, &dependencies); Ok(json!({ "command": "doctor", "cwd": cwd, @@ -756,15 +766,9 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result { "coordinator_reachability": coordinator_reachability, "auth": auth_state_value(&cwd)?, "project": config, - "dependencies": { - "cargo": command_available("cargo"), - "git": command_available("git"), - "podman": command_available("podman"), - "disasmer-node": command_available("disasmer-node") || sibling_binary("disasmer-node").is_some(), - "disasmer-coordinator": command_available("disasmer-coordinator") || sibling_binary("disasmer-coordinator").is_some(), - "disasmer-debug-dap": command_available("disasmer-debug-dap") || sibling_binary("disasmer-debug-dap").is_some(), - }, - "node_readiness": NodeCapabilities::detect_current(), + "dependencies": dependencies, + "node_readiness": node_readiness, + "node_readiness_summary": node_readiness_summary, "next_actions": [ "disasmer login --browser", "disasmer project init", @@ -774,6 +778,78 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result { })) } +fn node_readiness_summary(capabilities: &NodeCapabilities, dependencies: &Value) -> Value { + let has_command = capabilities.capabilities.contains(&Capability::Command); + let has_wasmtime = capabilities.capabilities.contains(&Capability::Wasmtime); + let has_vfs_artifacts = capabilities + .capabilities + .contains(&Capability::VfsArtifacts); + let has_source_filesystem = capabilities + .capabilities + .contains(&Capability::SourceFilesystem); + let has_container_backend = capabilities + .environment_backends + .contains(&disasmer_core::EnvironmentBackend::Container); + let podman_available = dependencies + .get("podman") + .and_then(Value::as_bool) + .unwrap_or(false); + let git_available = dependencies + .get("git") + .and_then(Value::as_bool) + .unwrap_or(false); + let disasmer_node_available = dependencies + .get("disasmer-node") + .and_then(Value::as_bool) + .unwrap_or(false); + + let mut missing = Vec::new(); + if has_container_backend && !podman_available { + missing.push("podman"); + } + if capabilities.source_providers.contains("git") && !git_available { + missing.push("git"); + } + if !disasmer_node_available { + missing.push("disasmer-node"); + } + + let basic_runtime_ready = + has_command && has_wasmtime && has_vfs_artifacts && has_source_filesystem; + let local_dependencies_ready = missing.is_empty(); + let status = if basic_runtime_ready && local_dependencies_ready { + "ready_to_attach" + } else if basic_runtime_ready { + "local_dependencies_missing" + } else { + "limited_capabilities" + }; + let next_actions = if status == "ready_to_attach" { + vec!["disasmer node enroll", "disasmer node attach --worker"] + } else { + vec![ + "install missing local dependencies", + "rerun disasmer doctor", + ] + }; + + json!({ + "status": status, + "explicit_attach_required": true, + "command_execution_capability": has_command, + "wasmtime_capability": has_wasmtime, + "artifact_capability": has_vfs_artifacts, + "source_filesystem_capability": has_source_filesystem, + "container_backend_reported": has_container_backend, + "podman_binary_available": podman_available, + "git_binary_available": git_available, + "node_binary_available": disasmer_node_available, + "missing_local_dependencies": missing, + "source_providers": capabilities.source_providers.iter().collect::>(), + "next_actions": next_actions, + }) +} + fn coordinator_reachability(coordinator: Option<&str>) -> Value { let Some(coordinator) = coordinator else { return json!({ @@ -2537,6 +2613,24 @@ fn human_report(value: &Value) -> String { } } } + if let Some(summary) = value.get("node_readiness_summary") { + push_nested_string_field(&mut lines, summary, "status", "node readiness"); + if let Some(missing) = summary + .get("missing_local_dependencies") + .and_then(Value::as_array) + { + let missing = missing.iter().filter_map(Value::as_str).collect::>(); + if !missing.is_empty() { + lines.push(format!("node missing dependencies: {}", missing.join(", "))); + } + } + if let Some(actions) = summary.get("next_actions").and_then(Value::as_array) { + let actions = actions.iter().filter_map(Value::as_str).collect::>(); + if !actions.is_empty() { + lines.push(format!("node next: {}", actions.join("; "))); + } + } + } if let Some(disclosures) = value .get("grant_disclosures") .and_then(Value::as_array) @@ -6887,6 +6981,28 @@ mod tests { report["coordinator_reachability"]["status"], "not_configured" ); + assert!(matches!( + report["node_readiness_summary"]["status"].as_str(), + Some("ready_to_attach") + | Some("local_dependencies_missing") + | Some("limited_capabilities") + )); + assert_eq!( + report["node_readiness_summary"]["explicit_attach_required"], + true + ); + assert_eq!( + report["node_readiness_summary"]["command_execution_capability"], + true + ); + assert!(report["node_readiness_summary"]["missing_local_dependencies"].is_array()); + assert!( + report["node_readiness_summary"]["next_actions"] + .as_array() + .unwrap() + .len() + >= 2 + ); } #[test] diff --git a/scripts/cli-first-contract-smoke.js b/scripts/cli-first-contract-smoke.js index 960f914..45ff46c 100644 --- a/scripts/cli-first-contract-smoke.js +++ b/scripts/cli-first-contract-smoke.js @@ -102,6 +102,11 @@ expect( "log redaction criteria", /Logs are capped, truncated honestly[\s\S]*preserve byte counts and truncation flags[\s\S]*Secret-like values are redacted[\s\S]*common token\/password\/bearer patterns/ ); +expect( + criteria, + "doctor node readiness criteria", + /`disasmer doctor` reports missing local dependencies[\s\S]*explicit node readiness summary[\s\S]*missing local dependencies[\s\S]*node next actions/ +); expect( criteria, "quota resource-category criteria", @@ -174,6 +179,7 @@ for (const [name, pattern] of [ ["human report renderer", /fn human_report\(value: &Value\) -> String/], ["shared report emitter", /fn emit_report\(report: &T, json_output: bool\) -> Result<\(\)>/], ["doctor command", /Doctor\(DoctorArgs\)/], + ["doctor node readiness summary", /fn node_readiness_summary[\s\S]*ready_to_attach[\s\S]*explicit_attach_required[\s\S]*missing_local_dependencies[\s\S]*next_actions/], ["auth status command", /enum AuthCommands[\s\S]*Status\(AuthStatusArgs\)/], ["auth logout command", /enum AuthCommands[\s\S]*Logout\(AuthLogoutArgs\)/], ["key lifecycle commands", /enum KeyCommands[\s\S]*Add\(KeyAddArgs\)[\s\S]*List\(KeyListArgs\)[\s\S]*Revoke\(KeyRevokeArgs\)/], @@ -477,6 +483,7 @@ for (const [name, pattern] of [ ["login JSON mode", /\["login", "--coordinator", "https:\/\/coord\.example\.test", "--json"\]/], ["doctor human mode", /\["doctor"\]/], ["doctor reachability JSON mode", /doctorJson\.coordinator_reachability\.status/], + ["doctor node readiness JSON mode", /doctorJson\.node_readiness_summary\.status[\s\S]*explicit_attach_required[\s\S]*missing_local_dependencies/], ["bundle inspect JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/], ["bundle inspect large input JSON mode", /large_input_policy[\s\S]*SourceSnapshot/], ["bundle inspect restart compatibility JSON mode", /restart_compatibility[\s\S]*source_edits_can_restart_from_clean_task_boundary/], diff --git a/scripts/cli-output-mode-smoke.js b/scripts/cli-output-mode-smoke.js index 4a601de..df98100 100644 --- a/scripts/cli-output-mode-smoke.js +++ b/scripts/cli-output-mode-smoke.js @@ -65,11 +65,25 @@ assertHuman("doctor", doctorHuman, [ /dependencies:/, /auth:/, /node capabilities:/, + /node readiness: (ready_to_attach|local_dependencies_missing|limited_capabilities)/, + /node next:/, ]); const doctorJson = json(["doctor", "--json"]); assert.strictEqual(doctorJson.coordinator_reachability.checked, false); assert.strictEqual(doctorJson.coordinator_reachability.status, "not_configured"); +assert( + ["ready_to_attach", "local_dependencies_missing", "limited_capabilities"].includes( + doctorJson.node_readiness_summary.status + ) +); +assert.strictEqual(doctorJson.node_readiness_summary.explicit_attach_required, true); +assert.strictEqual( + doctorJson.node_readiness_summary.command_execution_capability, + true +); +assert(Array.isArray(doctorJson.node_readiness_summary.missing_local_dependencies)); +assert(doctorJson.node_readiness_summary.next_actions.length >= 2); const authJson = json(["auth", "status", "--json"], { DISASMER_TOKEN: "token",