Summarize node readiness in doctor
This commit is contained in:
parent
1c4b045a6a
commit
ab84fd382d
4 changed files with 148 additions and 11 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "28a8b4578c0b3fd289b938c1e6f07cc15e6848b2",
|
"source_commit": "714ecfe0fc03dec4fa3ca48b8f8760a7aeead338",
|
||||||
"release_name": "dryrun-28a8b4578c0b",
|
"release_name": "dryrun-714ecfe0fc03",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -749,6 +749,16 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
.and_then(|config| config.coordinator.clone())
|
.and_then(|config| config.coordinator.clone())
|
||||||
});
|
});
|
||||||
let coordinator_reachability = coordinator_reachability(coordinator.as_deref());
|
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!({
|
Ok(json!({
|
||||||
"command": "doctor",
|
"command": "doctor",
|
||||||
"cwd": cwd,
|
"cwd": cwd,
|
||||||
|
|
@ -756,15 +766,9 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
"coordinator_reachability": coordinator_reachability,
|
"coordinator_reachability": coordinator_reachability,
|
||||||
"auth": auth_state_value(&cwd)?,
|
"auth": auth_state_value(&cwd)?,
|
||||||
"project": config,
|
"project": config,
|
||||||
"dependencies": {
|
"dependencies": dependencies,
|
||||||
"cargo": command_available("cargo"),
|
"node_readiness": node_readiness,
|
||||||
"git": command_available("git"),
|
"node_readiness_summary": node_readiness_summary,
|
||||||
"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(),
|
|
||||||
"next_actions": [
|
"next_actions": [
|
||||||
"disasmer login --browser",
|
"disasmer login --browser",
|
||||||
"disasmer project init",
|
"disasmer project init",
|
||||||
|
|
@ -774,6 +778,78 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::<Vec<_>>(),
|
||||||
|
"next_actions": next_actions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
|
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
|
||||||
let Some(coordinator) = coordinator else {
|
let Some(coordinator) = coordinator else {
|
||||||
return json!({
|
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::<Vec<_>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
if !actions.is_empty() {
|
||||||
|
lines.push(format!("node next: {}", actions.join("; ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(disclosures) = value
|
if let Some(disclosures) = value
|
||||||
.get("grant_disclosures")
|
.get("grant_disclosures")
|
||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
|
|
@ -6887,6 +6981,28 @@ mod tests {
|
||||||
report["coordinator_reachability"]["status"],
|
report["coordinator_reachability"]["status"],
|
||||||
"not_configured"
|
"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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,11 @@ expect(
|
||||||
"log redaction criteria",
|
"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/
|
/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(
|
expect(
|
||||||
criteria,
|
criteria,
|
||||||
"quota resource-category criteria",
|
"quota resource-category criteria",
|
||||||
|
|
@ -174,6 +179,7 @@ for (const [name, pattern] of [
|
||||||
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
||||||
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
||||||
["doctor command", /Doctor\(DoctorArgs\)/],
|
["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 status command", /enum AuthCommands[\s\S]*Status\(AuthStatusArgs\)/],
|
||||||
["auth logout command", /enum AuthCommands[\s\S]*Logout\(AuthLogoutArgs\)/],
|
["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\)/],
|
["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"\]/],
|
["login JSON mode", /\["login", "--coordinator", "https:\/\/coord\.example\.test", "--json"\]/],
|
||||||
["doctor human mode", /\["doctor"\]/],
|
["doctor human mode", /\["doctor"\]/],
|
||||||
["doctor reachability JSON mode", /doctorJson\.coordinator_reachability\.status/],
|
["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 JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/],
|
||||||
["bundle inspect large input JSON mode", /large_input_policy[\s\S]*SourceSnapshot/],
|
["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/],
|
["bundle inspect restart compatibility JSON mode", /restart_compatibility[\s\S]*source_edits_can_restart_from_clean_task_boundary/],
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,25 @@ assertHuman("doctor", doctorHuman, [
|
||||||
/dependencies:/,
|
/dependencies:/,
|
||||||
/auth:/,
|
/auth:/,
|
||||||
/node capabilities:/,
|
/node capabilities:/,
|
||||||
|
/node readiness: (ready_to_attach|local_dependencies_missing|limited_capabilities)/,
|
||||||
|
/node next:/,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const doctorJson = json(["doctor", "--json"]);
|
const doctorJson = json(["doctor", "--json"]);
|
||||||
assert.strictEqual(doctorJson.coordinator_reachability.checked, false);
|
assert.strictEqual(doctorJson.coordinator_reachability.checked, false);
|
||||||
assert.strictEqual(doctorJson.coordinator_reachability.status, "not_configured");
|
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"], {
|
const authJson = json(["auth", "status", "--json"], {
|
||||||
DISASMER_TOKEN: "token",
|
DISASMER_TOKEN: "token",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue