fix(cluster,cli): apply failure output — persisted statuses only, changes list printed

Two review findings (greptile, PR #165):

- ApplyOutput.resource_statuses on a failed state write now carries the
  pre-apply on-disk snapshot instead of the in-memory mutations that were
  never persisted, so automation reading the field independently of `ok`
  cannot see phantom applied/blocked statuses. Regression test forces the
  state write to fail via a read-only __cluster dir (unix-only, skips when
  permissions are not enforced).
- Human-mode `cluster apply` prints the classified changes list on failure
  too, so an operator debugging a partial apply without --json sees what was
  attempted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-06-10 00:35:03 +03:00
parent d870eaaf3f
commit 5e1dede08f
2 changed files with 113 additions and 20 deletions

View file

@ -820,34 +820,42 @@ fn print_cluster_apply_human(output: &ApplyOutput) {
"cluster apply: {} applied, {} deferred/blocked",
output.applied_count, output.deferred_count
);
for change in &output.changes {
match (&change.disposition, change.reason.as_deref()) {
(Some(disposition), Some(reason)) => println!(
" {:?} {} [{disposition:?}: {reason}]",
change.operation, change.resource
),
(Some(disposition), None) => println!(
" {:?} {} [{disposition:?}]",
change.operation, change.resource
),
_ => println!(" {:?} {}", change.operation, change.resource),
}
}
if output.changes.is_empty() {
println!(" no changes");
}
} else {
println!("cluster apply failed");
}
// The change list prints on failure too: an operator debugging a partial
// apply (payload or state-write error) needs to see what was attempted.
print_cluster_apply_changes(&output.changes);
if output.ok {
let state = &output.state_observations;
println!(
" state: revision {}, converged: {}, written: {}",
state.state_revision, output.converged, output.state_written
);
println!(" note: applied = recorded in the cluster catalog; the server still boots from omnigraph.yaml");
} else {
println!("cluster apply failed");
}
print_cluster_diagnostics(&output.diagnostics);
}
fn print_cluster_apply_changes(changes: &[omnigraph_cluster::PlanChange]) {
for change in changes {
match (&change.disposition, change.reason.as_deref()) {
(Some(disposition), Some(reason)) => println!(
" {:?} {} [{disposition:?}: {reason}]",
change.operation, change.resource
),
(Some(disposition), None) => println!(
" {:?} {} [{disposition:?}]",
change.operation, change.resource
),
_ => println!(" {:?} {}", change.operation, change.resource),
}
}
if changes.is_empty() {
println!(" no changes");
}
}
fn print_cluster_status_human(output: &StatusOutput) {
if output.ok {
let state = &output.state_observations;