use somatize_compiler::{CompileMode, DiagnosticLevel, SimpleNodeRegistry, compile};
use somatize_core::cache::CacheKey;
use somatize_core::filter::{Distribution, FilterKind, FilterMeta, StreamMode};
use somatize_core::graph::{Edge, Graph, Node};
use somatize_core::schema::{DataType, Schema};
use somatize_core::step::StepMeta;
fn filter_meta(name: &str, input: Option<Schema>, output: Option<Schema>) -> FilterMeta {
FilterMeta {
name: name.into(),
kind: FilterKind::Stateless,
cacheable: true,
differentiable: false,
deterministic: true,
stream_mode: StreamMode::FixedState,
distribution: Distribution::Local,
input_schema: input,
output_schema: output,
}
}
fn key(id: &str) -> CacheKey {
CacheKey::from_parts(&[id.as_bytes()])
}
#[test]
fn text_flows_into_a_conversation() {
assert!(DataType::Utf8.can_coerce_to(&DataType::Messages));
assert!(DataType::Messages.can_coerce_to(&DataType::Utf8));
}
#[test]
fn json_is_the_dynamic_type() {
for dt in [
DataType::Float64,
DataType::Utf8,
DataType::Messages,
DataType::Bytes,
] {
assert!(dt.can_coerce_to(&DataType::Json), "{dt} → json");
assert!(DataType::Json.can_coerce_to(&dt), "json → {dt}");
}
}
#[test]
fn numeric_widths_are_coercible_to_each_other() {
assert!(DataType::Float32.can_coerce_to(&DataType::Float64));
assert!(DataType::Int64.can_coerce_to(&DataType::Float64));
}
#[test]
fn tensors_and_conversations_do_not_mix() {
assert!(!DataType::Float64.can_coerce_to(&DataType::Messages));
assert!(!DataType::Messages.can_coerce_to(&DataType::Float64));
assert!(!DataType::Float64.can_coerce_to(&DataType::Utf8));
assert!(!DataType::Bytes.can_coerce_to(&DataType::Messages));
}
#[test]
fn an_impossible_edge_is_a_compile_error() {
let mut g = Graph::new();
g.add_node(Node::filter_with_id("encoder", "encoder"));
g.add_node(Node::step("agent", "Agent"));
g.add_edge(Edge::data("e", "encoder", "agent"));
let mut reg = SimpleNodeRegistry::new();
reg.register_meta(
"encoder",
filter_meta(
"encoder",
None,
Some(Schema::vector(DataType::Float64, 128)),
),
key("encoder"),
);
reg.register_step_meta(
"agent",
StepMeta::new("Agent").with_input_schema(Schema::messages()),
);
let err = compile(&g, ®, CompileMode::Inference, None)
.expect_err("a tensor cannot be a conversation");
let msg = err.to_string();
assert!(msg.contains("encoder"), "{msg}");
assert!(msg.contains("agent"), "{msg}");
assert!(msg.contains("no conversion"), "{msg}");
}
#[test]
fn a_step_output_meeting_a_tensor_input_is_rejected() {
let mut g = Graph::new();
g.add_node(Node::step("agent", "Agent"));
g.add_node(Node::filter_with_id("classifier", "classifier"));
g.add_edge(Edge::data("e", "agent", "classifier"));
let mut reg = SimpleNodeRegistry::new();
reg.register_step_meta(
"agent",
StepMeta::new("Agent").with_output_schema(Schema::messages()),
);
reg.register_meta(
"classifier",
filter_meta(
"classifier",
Some(Schema::vector(DataType::Float64, 10)),
None,
),
key("classifier"),
);
let err =
compile(&g, ®, CompileMode::Inference, None).expect_err("messages are not a tensor");
assert!(err.to_string().contains("no conversion"), "{err}");
}
#[test]
fn text_into_a_conversation_compiles_without_complaint() {
let mut g = Graph::new();
g.add_node(Node::filter_with_id("prompt", "prompt"));
g.add_node(Node::step("agent", "Agent"));
g.add_edge(Edge::data("e", "prompt", "agent"));
let mut reg = SimpleNodeRegistry::new();
reg.register_meta(
"prompt",
filter_meta("prompt", None, Some(Schema::text())),
key("prompt"),
);
reg.register_step_meta(
"agent",
StepMeta::new("Agent").with_input_schema(Schema::messages()),
);
let result = compile(&g, ®, CompileMode::Inference, None).expect("compiles");
assert!(
result
.diagnostics
.iter()
.all(|d| d.level != DiagnosticLevel::Warning || !d.message.contains("schema")),
"a legitimate coercion produced a schema warning: {:?}",
result.diagnostics
);
}
#[test]
fn a_plausible_mismatch_is_still_only_a_warning() {
let mut g = Graph::new();
g.add_node(Node::filter_with_id("a", "a"));
g.add_node(Node::filter_with_id("b", "b"));
g.add_edge(Edge::data("e", "a", "b"));
let mut reg = SimpleNodeRegistry::new();
reg.register_meta(
"a",
filter_meta("a", None, Some(Schema::vector(DataType::Float32, 8))),
key("a"),
);
reg.register_meta(
"b",
filter_meta("b", Some(Schema::vector(DataType::Float64, 8)), None),
key("b"),
);
let result = compile(&g, ®, CompileMode::Inference, None).expect("still compiles");
assert!(
result
.diagnostics
.iter()
.any(|d| d.message.contains("schema mismatch")),
"expected a warning, got {:?}",
result.diagnostics
);
}
#[test]
fn an_edge_labelling_an_undeclared_arm_is_rejected() {
let mut g = Graph::new();
g.add_node(Node::branch_over("router", ["billing", "tech"]));
g.add_node(Node::filter_with_id("billing", "billing"));
g.add_node(Node::filter_with_id("tech", "tech"));
g.add_edge(Edge::control("e1", "router", "billing").with_label("billing"));
g.add_edge(Edge::control("e2", "router", "tech").with_label("tec"));
let mut reg = SimpleNodeRegistry::new();
for id in ["billing", "tech"] {
reg.register_meta(id, filter_meta(id, None, None), key(id));
}
let err = compile(&g, ®, CompileMode::Inference, None).expect_err("typo in an arm label");
let msg = err.to_string();
assert!(msg.contains("tec"), "should name the bad label: {msg}");
assert!(
msg.contains("billing"),
"should list the declared arms: {msg}"
);
}
#[test]
fn a_declared_arm_without_an_edge_is_rejected() {
let mut g = Graph::new();
g.add_node(Node::branch_over("router", ["billing", "tech"]));
g.add_node(Node::filter_with_id("billing", "billing"));
g.add_edge(Edge::control("e1", "router", "billing").with_label("billing"));
let mut reg = SimpleNodeRegistry::new();
reg.register_meta(
"billing",
filter_meta("billing", None, None),
key("billing"),
);
let err = compile(&g, ®, CompileMode::Inference, None).expect_err("unreachable arm");
assert!(err.to_string().contains("tech"), "{err}");
}
#[test]
fn declared_arms_matching_their_edges_compile() {
let mut g = Graph::new();
g.add_node(Node::branch_over("router", ["billing", "tech"]));
g.add_node(Node::filter_with_id("billing", "billing"));
g.add_node(Node::filter_with_id("tech", "tech"));
g.add_edge(Edge::control("e1", "router", "billing").with_label("billing"));
g.add_edge(Edge::control("e2", "router", "tech").with_label("tech"));
let mut reg = SimpleNodeRegistry::new();
for id in ["billing", "tech"] {
reg.register_meta(id, filter_meta(id, None, None), key(id));
}
compile(&g, ®, CompileMode::Inference, None).expect("compiles");
}
#[test]
fn a_default_arm_needs_no_declaration() {
let mut g = Graph::new();
g.add_node(Node::branch_over("router", ["billing"]));
g.add_node(Node::filter_with_id("billing", "billing"));
g.add_node(Node::filter_with_id("other", "other"));
g.add_edge(Edge::control("e1", "router", "billing").with_label("billing"));
g.add_edge(Edge::control("e2", "router", "other").with_label("default"));
let mut reg = SimpleNodeRegistry::new();
for id in ["billing", "other"] {
reg.register_meta(id, filter_meta(id, None, None), key(id));
}
compile(&g, ®, CompileMode::Inference, None).expect("compiles");
}
#[test]
fn undeclared_arms_are_still_inferred() {
let mut g = Graph::new();
g.add_node(Node::branch("router"));
g.add_node(Node::filter_with_id("a", "a"));
g.add_node(Node::filter_with_id("b", "b"));
g.add_edge(Edge::control("e1", "router", "a").with_label("a"));
g.add_edge(Edge::control("e2", "router", "b").with_label("b"));
let mut reg = SimpleNodeRegistry::new();
for id in ["a", "b"] {
reg.register_meta(id, filter_meta(id, None, None), key(id));
}
compile(&g, ®, CompileMode::Inference, None).expect("compiles");
}