mod common;
use common::graph;
use graphrefly_core::{EqualsMode, FnId, HandleId};
use graphrefly_graph::{GraphProfileOptions, OrphanKind};
#[test]
fn nodecount_edgecount_subgraphcount_match_topology() {
let (rt, g) = graph("root");
let a = g.state(rt.core(), "a", Some(HandleId::new(1))).unwrap();
let b = g.state(rt.core(), "b", Some(HandleId::new(2))).unwrap();
g.derived(
rt.core(),
"sum",
&[a, b],
FnId::new(1),
EqualsMode::Identity,
)
.unwrap();
let _child = g
.mount_new(rt.core(), "child".to_string())
.expect("mount_new");
let profile = g.resource_profile(rt.core(), None);
assert_eq!(profile.node_count, 3, "a + b + sum");
assert_eq!(profile.edge_count, 2, "a→sum + b→sum");
assert_eq!(profile.subgraph_count, 1, "one mount");
}
#[test]
fn per_node_subscriber_count_reflects_active_subscriptions() {
use std::rc::Rc;
let (rt, g) = graph("root");
let a = g.state(rt.core(), "a", Some(HandleId::new(1))).unwrap();
let p0 = g.resource_profile(rt.core(), None);
let a0 = p0.nodes.iter().find(|n| n.path == "a").expect("a in nodes");
assert_eq!(a0.subscriber_count, 0);
let sink: graphrefly_core::Sink = Rc::new(|_msgs: &[_]| {});
let sub_id = rt.core().subscribe(a, sink);
let p1 = g.resource_profile(rt.core(), None);
let a1 = p1.nodes.iter().find(|n| n.path == "a").unwrap();
assert!(
a1.subscriber_count >= 1,
"expected >= 1 subscriber, got {}",
a1.subscriber_count
);
rt.core().unsubscribe(a, sub_id);
let p2 = g.resource_profile(rt.core(), None);
let a2 = p2.nodes.iter().find(|n| n.path == "a").unwrap();
assert_eq!(a2.subscriber_count, 0);
}
#[test]
fn orphan_detection_categorizes_idle_derived_and_excludes_state() {
let (rt, g) = graph("root");
let src = g
.state(rt.core(), "source", Some(HandleId::new(0)))
.unwrap();
g.derived(
rt.core(),
"idle_d",
&[src],
FnId::new(1),
EqualsMode::Identity,
)
.unwrap();
g.derived(
rt.core(),
"idle_d2",
&[src],
FnId::new(2),
EqualsMode::Identity,
)
.unwrap();
let profile = g.resource_profile(rt.core(), None);
let orphan_paths: std::collections::HashSet<&str> =
profile.orphans.iter().map(|p| p.path.as_str()).collect();
assert!(orphan_paths.contains("idle_d"), "idle_d in orphans");
assert!(orphan_paths.contains("idle_d2"), "idle_d2 in orphans");
let idle_d = profile.orphans.iter().find(|p| p.path == "idle_d").unwrap();
assert_eq!(idle_d.orphan_kind, Some(OrphanKind::IdleDerived));
assert!(!idle_d.is_orphan_effect, "derived is not an effect");
assert!(
!orphan_paths.contains("source"),
"state node should not be in orphans"
);
let source = profile
.nodes
.iter()
.find(|n| n.path == "source")
.expect("source in nodes");
assert_eq!(source.r#type, "state");
assert_eq!(source.subscriber_count, 0, "R2.2.6 lazy activation");
assert!(
source.orphan_kind.is_none(),
"state is never categorized as orphan"
);
}
#[test]
fn hotspots_sorted_descending_and_capped_by_top_n() {
use std::rc::Rc;
let (rt, g) = graph("root");
let mut nodes = Vec::new();
for i in 0..5 {
nodes.push(
g.state(rt.core(), format!("n{i}"), Some(HandleId::new(i)))
.unwrap(),
);
}
let mut subs = Vec::new();
for _ in 0..3 {
let sink: graphrefly_core::Sink = Rc::new(|_msgs: &[_]| {});
subs.push((nodes[0], rt.core().subscribe(nodes[0], sink)));
}
for _ in 0..2 {
let sink: graphrefly_core::Sink = Rc::new(|_msgs: &[_]| {});
subs.push((nodes[1], rt.core().subscribe(nodes[1], sink)));
}
let profile = g.resource_profile(rt.core(), Some(GraphProfileOptions { top_n: Some(2) }));
assert!(profile.hotspots.by_subscriber_count.len() <= 2);
let top = &profile.hotspots.by_subscriber_count;
assert!(top[0].subscriber_count >= top[1].subscriber_count);
assert_eq!(top[0].path, "n0");
assert_eq!(top[0].subscriber_count, 3);
for (id, sub_id) in subs {
rt.core().unsubscribe(id, sub_id);
}
}
#[test]
fn default_top_n_is_ten() {
let (rt, g) = graph("root");
for i in 0..12 {
g.state(rt.core(), format!("n{i}"), Some(HandleId::new(i)))
.unwrap();
}
let profile = g.resource_profile(rt.core(), None);
assert_eq!(profile.node_count, 12);
assert_eq!(
profile.hotspots.by_subscriber_count.len(),
10,
"default top_n = 10"
);
assert_eq!(profile.hotspots.by_dep_count.len(), 10);
}
#[test]
fn d284_amendment_no_value_size_fields_in_serde_output() {
let (rt, g) = graph("root");
g.state(rt.core(), "a", Some(HandleId::new(1))).unwrap();
let profile = g.resource_profile(rt.core(), None);
let json_str = serde_json::to_string(&profile).unwrap();
assert!(
!json_str.contains("value_size") && !json_str.contains("valueSize"),
"D284 amendment: no value-size fields, got: {json_str}"
);
assert!(
!json_str.contains("by_value_size") && !json_str.contains("byValueSize"),
"D284 amendment: no by_value_size hotspot, got: {json_str}"
);
}
#[test]
fn d286_napi_camel_case_json_keys() {
let (rt, g) = graph("root");
let a = g.state(rt.core(), "a", Some(HandleId::new(1))).unwrap();
g.derived(rt.core(), "d", &[a], FnId::new(1), EqualsMode::Identity)
.unwrap();
let profile = g.resource_profile(rt.core(), None);
let json_str = serde_json::to_string(&profile).unwrap();
assert!(json_str.contains("\"nodeCount\""), "got: {json_str}");
assert!(json_str.contains("\"edgeCount\""), "got: {json_str}");
assert!(json_str.contains("\"subgraphCount\""), "got: {json_str}");
assert!(json_str.contains("\"orphanEffects\""), "got: {json_str}");
assert!(
json_str.contains("\"bySubscriberCount\""),
"got: {json_str}"
);
assert!(json_str.contains("\"byDepCount\""), "got: {json_str}");
assert!(json_str.contains("\"subscriberCount\""), "got: {json_str}");
assert!(json_str.contains("\"depCount\""), "got: {json_str}");
assert!(json_str.contains("\"isOrphanEffect\""), "got: {json_str}");
assert!(json_str.contains("\"orphanKind\""), "got: {json_str}");
assert!(json_str.contains("\"idle-derived\""), "got: {json_str}");
for snake in [
"\"node_count\"",
"\"edge_count\"",
"\"subgraph_count\"",
"\"subscriber_count\"",
"\"dep_count\"",
"\"is_orphan_effect\"",
"\"orphan_kind\"",
"\"by_subscriber_count\"",
"\"by_dep_count\"",
"\"orphan_effects\"",
] {
assert!(
!json_str.contains(snake),
"snake_case key {snake} leaked: {json_str}"
);
}
}