//! 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| 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" ); }