use super::common::*;
#[test]
fn node_topic_info_json_contract_has_stable_shape() {
let status = temp_path("status-contract", "json");
let status_arg = status.to_str().expect("utf-8 path");
let node_output = run_cli(&[
"node",
"info",
"demo_node",
"--report",
status_arg,
"--refresh-demo",
"--json",
]);
assert!(
node_output.status.success(),
"stderr: {}",
node_output.stderr
);
let node_json: Value = serde_json::from_str(&node_output.stdout).expect("parse node info json");
assert_exact_object_keys(
&node_json,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
node_json.get("query").expect("node query object"),
&["node_name"],
);
assert_exact_object_keys(
node_json.get("result").expect("node result object"),
&["name", "namespace", "capabilities"],
);
let topic_output = run_cli(&[
"topic",
"info",
"/demo/camera/raw",
"--report",
status_arg,
"--json",
]);
assert!(
topic_output.status.success(),
"stderr: {}",
topic_output.stderr
);
let topic_json: Value =
serde_json::from_str(&topic_output.stdout).expect("parse topic info json");
assert_exact_object_keys(
&topic_json,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
topic_json.get("query").expect("topic query object"),
&["topic_name"],
);
assert_exact_object_keys(
topic_json.get("result").expect("topic result object"),
&["name", "schema", "qos", "load", "endpoints"],
);
assert_exact_object_keys(
topic_json["result"]
.get("endpoints")
.expect("topic endpoints object"),
&["publishers", "subscribers", "reliable_ack"],
);
assert_exact_object_keys(
topic_json["result"]["endpoints"]
.get("reliable_ack")
.expect("topic reliable_ack object"),
&["local", "remote"],
);
assert_exact_object_keys(
topic_json["result"]["endpoints"]["reliable_ack"]
.get("local")
.expect("topic local reliable ack object"),
&["subscribers", "acked_subscribers", "min_acked_seq", "max_acked_seq"],
);
assert_exact_object_keys(
topic_json["result"]["endpoints"]["reliable_ack"]
.get("remote")
.expect("topic remote reliable ack object"),
&["subscribers", "acked_subscribers", "min_acked_seq", "max_acked_seq"],
);
}
#[test]
fn resource_list_json_contract_has_stable_shape() {
let report = temp_path("resource-list-contract", "json");
let report_arg = report.to_str().expect("utf-8 path");
let cases = [
("node", "robotrt.node.list.v1"),
("topic", "robotrt.topic.list.v1"),
("service", "robotrt.service.list.v1"),
("action", "robotrt.action.list.v1"),
("mission", "robotrt.mission.list.v1"),
];
for (index, (kind, api_version)) in cases.iter().enumerate() {
let mut args = vec![*kind, "list", "--report", report_arg, "--json"];
if index == 0 {
args.push("--refresh-demo");
}
let output = run_cli(args.as_slice());
assert!(output.status.success(), "stderr: {}", output.stderr);
let payload: Value =
serde_json::from_str(&output.stdout).expect("parse resource list json");
assert_exact_object_keys(
&payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"result",
],
);
assert_eq!(
payload.get("api_version").and_then(Value::as_str),
Some(*api_version)
);
assert_eq!(payload.get("kind").and_then(Value::as_str), Some(*kind));
assert_exact_object_keys(
payload.get("source").expect("source object"),
&["mode", "service", "endpoint", "timeout_ms"],
);
assert_exact_object_keys(
payload.get("result").expect("result object"),
&["count", "items"],
);
}
}
#[test]
fn runtime_status_json_contract_has_stable_shape() {
let status = temp_path("runtime-status-contract", "json");
let status_arg = status.to_str().expect("utf-8 path");
let mission_watch = run_cli(&[
"mission",
"watch",
"demo_mission",
"--report",
status_arg,
"--iterations",
"1",
"--refresh-demo",
"--json",
]);
assert!(
mission_watch.status.success(),
"stderr: {}",
mission_watch.stderr
);
let mission_payload: Value =
serde_json::from_str(&mission_watch.stdout).expect("parse mission watch json");
assert_exact_object_keys(
&mission_payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
mission_payload.get("query").expect("mission query object"),
&["mission_name", "iterations", "interval_ms"],
);
assert_exact_object_keys(
mission_payload
.get("result")
.expect("mission result object"),
&["samples"],
);
let plugin_list = run_cli(&[
"plugin", "list", "--report", status_arg, "--loaded", "--json",
]);
assert!(
plugin_list.status.success(),
"stderr: {}",
plugin_list.stderr
);
let plugin_payload: Value =
serde_json::from_str(&plugin_list.stdout).expect("parse plugin list json");
assert_exact_object_keys(
&plugin_payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
plugin_payload.get("query").expect("plugin query object"),
&["loaded_only"],
);
assert_exact_object_keys(
plugin_payload.get("result").expect("plugin result object"),
&["plugins"],
);
let action_watch = run_cli(&[
"action",
"watch",
"/demo/calibrate",
"--report",
status_arg,
"--iterations",
"1",
"--json",
]);
assert!(
action_watch.status.success(),
"stderr: {}",
action_watch.stderr
);
let action_watch_payload: Value =
serde_json::from_str(&action_watch.stdout).expect("parse action watch json");
assert_exact_object_keys(
&action_watch_payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
action_watch_payload
.get("query")
.expect("action watch query object"),
&["action_name", "iterations", "interval_ms"],
);
assert_exact_object_keys(
action_watch_payload
.get("result")
.expect("action watch result object"),
&["samples"],
);
let health = run_cli(&["health", "--report", status_arg, "--json"]);
assert!(health.status.success(), "stderr: {}", health.stderr);
let health_payload: Value = serde_json::from_str(&health.stdout).expect("parse health json");
assert_exact_object_keys(
&health_payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"result",
],
);
assert_exact_object_keys(
health_payload.get("result").expect("health result object"),
&["health"],
);
let graph = run_cli(&["graph", "--report", status_arg, "--json"]);
assert!(graph.status.success(), "stderr: {}", graph.stderr);
let graph_payload: Value = serde_json::from_str(&graph.stdout).expect("parse graph json");
assert_exact_object_keys(
&graph_payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"result",
],
);
assert_exact_object_keys(
graph_payload.get("result").expect("graph result object"),
&["edges"],
);
}
#[test]
fn action_info_json_contract_has_stable_shape() {
let status = temp_path("action-info-contract", "json");
let status_arg = status.to_str().expect("utf-8 path");
let output = run_cli(&[
"action",
"info",
"/demo/calibrate",
"--report",
status_arg,
"--refresh-demo",
"--json",
]);
assert!(output.status.success(), "stderr: {}", output.stderr);
let payload: Value = serde_json::from_str(&output.stdout).expect("parse action info json");
assert_exact_object_keys(
&payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_exact_object_keys(
payload.get("query").expect("query object"),
&["action_name"],
);
assert_exact_object_keys(
payload.get("result").expect("result object"),
&["name", "endpoints", "health"],
);
assert_exact_object_keys(
payload["result"]
.get("endpoints")
.expect("endpoints object"),
&["clients", "servers"],
);
assert_exact_object_keys(
payload["result"].get("health").expect("health object"),
&[
"current_state",
"active_goals",
"health_state",
"heartbeat_timeout_ms",
"last_heartbeat_at_unix_nanos",
"last_feedback_at_unix_nanos",
"last_result_at_unix_nanos",
],
);
}
#[test]
fn obs_export_prometheus_json_contract_has_stable_shape() {
let status = temp_path("obs-export-contract", "json");
let status_arg = status.to_str().expect("utf-8 path");
let output = run_cli(&[
"obs",
"export",
"--report",
status_arg,
"--refresh-demo",
"--format",
"prometheus",
"--alert-template",
"basic",
"--json",
]);
assert!(output.status.success(), "stderr: {}", output.stderr);
let payload: Value = serde_json::from_str(&output.stdout).expect("parse obs export json");
assert_exact_object_keys(
&payload,
&[
"api_version",
"kind",
"captured_at_unix_nanos",
"source",
"query",
"result",
],
);
assert_eq!(
payload.get("api_version").and_then(Value::as_str),
Some("robotrt.obs.export.v1")
);
assert_exact_object_keys(
payload.get("source").expect("source object"),
&["mode", "service", "endpoint", "timeout_ms"],
);
assert_exact_object_keys(
payload.get("query").expect("query object"),
&["format", "alert_template"],
);
assert_exact_object_keys(
payload.get("result").expect("result object"),
&["metrics", "alert_template"],
);
let metrics = payload["result"]["metrics"]
.as_str()
.expect("prometheus metrics should be string");
assert!(metrics.contains("robotrt_runtime_topics_count"));
assert!(metrics.contains("robotrt_health_overall"));
let template = payload["result"]["alert_template"]
.as_str()
.expect("alert template should be string");
assert!(template.contains("RobotRTUnhealthy"));
}
#[test]
fn obs_export_remote_endpoint_supports_otel_json() {
let endpoint = allocate_udp_endpoint();
let mut server = Command::new(env!("CARGO_BIN_EXE_robotrt-cli"))
.args([
"gateway",
"serve",
"--bind",
endpoint.as_str(),
"--source",
"demo",
"--once",
])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn robotrt-cli gateway serve");
std::thread::sleep(Duration::from_millis(120));
let mut output = run_cli(&[
"obs",
"export",
"--endpoint",
endpoint.as_str(),
"--timeout-ms",
"2000",
"--format",
"otel",
"--json",
]);
for _ in 0..4 {
if output.status.success() {
break;
}
std::thread::sleep(Duration::from_millis(150));
output = run_cli(&[
"obs",
"export",
"--endpoint",
endpoint.as_str(),
"--timeout-ms",
"2000",
"--format",
"otel",
"--json",
]);
}
assert!(output.status.success(), "stderr: {}", output.stderr);
let payload: Value = serde_json::from_str(&output.stdout).expect("parse obs export json");
assert_eq!(payload["source"]["mode"].as_str(), Some("remote_service"));
assert_eq!(
payload["source"]["service"].as_str(),
Some("/robotrt/gateway/query")
);
assert_eq!(
payload["result"]["metrics"]["schema"].as_str(),
Some("robotrt.obs.otel_metrics.v1")
);
let status = server.wait().expect("wait gateway server child");
assert!(
status.success(),
"gateway serve child exited with {status:?}"
);
}