mod common;
use common::graph;
use graphrefly_core::{EqualsMode, FnId, HandleId};
use graphrefly_graph::{DescribeValue, NodeStatus, NodeTypeStr};
#[test]
fn empty_graph_describes_as_empty() {
let (rt, g) = graph("system");
let d = g.describe(rt.core());
assert_eq!(d.name, "system");
assert!(d.nodes.is_empty());
assert!(d.edges.is_empty());
assert!(d.subgraphs.is_empty());
}
#[test]
fn state_node_status_sentinel() {
let (rt, g) = graph("system");
g.state(rt.core(), "knob", None).unwrap();
let d = g.describe(rt.core());
let n = d.nodes.get("knob").unwrap();
assert!(matches!(n.r#type, NodeTypeStr::State));
assert_eq!(n.status, NodeStatus::Sentinel);
assert!(n.value.is_none());
assert!(n.deps.is_empty());
}
#[test]
fn state_node_status_settled_when_initial_present() {
let (rt, g) = graph("system");
g.state(rt.core(), "retry_limit", Some(HandleId::new(3)))
.unwrap();
let d = g.describe(rt.core());
let n = d.nodes.get("retry_limit").unwrap();
assert_eq!(n.status, NodeStatus::Settled);
assert_eq!(n.value, Some(DescribeValue::Handle(HandleId::new(3))));
}
#[test]
fn derived_node_status_pending_before_first_fire() {
let (rt, g) = graph("system");
let s = g.state(rt.core(), "a", None).unwrap(); g.derived(rt.core(), "d", &[s], FnId::new(1), EqualsMode::Identity)
.unwrap();
let d = g.describe(rt.core());
let nd = d.nodes.get("d").unwrap();
assert!(matches!(nd.r#type, NodeTypeStr::Derived));
assert_eq!(nd.status, NodeStatus::Pending);
}
#[test]
fn derived_node_status_settled_after_dep_fires() {
let (rt, g) = graph("system");
let s = g.state(rt.core(), "a", Some(HandleId::new(7))).unwrap();
g.derived(rt.core(), "d", &[s], FnId::new(1), EqualsMode::Identity)
.unwrap();
let did = g.node("d");
let sub = g.subscribe(rt.core(), did, std::sync::Arc::new(|_msgs| {}));
let d = g.describe(rt.core());
let nd = d.nodes.get("d").unwrap();
assert_eq!(nd.status, NodeStatus::Settled);
assert_eq!(nd.value, Some(DescribeValue::Handle(HandleId::new(7)))); assert_eq!(nd.deps, vec!["a"]);
g.unsubscribe(rt.core(), did, sub);
}
#[test]
fn complete_node_surfaces_completed_status() {
let (rt, g) = graph("system");
let s = g.state(rt.core(), "s", Some(HandleId::new(1))).unwrap();
g.complete(rt.core(), s);
let d = g.describe(rt.core());
assert_eq!(d.nodes.get("s").unwrap().status, NodeStatus::Completed);
}
#[test]
fn errored_node_surfaces_errored_status() {
let (rt, g) = graph("system");
let s = g.state(rt.core(), "s", Some(HandleId::new(1))).unwrap();
g.error(rt.core(), s, HandleId::new(99));
let d = g.describe(rt.core());
assert_eq!(d.nodes.get("s").unwrap().status, NodeStatus::Errored);
}
#[test]
fn edges_emitted_in_dep_order_per_consumer() {
let (rt, g) = graph("system");
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(), "c", &[a, b], FnId::new(1), EqualsMode::Identity)
.unwrap();
let d = g.describe(rt.core());
assert_eq!(d.edges.len(), 2);
assert_eq!(d.edges[0].from, "a");
assert_eq!(d.edges[0].to, "c");
assert_eq!(d.edges[1].from, "b");
assert_eq!(d.edges[1].to, "c");
}
#[test]
fn unnamed_dep_surfaces_as_anon_name() {
let (rt, g) = graph("system");
let raw = rt.core().register_state(HandleId::new(5), false).unwrap();
let named = g.state(rt.core(), "named", Some(HandleId::new(7))).unwrap();
g.derived(
rt.core(),
"d",
&[raw, named],
FnId::new(1),
EqualsMode::Identity,
)
.unwrap();
let d = g.describe(rt.core());
let nd = d.nodes.get("d").unwrap();
assert_eq!(nd.deps[0], format!("_anon_{}", raw.raw()));
assert_eq!(nd.deps[1], "named");
assert!(d
.edges
.iter()
.any(|e| e.from == format!("_anon_{}", raw.raw()) && e.to == "d"));
}
#[test]
fn subgraphs_field_lists_mounted_children() {
let (rt, parent) = graph("system");
parent.mount_new(rt.core(), "payment").unwrap();
parent.mount_new(rt.core(), "auth").unwrap();
let d = parent.describe(rt.core());
assert_eq!(d.subgraphs, vec!["payment", "auth"]);
assert!(d.nodes.is_empty());
}
#[test]
fn describe_serializes_to_json_string() {
let (rt, g) = graph("system");
g.state(rt.core(), "x", Some(HandleId::new(42))).unwrap();
let d = g.describe(rt.core());
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("\"name\":\"system\""));
assert!(json.contains("\"x\""));
assert!(json.contains("\"value\":42"));
assert!(json.contains("\"type\":\"state\""));
}
#[test]
fn describe_meta_field_omitted_when_none() {
let (rt, g) = graph("system");
g.state(rt.core(), "x", Some(HandleId::new(1))).unwrap();
let d = g.describe(rt.core());
let n = d.nodes.get("x").unwrap();
assert!(n.meta.is_none());
let json = serde_json::to_string(&d).unwrap();
assert!(!json.contains("\"meta\""));
}
#[test]
fn describe_meta_field_round_trips_via_json() {
let (rt, g) = graph("system");
g.state(rt.core(), "x", Some(HandleId::new(1))).unwrap();
let mut d = g.describe(rt.core());
d.nodes.get_mut("x").unwrap().meta = Some(serde_json::json!({ "description": "knob" }));
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("\"meta\":{\"description\":\"knob\"}"));
}
#[test]
fn describe_node_order_matches_namespace_insertion_order() {
let (rt, g) = graph("system");
g.state(rt.core(), "z", None).unwrap();
g.state(rt.core(), "a", None).unwrap();
g.state(rt.core(), "m", None).unwrap();
let d = g.describe(rt.core());
let names: Vec<&str> = d.nodes.keys().map(String::as_str).collect();
assert_eq!(names, vec!["z", "a", "m"]);
}
mod debug_render {
use std::collections::HashMap;
use std::sync::Arc;
use graphrefly_core::{BindingBoundary, FnResult, HandleId, OwnedCore};
use graphrefly_graph::{DebugBindingBoundary, DescribeValue, Graph};
use parking_lot::Mutex;
struct DebugBinding {
values: Mutex<HashMap<HandleId, serde_json::Value>>,
}
impl DebugBinding {
fn new() -> Arc<Self> {
Arc::new(Self {
values: Mutex::new(HashMap::new()),
})
}
fn register(&self, h: HandleId, v: serde_json::Value) {
self.values.lock().insert(h, v);
}
}
impl BindingBoundary for DebugBinding {
fn invoke_fn(
&self,
_node_id: graphrefly_core::NodeId,
_fn_id: graphrefly_core::FnId,
dep_data: &[graphrefly_core::DepBatch],
) -> FnResult {
let h = dep_data
.first()
.map_or(HandleId::new(0), graphrefly_core::DepBatch::latest);
FnResult::Data {
handle: h,
tracked: None,
}
}
fn custom_equals(&self, _f: graphrefly_core::FnId, a: HandleId, b: HandleId) -> bool {
a == b
}
fn release_handle(&self, _h: HandleId) {}
}
impl DebugBindingBoundary for DebugBinding {
fn handle_to_debug(&self, handle: HandleId) -> serde_json::Value {
self.values
.lock()
.get(&handle)
.cloned()
.unwrap_or(serde_json::Value::Null)
}
}
#[test]
fn describe_with_debug_renders_value_via_binding() {
let binding = DebugBinding::new();
let h42 = HandleId::new(42);
binding.register(h42, serde_json::json!(3));
let rt = OwnedCore::new(binding.clone() as Arc<dyn BindingBoundary>);
let g = Graph::new("system");
g.state(rt.core(), "retry_limit", Some(h42)).unwrap();
let default = g.describe(rt.core());
assert_eq!(
default.nodes.get("retry_limit").unwrap().value,
Some(DescribeValue::Handle(h42))
);
let default_json = serde_json::to_string(&default).unwrap();
assert!(
default_json.contains("\"value\":42"),
"default describe() emits raw u64 handle. JSON: {default_json}"
);
let rendered = g.describe_with_debug(rt.core(), binding.as_ref());
assert_eq!(
rendered.nodes.get("retry_limit").unwrap().value,
Some(DescribeValue::Rendered(serde_json::json!(3)))
);
let rendered_json = serde_json::to_string(&rendered).unwrap();
assert!(
rendered_json.contains("\"value\":3"),
"describe_with_debug emits binding-rendered value. JSON: {rendered_json}"
);
}
#[test]
fn describe_with_debug_preserves_null_for_sentinel_cache() {
let binding = DebugBinding::new();
let rt = OwnedCore::new(binding.clone() as Arc<dyn BindingBoundary>);
let g = Graph::new("system");
g.state(rt.core(), "knob", None).unwrap();
let rendered = g.describe_with_debug(rt.core(), binding.as_ref());
assert!(rendered.nodes.get("knob").unwrap().value.is_none());
let json = serde_json::to_string(&rendered).unwrap();
assert!(
json.contains("\"value\":null"),
"sentinel cache → value:null. JSON: {json}"
);
}
#[test]
fn describe_with_debug_supports_complex_json_shapes() {
let binding = DebugBinding::new();
let h1 = HandleId::new(1);
binding.register(
h1,
serde_json::json!({ "kind": "tuple", "items": [1, 2, 3] }),
);
let rt = OwnedCore::new(binding.clone() as Arc<dyn BindingBoundary>);
let g = Graph::new("system");
g.state(rt.core(), "payload", Some(h1)).unwrap();
let rendered = g.describe_with_debug(rt.core(), binding.as_ref());
let v = rendered.nodes.get("payload").unwrap().value.clone();
assert_eq!(
v,
Some(DescribeValue::Rendered(
serde_json::json!({ "kind": "tuple", "items": [1, 2, 3] })
)),
"complex shape round-trips through describe_with_debug"
);
}
}