flowrs-engine/crates/flowrs-core/tests/parameter_values.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

117 lines
4.7 KiB
Rust

//! What a stored parameter value MEANS must be the same to the validator, to T0 and to
//! the generated program.
//!
//! Two places disagreed. `check_parameters` trimmed a value before checking it, so `" 1"`
//! was a valid number — and every reader parsed it untrimmed, failed, and quietly used the
//! default. And `check_parameters` read an empty value as "use the default", while the
//! editor stored `""` on a cleared field and the readers took `""` as the value.
use flowrs_core::graph::diagnostics::check_parameters;
use flowrs_core::graph::executor::GraphExecutor;
use flowrs_core::graph::value::Value;
use flowrs_core::graph::widget::{ExecContext, WidgetRegistry, stored_value};
use flowrs_core::{Graph, NodeId};
use std::collections::HashMap;
/// An `ai.complete` whose temperature is `temperature`. With nothing to answer it, the
/// local completer replies, and a non-zero temperature rotates the prompt's words — so
/// the temperature the node actually read is visible in its output.
fn complete_at(temperature: &str) -> (Graph, NodeId) {
let mut g = Graph::new();
let id = g.add_node("ai.complete", 0.0, 0.0);
let props = &mut g.get_node_mut(id).unwrap().properties;
props.insert("prompt".into(), "alpha beta gamma delta".into());
props.insert("temperature".into(), temperature.into());
(g, id)
}
fn t0_text(g: &Graph, id: NodeId) -> String {
let mut ex = GraphExecutor::new(WidgetRegistry::with_builtins());
let steps = ex.run(g).expect("runs");
match steps
.iter()
.find(|s| s.node_id == id)
.and_then(|s| s.outputs.get("text"))
{
Some(Value::String(s)) => s.clone(),
other => panic!("no text output: {other:?}"),
}
}
#[test]
fn a_number_with_spaces_around_it_means_the_number_in_both_tiers() {
let (spaced, a) = complete_at(" 1 ");
let (plain, b) = complete_at("1");
let (zero, c) = complete_at("0");
// The validator accepts it…
assert!(check_parameters(&spaced, &WidgetRegistry::with_builtins()).is_empty());
// …so T0 must read 1, not fall back to 0. (0 and 1 differ, or this proves nothing.)
assert_ne!(t0_text(&plain, b), t0_text(&zero, c));
assert_eq!(t0_text(&spaced, a), t0_text(&plain, b));
// …and so must the generated program.
assert_eq!(spaced.to_rust().unwrap(), plain.to_rust().unwrap());
assert_ne!(zero.to_rust().unwrap(), plain.to_rust().unwrap());
}
#[test]
fn free_text_keeps_its_spaces() {
// A separator of " " is a space, not "nothing": only tokens are trimmed.
let reg = WidgetRegistry::with_builtins();
let join = reg.get("text.join").expect("text.join");
let props = HashMap::from([("separator".to_string(), " ".to_string())]);
let ctx = ExecContext::for_widget(join, &props);
assert_eq!(ctx.params["separator"], " ");
}
#[test]
fn clearing_a_declared_parameter_removes_it_so_the_default_applies() {
let reg = WidgetRegistry::with_builtins();
let chart = reg.get("flow.statechart");
// Cleared: remove, which is what `check_parameters` already assumed "" meant.
assert_eq!(stored_value(chart, "machine", String::new()), None);
// Anything else is stored as typed.
assert_eq!(
stored_value(chart, "machine", "initial: a\nstate: a\n".into()).as_deref(),
Some("initial: a\nstate: a\n")
);
// A token that is only whitespace is empty too; free text that is only a space is not.
let complete = reg.get("ai.complete");
assert_eq!(stored_value(complete, "temperature", " ".into()), None);
let join = reg.get("text.join");
assert_eq!(
stored_value(join, "separator", " ".into()).as_deref(),
Some(" ")
);
// Not a declared parameter (a literal's value, a code node's src): there is no
// default to fall back to, so an empty value is kept.
assert_eq!(
stored_value(None, "src", String::new()).as_deref(),
Some("")
);
assert_eq!(
stored_value(chart, "label", String::new()).as_deref(),
Some("")
);
}
#[test]
fn a_removed_statechart_machine_runs_the_default_chart() {
// The behaviour the removal exists for: no `machine` property is the default chart,
// where `""` was an empty chart that does not parse.
let reg = WidgetRegistry::with_builtins();
let chart = reg.get("flow.statechart").unwrap();
let state = |props: HashMap<String, String>| match chart
.execute(&ExecContext::for_widget(chart, &props), HashMap::new())
.get("state")
{
Some(Value::String(s)) => s.clone(),
other => panic!("{other:?}"),
};
assert_eq!(state(HashMap::new()), "off");
assert_ne!(
state(HashMap::from([("machine".to_string(), String::new())])),
"off",
"an empty chart is not the default one, which is why clearing must remove"
);
}