2026-06-09 09:29:07 +00:00
|
|
|
//! Dashboard render contract: source records → frames → reconstructed views.
|
|
|
|
|
//!
|
|
|
|
|
//! Feeds the real `FleetView` consumer a scripted set of one node's frames and
|
|
|
|
|
//! checks the `FleetUpdate` it produces carries the migrated metrics: the
|
|
|
|
|
//! distribution panels show real cache / registry / directory / peer-auth
|
2026-06-23 15:42:28 +00:00
|
|
|
//! values, and the Actors table shows the real per-actor rows. Every expectation
|
|
|
|
|
//! is derived from the input records, never read back from the consumer, and the
|
|
|
|
|
//! test asserts on reconstructed JSON / stats — not on rendered HTML — so it
|
2026-06-09 09:29:07 +00:00
|
|
|
//! survives a UI refactor.
|
|
|
|
|
|
|
|
|
|
use dashboard::datastream_source::{FleetUpdate, FleetView};
|
2026-06-23 15:42:28 +00:00
|
|
|
use dashboard::telemetry::{ActorRec, ActorRuntimeDetail, IdentityRecord};
|
|
|
|
|
use datastream::Record;
|
2026-06-09 09:29:07 +00:00
|
|
|
use datastream::frame::{Frame, Lifetime, NodeId, Position, StreamId};
|
2026-06-23 15:42:28 +00:00
|
|
|
use distribution::telemetry::DistributionState;
|
2026-06-09 09:29:07 +00:00
|
|
|
|
|
|
|
|
fn node_stream() -> StreamId {
|
|
|
|
|
StreamId::new(NodeId::new(&"ab".repeat(32)), Lifetime(1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn frame<R: Record>(record: &R, pos: u64) -> Frame {
|
|
|
|
|
Frame::new(R::channel(), Position(pos), record.encode())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn identity(stream: &StreamId, node_name: &str) -> IdentityRecord {
|
|
|
|
|
IdentityRecord {
|
|
|
|
|
node: stream.node.as_str().to_string(),
|
|
|
|
|
life: stream.life.0,
|
|
|
|
|
node_name: node_name.to_string(),
|
|
|
|
|
listen_addr: "host:4242".to_string(),
|
|
|
|
|
relay_url: String::new(),
|
|
|
|
|
version: "v1".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fold a script of frames for one node and return the final `FleetUpdate`.
|
|
|
|
|
fn ingest_all(view: &mut FleetView, stream: &StreamId, frames: &[Frame]) -> FleetUpdate {
|
|
|
|
|
let mut last = None;
|
|
|
|
|
for f in frames {
|
|
|
|
|
last = Some(view.ingest(stream, f));
|
|
|
|
|
}
|
|
|
|
|
last.expect("at least one frame")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn distribution_panels_render_real_state_from_frames() {
|
|
|
|
|
let stream = node_stream();
|
|
|
|
|
let mut view = FleetView::new(None);
|
|
|
|
|
|
|
|
|
|
let dist = DistributionState {
|
|
|
|
|
cache_size: 4,
|
|
|
|
|
directory_route_count: 9,
|
|
|
|
|
registry_size: 6,
|
|
|
|
|
registry_tombstones: 1,
|
|
|
|
|
peer_auth_mode: "allow-list".into(),
|
|
|
|
|
authorized_peer_count: 3,
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let frames = vec![
|
|
|
|
|
frame(&identity(&stream, "swift-falcon"), 0),
|
|
|
|
|
frame(&dist, 1),
|
|
|
|
|
];
|
|
|
|
|
let update = ingest_all(&mut view, &stream, &frames);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let dj: serde_json::Value = serde_json::from_str(
|
|
|
|
|
update
|
|
|
|
|
.dist_json
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("dist json for selected node"),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-06-09 09:29:07 +00:00
|
|
|
// The migrated distribution metrics arrive as real values (not the zeros the
|
|
|
|
|
// pre-datastream reconstruction used to fill).
|
|
|
|
|
assert_eq!(dj["cache_size"], 4);
|
|
|
|
|
assert_eq!(dj["directory_route_count"], 9);
|
|
|
|
|
assert_eq!(dj["registry_size"], 6);
|
|
|
|
|
assert_eq!(dj["registry_tombstones"], 1);
|
|
|
|
|
assert_eq!(dj["peer_auth_mode"], "allow-list");
|
|
|
|
|
assert_eq!(dj["authorized_peer_count"], 3);
|
|
|
|
|
// Identity extras ride the identity channel.
|
|
|
|
|
assert_eq!(dj["node_name"], "swift-falcon");
|
|
|
|
|
assert_eq!(dj["listen_addr"], "host:4242");
|
|
|
|
|
assert_eq!(dj["version"], "v1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn actors_table_renders_real_per_actor_rows() {
|
|
|
|
|
let stream = node_stream();
|
|
|
|
|
let mut view = FleetView::new(None);
|
|
|
|
|
|
|
|
|
|
let detail = ActorRuntimeDetail {
|
|
|
|
|
actors: vec![
|
|
|
|
|
ActorRec {
|
|
|
|
|
address: "ab".repeat(32),
|
|
|
|
|
name: "SwimActor".into(),
|
|
|
|
|
mailbox_depth: 2,
|
|
|
|
|
messages_processed: 42,
|
|
|
|
|
last_msg_type: "Ping".into(),
|
|
|
|
|
poisoned: false,
|
|
|
|
|
message_type_counts: vec![("Ping".into(), 42)],
|
|
|
|
|
},
|
|
|
|
|
ActorRec {
|
|
|
|
|
address: "cd".repeat(32),
|
|
|
|
|
name: "RegistryActor".into(),
|
|
|
|
|
mailbox_depth: 0,
|
|
|
|
|
messages_processed: 7,
|
|
|
|
|
last_msg_type: "Tick".into(),
|
|
|
|
|
poisoned: true,
|
|
|
|
|
message_type_counts: vec![],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
let frames = vec![frame(&identity(&stream, "node"), 0), frame(&detail, 1)];
|
|
|
|
|
let update = ingest_all(&mut view, &stream, &frames);
|
|
|
|
|
let stats = update.stats.expect("stats for the selected node");
|
|
|
|
|
|
|
|
|
|
// The real per-actor rows are rendered (not synthetic per-channel rows): the
|
|
|
|
|
// names, message tallies, and poisoned flag carry through unchanged.
|
|
|
|
|
let swim = stats
|
|
|
|
|
.actor_details
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.name.as_deref() == Some("SwimActor"))
|
|
|
|
|
.expect("SwimActor row");
|
|
|
|
|
assert_eq!(swim.messages_processed, 42);
|
|
|
|
|
assert_eq!(swim.mailbox_depth, 2);
|
|
|
|
|
assert!(!swim.poisoned);
|
|
|
|
|
|
|
|
|
|
let registry = stats
|
|
|
|
|
.actor_details
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.name.as_deref() == Some("RegistryActor"))
|
|
|
|
|
.expect("RegistryActor row");
|
|
|
|
|
assert!(registry.poisoned, "poisoned flag carried through");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-23 15:42:28 +00:00
|
|
|
fn dashboard_ignores_unknown_plugin_channel_without_corrupting_stream_state() {
|
2026-06-09 09:29:07 +00:00
|
|
|
let stream = node_stream();
|
|
|
|
|
let mut view = FleetView::new(None);
|
|
|
|
|
let frames = vec![
|
|
|
|
|
frame(&identity(&stream, "node"), 0),
|
2026-06-23 15:42:28 +00:00
|
|
|
Frame::new(
|
|
|
|
|
"external.plugin.sample",
|
|
|
|
|
Position(1),
|
|
|
|
|
br#"{"value":42}"#.to_vec(),
|
|
|
|
|
),
|
2026-06-09 09:29:07 +00:00
|
|
|
];
|
2026-06-23 15:42:28 +00:00
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
let update = ingest_all(&mut view, &stream, &frames);
|
2026-06-23 15:42:28 +00:00
|
|
|
let fleet: serde_json::Value = serde_json::from_str(&update.fleet_json).unwrap();
|
|
|
|
|
let rows = fleet["nodes"].as_array().expect("fleet rows");
|
2026-06-09 09:29:07 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
assert_eq!(rows.len(), 1);
|
|
|
|
|
assert_eq!(rows[0]["id"], stream.node.as_str());
|
2026-06-09 09:29:07 +00:00
|
|
|
}
|