// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: The wire contract with metralectl, pinned from this side.
//!
//! Owner: server CLI (`met benchmark certify`).
//! The JSON shapes of `bench nodes/submit/attach/fetch --json` parse into what
//! the driver acts on, and every exit code maps to its class.
//! Invariants: none beyond the types.
use super::*;
/// 2026-09-26: A full `bench nodes --json` row (`agent_version` 0.5.0); `node_tests` reuses it.
pub(in crate::cli::bench_certify::remote) const NODE_ROW: &str = r#"{"node":"10.10.10.2","ok":true,"info":{"node":"1730e1bea1873a8a7000000000000000000000000000000000000000000000000","name":"spark-43fa","agent_version":"0.5.0","peer_version_max":3,"bench_enabled":true,"disabled_reason":null,"gpu":{"name":"NVIDIA GB10","count":1,"driver_version":"580.126.09","cuda_version":"13.0","sm_clock_mhz":{"state":"reading","value":208.0},"sm_clock_healthy_mhz":3003,"temperature_c":{"state":"reading","value":48.0},"memory_total_bytes":{"state":"reading","value":130663886848.0},"memory_used_frac":{"state":"reading","value":0.12},"memory_is_unified":true},"thermal":{"chassis_temps_c":[65.0,62.0,59.0],"throttle_thermal":false,"sm_clock_max_mhz":3003.0,"mem_total_kb":127601452},"alerts":[],"hardware_class":"gb10","metrale_repo":{"path":"/workspace/metrale","remote_name":"metrale","remote_url":"git@github.com:Metrale/metrale-inference.git","head_sha":"1a0dc88a8c9083bb956bd84cafa2cccbdb8e6e18","fetched_at_s":1757770000},"metrale_home":"/workspace/.metrale","signer_fp":"a27dbc8ed2fc2a31","signer_pubkey_hex":"00","recipes_synced":true,"built_shas":[{"sha":"1a0dc88a8c9083bb956bd84cafa2cccbdb8e6e18","binary_sha256":"ff","built_at_s":1,"bytes":1}],"busy":false,"busy_reason":null,"running_job":null,"queued":0,"queue_depth":2,"disk_free_bytes":{"state":"reading","value":50000000000.0},"min_free_disk_bytes":21474836480,"min_free_fraction":0.85,"host_free_fraction":{"state":"reading","value":0.91},"max_run_s":10800}}"#;
#[test]
fn a_node_row_parses_into_the_facts_the_driver_reads() {
let row: NodeRow = serde_json::from_str(NODE_ROW).expect("parses");
assert!(row.ok);
let info = row.info.expect("info");
assert!(info.bench_enabled);
assert_eq!(info.hardware_class.as_deref(), Some("gb10"));
assert_eq!(info.signer_fp.as_deref(), Some("a27dbc8ed2fc2a31"));
assert_eq!(
info.gpu.as_ref().map(|g| g.driver_version.as_str()),
Some("580.126.09")
);
let t = info.thermal.as_ref().expect("thermal");
assert_eq!(t.chassis_temps_c, vec![65.0, 62.0, 59.0]);
assert_eq!(t.throttle_thermal, Some(false));
assert_eq!(t.sm_clock_max_mhz, Some(3003.0));
assert_eq!(t.mem_total_kb, Some(127_601_452));
assert_eq!(info.host_free_fraction.value(), Some(0.91));
assert_eq!(info.built_shas.len(), 1);
// 2026-09-26: An error row, and an unknown extra field, both parse.
let err: NodeRow = serde_json::from_str(
r#"{"node":"10.10.10.9","ok":false,"error":{"code":"unreachable","message":"no","node":"10.10.10.9","retryable":true},"future_field":1}"#,
)
.unwrap();
assert!(!err.ok && err.info.is_none());
assert_eq!(err.error.unwrap().code, "unreachable");
// 2026-09-26: A node with no thermal block at all: the field is None, not a parse error.
let bare: NodeRow =
serde_json::from_str(r#"{"node":"x","ok":true,"info":{"bench_enabled":true}}"#).unwrap();
assert!(bare.info.unwrap().thermal.is_none());
}
#[test]
fn stream_events_and_the_done_line_carry_what_classification_needs() {
let ev: StreamEvent = serde_json::from_str(
r#"{"job":"jb-1-a","seq":9,"at_ms":1,"kind":"progress","phase":"isl 512","detail":"[3/8]"}"#,
)
.unwrap();
assert_eq!((ev.seq, ev.kind.as_str()), (9, "progress"));
let done: StreamEvent = serde_json::from_str(
r#"{"job":"jb-1-a","seq":40,"at_ms":1,"kind":"done","outcome":"completed","exit_code":2,"verdict":{"kind":"fail","text":"below floor"},"record":"2026-09-13-1a0dc88a8c.json","signature":null}"#,
)
.unwrap();
assert_eq!(done.outcome.as_deref(), Some("completed"));
assert_eq!(done.exit_code, Some(2));
let failed: StreamEvent = serde_json::from_str(
r#"{"job":"jb-1-a","seq":3,"at_ms":1,"kind":"done","outcome":"failed","stage":"building","reason":"cargo exited 101"}"#,
)
.unwrap();
assert_eq!(failed.stage.as_deref(), Some("building"));
assert_eq!(failed.reason.as_deref(), Some("cargo exited 101"));
let s: Submitted = serde_json::from_str(
r#"{"node":"10.10.10.2","node_id":"ab","job_id":"jb-1-a","job_key":"k","existing":true,"state":"queued","position":0}"#,
)
.unwrap();
assert!(s.existing);
let files: Vec<FetchedFile> = serde_json::from_str(
r#"[{"name":"r.json","relative_path":".benchmarks/decode-floor/r.json","path":"/tmp/x/.benchmarks/decode-floor/r.json","bytes":3,"sha256":"00"}]"#,
)
.unwrap();
assert_eq!(
files[0].path,
PathBuf::from("/tmp/x/.benchmarks/decode-floor/r.json")
);
}
#[test]
fn every_exit_code_maps_to_its_class_and_a_signal_is_other() {
let table = [
(0, Exit::Done),
(1, Exit::Usage),
(2, Exit::Unreachable),
(3, Exit::NotPaired),
(4, Exit::Refused),
(5, Exit::JobFailed),
(6, Exit::Cancelled),
(7, Exit::StreamLost),
(8, Exit::Unsupported),
];
for (code, exit) in table {
assert_eq!(Exit::from_code(Some(code)), exit);
}
assert_eq!(Exit::from_code(None), Exit::Other(None));
assert_eq!(Exit::from_code(Some(9)), Exit::Other(Some(9)));
let e = ErrorObj {
code: "refused:busy".into(),
message: "busy".into(),
fix: Some("retry after 60 s".into()),
..Default::default()
};
assert_eq!(e.to_string(), "refused:busy: busy (fix: retry after 60 s)");
}