1use std::sync::Arc;
43
44use rustc_hash::FxHashMap;
45use serde_json::Value;
46
47use super::jsonpath;
48use crate::error::NikaError;
49use crate::event::EventKind;
50use crate::store::RunContext;
51
52use super::transform::TransformExpr;
53use super::types::{BindingPath, BindingSource, BindingType, PathSegment};
54use super::{BindingEntry, BindingSpec, WithEntry, WithSpec};
55
56#[derive(Debug, Clone)]
62pub enum LazyBinding {
63 Resolved(Value),
65 Pending {
67 path: String,
68 default: Option<Value>,
69 },
70 PendingWithEntry {
72 source: BindingPath,
73 binding_type: BindingType,
74 default: Option<Value>,
75 transform: Option<TransformExpr>,
76 },
77}
78
79impl LazyBinding {
80 pub fn is_pending(&self) -> bool {
82 matches!(
83 self,
84 LazyBinding::Pending { .. } | LazyBinding::PendingWithEntry { .. }
85 )
86 }
87
88 pub fn get_value(&self) -> Option<&Value> {
90 match self {
91 LazyBinding::Resolved(v) => Some(v),
92 LazyBinding::Pending { .. } | LazyBinding::PendingWithEntry { .. } => None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Default)]
103pub struct ResolvedBindings {
104 bindings: FxHashMap<String, LazyBinding>,
106 source_tasks: FxHashMap<String, String>,
114}
115
116impl ResolvedBindings {
117 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn from_binding_spec(
137 binding_spec: Option<&BindingSpec>,
138 datastore: &RunContext,
139 ) -> Result<Self, NikaError> {
140 let Some(spec) = binding_spec else {
141 return Ok(Self::new());
142 };
143
144 let mut resolved = Self::new();
145
146 for (alias, entry) in spec {
147 let (task_id, _) = split_path(&entry.path);
149 if !task_id.starts_with("inputs.")
150 && !task_id.starts_with("context.")
151 && !task_id.starts_with("env.")
152 {
153 resolved
154 .source_tasks
155 .insert(alias.clone(), task_id.to_string());
156 }
157
158 if entry.is_lazy() {
159 resolved.bindings.insert(
161 alias.clone(),
162 LazyBinding::Pending {
163 path: entry.path.clone(),
164 default: entry.default.clone(),
165 },
166 );
167 } else {
168 let value = resolve_entry(entry, alias, datastore)?;
170 resolved
171 .bindings
172 .insert(alias.clone(), LazyBinding::Resolved(value));
173 }
174 }
175
176 Ok(resolved)
177 }
178
179 pub fn from_with_spec(
194 with_spec: Option<&WithSpec>,
195 datastore: &RunContext,
196 ) -> Result<Self, NikaError> {
197 let Some(spec) = with_spec else {
198 return Ok(Self::new());
199 };
200
201 let mut bindings = Self::new();
202
203 for (alias, entry) in spec {
204 if let Some(task_id) = entry.source.task_id() {
206 bindings
207 .source_tasks
208 .insert(alias.clone(), task_id.to_string());
209 }
210
211 if entry.is_lazy() {
212 bindings.bindings.insert(
213 alias.clone(),
214 LazyBinding::PendingWithEntry {
215 source: entry.source.clone(),
216 binding_type: entry.binding_type,
217 default: entry.default.clone(),
218 transform: entry.transform.clone(),
219 },
220 );
221 } else {
222 let value = resolve_with_entry(entry, alias, datastore)?;
223 bindings
224 .bindings
225 .insert(alias.clone(), LazyBinding::Resolved(value));
226 }
227 }
228
229 Ok(bindings)
230 }
231
232 pub fn from_with_spec_traced(
237 with_spec: Option<&WithSpec>,
238 datastore: &RunContext,
239 task_id: &Arc<str>,
240 ) -> Result<(Self, Vec<EventKind>), NikaError> {
241 let Some(spec) = with_spec else {
242 return Ok((Self::new(), vec![]));
243 };
244
245 let mut bindings = Self::new();
246 let mut events = Vec::new();
247
248 for (alias, entry) in spec {
249 if let Some(tid) = entry.source.task_id() {
250 bindings.source_tasks.insert(alias.clone(), tid.to_string());
251 }
252
253 if entry.is_lazy() {
254 bindings.bindings.insert(
255 alias.clone(),
256 LazyBinding::PendingWithEntry {
257 source: entry.source.clone(),
258 binding_type: entry.binding_type,
259 default: entry.default.clone(),
260 transform: entry.transform.clone(),
261 },
262 );
263 } else {
264 let value =
265 resolve_with_entry_traced(entry, alias, datastore, task_id, &mut events)?;
266 bindings
267 .bindings
268 .insert(alias.clone(), LazyBinding::Resolved(value));
269 }
270 }
271
272 Ok((bindings, events))
273 }
274
275 pub fn set(&mut self, alias: impl Into<String>, value: Value) {
281 self.bindings
282 .insert(alias.into(), LazyBinding::Resolved(value));
283 }
284
285 pub fn set_with_source(
291 &mut self,
292 alias: impl Into<String>,
293 value: Value,
294 source_task_id: impl Into<String>,
295 ) {
296 let alias = alias.into();
297 self.source_tasks
298 .insert(alias.clone(), source_task_id.into());
299 self.bindings.insert(alias, LazyBinding::Resolved(value));
300 }
301
302 pub fn source_task_id(&self, alias: &str) -> Option<&str> {
307 self.source_tasks.get(alias).map(|s| s.as_str())
308 }
309
310 pub fn get(&self, alias: &str) -> Option<&Value> {
315 self.bindings.get(alias).and_then(|b| b.get_value())
316 }
317
318 pub fn get_resolved(&self, alias: &str, datastore: &RunContext) -> Result<Value, NikaError> {
326 match self.bindings.get(alias) {
327 Some(LazyBinding::Resolved(value)) => Ok(value.clone()),
328 Some(LazyBinding::Pending { path, default }) => {
329 let entry = BindingEntry {
331 path: path.clone(),
332 default: default.clone(),
333 lazy: true,
334 };
335 resolve_entry(&entry, alias, datastore)
336 }
337 Some(LazyBinding::PendingWithEntry {
338 source,
339 binding_type,
340 default,
341 transform,
342 }) => {
343 let entry = WithEntry {
345 source: source.clone(),
346 binding_type: *binding_type,
347 default: default.clone(),
348 lazy: true,
349 transform: transform.clone(),
350 };
351 resolve_with_entry(&entry, alias, datastore)
352 }
353 None => Err(NikaError::BindingNotFound {
354 alias: alias.to_string(),
355 }),
356 }
357 }
358
359 pub fn is_lazy(&self, alias: &str) -> bool {
361 self.bindings
362 .get(alias)
363 .map(|b| b.is_pending())
364 .unwrap_or(false)
365 }
366
367 pub fn is_empty(&self) -> bool {
369 self.bindings.is_empty()
370 }
371
372 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
377 self.bindings
378 .iter()
379 .filter_map(|(alias, binding)| binding.get_value().map(|value| (alias.as_str(), value)))
380 }
381
382 pub fn to_value(&self) -> Value {
388 let mut map = serde_json::Map::new();
389 for (alias, binding) in &self.bindings {
390 match binding {
391 LazyBinding::Resolved(v) => {
392 map.insert(alias.clone(), v.clone());
393 }
394 LazyBinding::Pending { path, default: _ } => {
395 map.insert(
397 alias.clone(),
398 serde_json::json!({"__lazy__": true, "path": path}),
399 );
400 }
401 LazyBinding::PendingWithEntry {
402 source, default: _, ..
403 } => {
404 map.insert(
406 alias.clone(),
407 serde_json::json!({"__lazy__": true, "path": source.to_string()}),
408 );
409 }
410 }
411 }
412 Value::Object(map)
413 }
414}
415
416fn resolve_entry(
429 entry: &BindingEntry,
430 alias: &str,
431 datastore: &RunContext,
432) -> Result<Value, NikaError> {
433 let path = &entry.path;
434
435 if path.starts_with("inputs.") {
437 let value = datastore.resolve_input_path(path);
438 return match value {
439 Some(v) if !v.is_null() => Ok(v),
440 Some(_) => entry
441 .default
442 .as_ref()
443 .cloned()
444 .ok_or_else(|| NikaError::NullValue {
445 path: path.clone(),
446 alias: alias.to_string(),
447 }),
448 None => entry
449 .default
450 .as_ref()
451 .cloned()
452 .ok_or_else(|| NikaError::PathNotFound { path: path.clone() }),
453 };
454 }
455
456 let (task_id, field_path) = split_path(path);
458
459 if let Some(fp) = field_path {
464 if fp == "media" || fp.starts_with("media.") || fp.starts_with("media[") {
465 let value = datastore.resolve_path(path);
466 return match value {
467 Some(v) if !v.is_null() => Ok(v),
468 Some(_) => entry
469 .default
470 .as_ref()
471 .cloned()
472 .ok_or_else(|| NikaError::NullValue {
473 path: path.clone(),
474 alias: alias.to_string(),
475 }),
476 None => entry
477 .default
478 .as_ref()
479 .cloned()
480 .ok_or_else(|| NikaError::PathNotFound { path: path.clone() }),
481 };
482 }
483 }
484
485 let value = match datastore.get_output(task_id) {
487 Some(output) => {
488 if let Some(fp) = field_path {
489 jsonpath::resolve(&output, fp)?
490 } else {
491 Some((*output).clone())
492 }
493 }
494 None => None,
495 };
496
497 match value {
499 Some(v) if !v.is_null() => Ok(v),
500 Some(_) => entry
501 .default
502 .as_ref()
503 .cloned()
504 .ok_or_else(|| NikaError::NullValue {
505 path: path.clone(),
506 alias: alias.to_string(),
507 }),
508 None => entry
509 .default
510 .as_ref()
511 .cloned()
512 .ok_or_else(|| NikaError::PathNotFound { path: path.clone() }),
513 }
514}
515
516fn split_path(path: &str) -> (&str, Option<&str>) {
523 if let Some(dot_idx) = path.find('.') {
524 let task_id = &path[..dot_idx];
525 let field_path = &path[dot_idx + 1..];
526 (task_id, Some(field_path))
527 } else {
528 (path, None)
529 }
530}
531
532fn resolve_with_entry(
545 entry: &WithEntry,
546 alias: &str,
547 datastore: &RunContext,
548) -> Result<Value, NikaError> {
549 let path_str = entry.source.to_string();
550
551 let raw_value = resolve_binding_path(&entry.source, alias, datastore)?;
553
554 let transformed = match (&raw_value, &entry.transform) {
556 (Some(v), Some(expr)) if !v.is_null() => {
557 Some(expr.apply(v).map_err(|e| NikaError::PathNotFound {
558 path: format!("{} (transform error: {})", path_str, e),
559 })?)
560 }
561 _ => raw_value,
562 };
563
564 let value = match transformed {
566 Some(v) if !v.is_null() => v,
567 Some(_null) => {
568 match &entry.default {
570 Some(d) => d.clone(),
571 None => {
572 return Err(NikaError::NullValue {
573 path: path_str,
574 alias: alias.to_string(),
575 });
576 }
577 }
578 }
579 None => {
580 match &entry.default {
582 Some(d) => d.clone(),
583 None => {
584 return Err(NikaError::PathNotFound { path: path_str });
585 }
586 }
587 }
588 };
589
590 validate_binding_type(&value, entry.binding_type, alias, &path_str)?;
592
593 Ok(value)
594}
595
596fn resolve_binding_path(
601 binding_path: &BindingPath,
602 alias: &str,
603 datastore: &RunContext,
604) -> Result<Option<Value>, NikaError> {
605 match &binding_path.source {
606 BindingSource::Task(task_id) => {
607 if matches!(
611 binding_path.segments.first(),
612 Some(crate::binding::types::PathSegment::Field(f)) if f.as_ref() == "media"
613 ) {
614 let full_path = format!(
616 "{}{}",
617 task_id,
618 binding_path
619 .segments
620 .iter()
621 .fold(String::new(), |mut acc, seg| {
622 match seg {
623 crate::binding::types::PathSegment::Field(f) => {
624 acc.push('.');
625 acc.push_str(f);
626 }
627 crate::binding::types::PathSegment::Index(i) => {
628 acc.push_str(&format!("[{}]", i));
629 }
630 }
631 acc
632 })
633 );
634 return Ok(datastore.resolve_path(&full_path));
635 }
636
637 let output = match datastore.get_output(task_id) {
638 Some(o) => o,
639 None => return Ok(None),
640 };
641
642 navigate_segments(&output, &binding_path.segments)
644 }
645
646 BindingSource::Input(sub_path) => {
647 let full_path = format!("inputs.{}", sub_path);
649 Ok(datastore.resolve_input_path(&full_path))
650 }
651
652 BindingSource::Context(sub_path) => {
653 let full_path = format!("context.{}", sub_path);
659 Ok(datastore.resolve_context_path(&full_path))
660 }
661
662 BindingSource::Env(var_name) => {
663 let name_upper = var_name.to_uppercase();
664 const SECRET_PATTERNS: &[&str] =
666 &["KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", "AUTH"];
667 let is_secret = SECRET_PATTERNS.iter().any(|p| name_upper.contains(p));
668 const SAFE_VARS: &[&str] = &[
670 "PATH", "HOME", "USER", "SHELL", "LANG", "TERM", "PWD", "TMPDIR", "TZ",
671 ];
672 let is_allowed =
673 name_upper.starts_with("NIKA_") || SAFE_VARS.iter().any(|v| name_upper == *v);
674 if is_secret || !is_allowed {
675 tracing::warn!(var = %var_name, "Blocked $env access to restricted variable");
676 Ok(None)
677 } else {
678 match std::env::var(var_name.as_ref()) {
679 Ok(val) => Ok(Some(Value::String(val))),
680 Err(_) => Ok(None),
681 }
682 }
683 }
684
685 BindingSource::LoopVar(name) => {
686 Err(NikaError::BindingNotFound {
689 alias: format!("{} (loop variable '{}' not pre-resolved)", alias, name),
690 })
691 }
692 }
693}
694
695fn resolve_with_entry_traced(
697 entry: &WithEntry,
698 alias: &str,
699 datastore: &RunContext,
700 task_id: &Arc<str>,
701 events: &mut Vec<EventKind>,
702) -> Result<Value, NikaError> {
703 let path_str = entry.source.to_string();
704
705 let raw_value = resolve_binding_path_traced(&entry.source, alias, datastore, task_id, events)?;
707
708 let transformed = match (&raw_value, &entry.transform) {
710 (Some(v), Some(expr)) if !v.is_null() => {
711 let result = expr.apply(v).map_err(|e| NikaError::PathNotFound {
712 path: format!("{} (transform error: {})", path_str, e),
713 })?;
714 events.push(EventKind::BindingTransformApplied {
716 task_id: Arc::clone(task_id),
717 alias: alias.to_string(),
718 transform_chain: format!("{:?}", expr),
719 });
720 Some(result)
721 }
722 _ => raw_value,
723 };
724
725 let value = match transformed {
727 Some(v) if !v.is_null() => v,
728 Some(_null) => {
729 match &entry.default {
730 Some(d) => {
731 events.push(EventKind::BindingDefaultApplied {
733 task_id: Arc::clone(task_id),
734 alias: alias.to_string(),
735 path: path_str.clone(),
736 default_value: d.clone(),
737 });
738 d.clone()
739 }
740 None => {
741 return Err(NikaError::NullValue {
742 path: path_str,
743 alias: alias.to_string(),
744 });
745 }
746 }
747 }
748 None => {
749 match &entry.default {
750 Some(d) => {
751 events.push(EventKind::BindingDefaultApplied {
753 task_id: Arc::clone(task_id),
754 alias: alias.to_string(),
755 path: path_str.clone(),
756 default_value: d.clone(),
757 });
758 d.clone()
759 }
760 None => {
761 return Err(NikaError::PathNotFound { path: path_str });
762 }
763 }
764 }
765 };
766
767 validate_binding_type(&value, entry.binding_type, alias, &path_str)?;
769
770 Ok(value)
771}
772
773fn resolve_binding_path_traced(
775 binding_path: &BindingPath,
776 alias: &str,
777 datastore: &RunContext,
778 task_id: &Arc<str>,
779 events: &mut Vec<EventKind>,
780) -> Result<Option<Value>, NikaError> {
781 match &binding_path.source {
782 BindingSource::Env(var_name) => {
783 let result = std::env::var(var_name.as_ref());
784 let found = result.is_ok();
785 events.push(EventKind::BindingEnvResolved {
787 task_id: Arc::clone(task_id),
788 var_name: var_name.to_string(),
789 found,
790 });
791 match result {
792 Ok(val) => Ok(Some(Value::String(val))),
793 Err(_) => Ok(None),
794 }
795 }
796 _ => resolve_binding_path(binding_path, alias, datastore),
798 }
799}
800
801fn navigate_segments(value: &Value, segments: &[PathSegment]) -> Result<Option<Value>, NikaError> {
805 if segments.is_empty() {
806 return Ok(Some(value.clone()));
807 }
808
809 let parsed;
812 let root = if let Some(v) = crate::binding::jsonpath::try_parse_json_str(value) {
813 parsed = v;
814 &parsed
815 } else {
816 value
817 };
818
819 let mut current = root;
820 for segment in segments {
821 match segment {
822 PathSegment::Field(name) => match current {
823 Value::Object(map) => match map.get(name.as_ref()) {
824 Some(v) => current = v,
825 None => return Ok(None),
826 },
827 _ => return Ok(None),
828 },
829 PathSegment::Index(idx) => match current {
830 Value::Array(arr) => match arr.get(*idx) {
831 Some(v) => current = v,
832 None => return Ok(None),
833 },
834 _ => return Ok(None),
835 },
836 }
837 }
838
839 Ok(Some(current.clone()))
840}
841
842fn validate_binding_type(
846 value: &Value,
847 binding_type: BindingType,
848 alias: &str,
849 path: &str,
850) -> Result<(), NikaError> {
851 let matches = match binding_type {
852 BindingType::Any => true,
853 BindingType::String => value.is_string(),
854 BindingType::Number => value.is_number(),
855 BindingType::Integer => value.is_i64() || value.is_u64(),
856 BindingType::Boolean => value.is_boolean(),
857 BindingType::Array => value.is_array(),
858 BindingType::Object => value.is_object(),
859 };
860
861 if !matches {
862 return Err(NikaError::BindingTypeMismatch {
863 expected: binding_type.to_string(),
864 actual: json_type_name(value).to_string(),
865 path: format!("{} (alias: {})", path, alias),
866 });
867 }
868
869 Ok(())
870}
871
872fn json_type_name(value: &Value) -> &'static str {
874 match value {
875 Value::Null => "null",
876 Value::Bool(_) => "boolean",
877 Value::Number(_) => "number",
878 Value::String(_) => "string",
879 Value::Array(_) => "array",
880 Value::Object(_) => "object",
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887 use crate::binding::types::BindingPath;
888 use crate::store::TaskResult;
889 use serde_json::json;
890 use std::sync::Arc;
891 use std::time::Duration;
892
893 #[test]
898 fn set_and_get() {
899 let mut bindings = ResolvedBindings::new();
900 bindings.set("forecast", json!("Sunny"));
901
902 assert_eq!(bindings.get("forecast"), Some(&json!("Sunny")));
903 assert_eq!(bindings.get("unknown"), None);
904 }
905
906 #[test]
907 fn is_empty() {
908 let mut bindings = ResolvedBindings::new();
909 assert!(bindings.is_empty());
910
911 bindings.set("key", json!("value"));
912 assert!(!bindings.is_empty());
913 }
914
915 #[test]
916 fn from_binding_spec_none() {
917 let store = RunContext::new();
918 let bindings = ResolvedBindings::from_binding_spec(None, &store).unwrap();
919 assert!(bindings.is_empty());
920 }
921
922 #[test]
923 fn from_with_spec_none() {
924 let store = RunContext::new();
925 let bindings = ResolvedBindings::from_with_spec(None, &store).unwrap();
926 assert!(bindings.is_empty());
927 }
928
929 #[test]
934 fn resolve_simple_path() {
935 let store = RunContext::new();
936 store.insert(
937 Arc::from("weather"),
938 TaskResult::success(json!({"summary": "Sunny"}), Duration::from_secs(1)),
939 );
940
941 let mut spec = BindingSpec::default();
942 spec.insert("forecast".to_string(), BindingEntry::new("weather.summary"));
943
944 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
945 assert_eq!(bindings.get("forecast"), Some(&json!("Sunny")));
946 }
947
948 #[test]
949 fn resolve_entire_task_output() {
950 let store = RunContext::new();
951 store.insert(
952 Arc::from("weather"),
953 TaskResult::success(
954 json!({"summary": "Sunny", "temp": 25}),
955 Duration::from_secs(1),
956 ),
957 );
958
959 let mut spec = BindingSpec::default();
960 spec.insert("data".to_string(), BindingEntry::new("weather"));
961
962 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
963 assert_eq!(
964 bindings.get("data"),
965 Some(&json!({"summary": "Sunny", "temp": 25}))
966 );
967 }
968
969 #[test]
970 fn resolve_nested_path() {
971 let store = RunContext::new();
972 store.insert(
973 Arc::from("weather"),
974 TaskResult::success(
975 json!({"data": {"temp": {"celsius": 25}}}),
976 Duration::from_secs(1),
977 ),
978 );
979
980 let mut spec = BindingSpec::default();
981 spec.insert(
982 "temp".to_string(),
983 BindingEntry::new("weather.data.temp.celsius"),
984 );
985
986 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
987 assert_eq!(bindings.get("temp"), Some(&json!(25)));
988 }
989
990 #[test]
991 fn resolve_with_default_on_missing() {
992 let store = RunContext::new();
993
994 let mut spec = BindingSpec::default();
995 spec.insert(
996 "forecast".to_string(),
997 BindingEntry::with_default("weather.summary", json!("Unknown")),
998 );
999
1000 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1001 assert_eq!(bindings.get("forecast"), Some(&json!("Unknown")));
1002 }
1003
1004 #[test]
1005 fn resolve_with_default_on_null() {
1006 let store = RunContext::new();
1007 store.insert(
1008 Arc::from("weather"),
1009 TaskResult::success(json!({"summary": null}), Duration::from_secs(1)),
1010 );
1011
1012 let mut spec = BindingSpec::default();
1013 spec.insert(
1014 "forecast".to_string(),
1015 BindingEntry::with_default("weather.summary", json!("N/A")),
1016 );
1017
1018 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1019 assert_eq!(bindings.get("forecast"), Some(&json!("N/A")));
1020 }
1021
1022 #[test]
1023 fn resolve_with_default_object() {
1024 let store = RunContext::new();
1025
1026 let mut spec = BindingSpec::default();
1027 spec.insert(
1028 "cfg".to_string(),
1029 BindingEntry::with_default("settings", json!({"debug": false})),
1030 );
1031
1032 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1033 assert_eq!(bindings.get("cfg"), Some(&json!({"debug": false})));
1034 }
1035
1036 #[test]
1037 fn resolve_with_default_array() {
1038 let store = RunContext::new();
1039
1040 let mut spec = BindingSpec::default();
1041 spec.insert(
1042 "tags".to_string(),
1043 BindingEntry::with_default("meta.tags", json!(["default"])),
1044 );
1045
1046 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1047 assert_eq!(bindings.get("tags"), Some(&json!(["default"])));
1048 }
1049
1050 #[test]
1055 fn resolve_path_not_found_error() {
1056 let store = RunContext::new();
1057
1058 let mut spec = BindingSpec::default();
1059 spec.insert("x".to_string(), BindingEntry::new("missing.path"));
1060
1061 let result = ResolvedBindings::from_binding_spec(Some(&spec), &store);
1062 assert!(result.is_err());
1063 assert!(result.unwrap_err().to_string().contains("NIKA-052"));
1064 }
1065
1066 #[test]
1067 fn resolve_null_strict_error() {
1068 let store = RunContext::new();
1069 store.insert(
1070 Arc::from("weather"),
1071 TaskResult::success(json!({"summary": null}), Duration::from_secs(1)),
1072 );
1073
1074 let mut spec = BindingSpec::default();
1075 spec.insert("forecast".to_string(), BindingEntry::new("weather.summary"));
1076
1077 let result = ResolvedBindings::from_binding_spec(Some(&spec), &store);
1078 assert!(result.is_err());
1079 assert!(result.unwrap_err().to_string().contains("NIKA-072"));
1080 }
1081
1082 #[test]
1087 fn resolve_jsonpath_array_index() {
1088 let store = RunContext::new();
1089 store.insert(
1090 Arc::from("data"),
1091 TaskResult::success(
1092 json!({"items": [{"name": "first"}, {"name": "second"}]}),
1093 Duration::from_secs(1),
1094 ),
1095 );
1096
1097 let mut spec = BindingSpec::default();
1098 spec.insert("first".to_string(), BindingEntry::new("data.items[0].name"));
1099
1100 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1101 assert_eq!(bindings.get("first"), Some(&json!("first")));
1102 }
1103
1104 #[test]
1109 fn split_path_task_only() {
1110 let (task_id, field_path) = split_path("weather");
1111 assert_eq!(task_id, "weather");
1112 assert_eq!(field_path, None);
1113 }
1114
1115 #[test]
1116 fn split_path_with_field() {
1117 let (task_id, field_path) = split_path("weather.summary");
1118 assert_eq!(task_id, "weather");
1119 assert_eq!(field_path, Some("summary"));
1120 }
1121
1122 #[test]
1123 fn split_path_nested() {
1124 let (task_id, field_path) = split_path("weather.data.temp.celsius");
1125 assert_eq!(task_id, "weather");
1126 assert_eq!(field_path, Some("data.temp.celsius"));
1127 }
1128
1129 #[test]
1134 fn to_value_serializes_resolved_inputs() {
1135 let mut bindings = ResolvedBindings::new();
1136 bindings.set("weather", json!("sunny"));
1137 bindings.set("temp", json!(25));
1138 bindings.set("nested", json!({"key": "value"}));
1139
1140 let value = bindings.to_value();
1141
1142 assert!(value.is_object());
1143 assert_eq!(value["weather"], "sunny");
1144 assert_eq!(value["temp"], 25);
1145 assert_eq!(value["nested"]["key"], "value");
1146 }
1147
1148 #[test]
1149 fn to_value_empty_bindings() {
1150 let bindings = ResolvedBindings::new();
1151 let value = bindings.to_value();
1152
1153 assert!(value.is_object());
1154 assert!(value.as_object().unwrap().is_empty());
1155 }
1156
1157 #[test]
1162 fn lazy_binding_resolved_not_pending() {
1163 let binding = LazyBinding::Resolved(json!("value"));
1164 assert!(!binding.is_pending());
1165 }
1166
1167 #[test]
1168 fn lazy_binding_pending_is_pending() {
1169 let binding = LazyBinding::Pending {
1170 path: "task.path".to_string(),
1171 default: None,
1172 };
1173 assert!(binding.is_pending());
1174 }
1175
1176 #[test]
1177 fn lazy_binding_pending_with_default_is_pending() {
1178 let binding = LazyBinding::Pending {
1179 path: "task.path".to_string(),
1180 default: Some(json!("fallback")),
1181 };
1182 assert!(binding.is_pending());
1183 }
1184
1185 #[test]
1186 fn lazy_binding_pending_with_entry_is_pending() {
1187 let binding = LazyBinding::PendingWithEntry {
1188 source: BindingPath::parse("$step1.data").unwrap(),
1189 binding_type: BindingType::Any,
1190 default: None,
1191 transform: None,
1192 };
1193 assert!(binding.is_pending());
1194 }
1195
1196 #[test]
1201 fn lazy_binding_get_value_resolved() {
1202 let binding = LazyBinding::Resolved(json!("resolved"));
1203 assert_eq!(binding.get_value(), Some(&json!("resolved")));
1204 }
1205
1206 #[test]
1207 fn lazy_binding_get_value_pending() {
1208 let binding = LazyBinding::Pending {
1209 path: "task.path".to_string(),
1210 default: None,
1211 };
1212 assert_eq!(binding.get_value(), None);
1213 }
1214
1215 #[test]
1216 fn lazy_binding_get_value_pending_with_entry() {
1217 let binding = LazyBinding::PendingWithEntry {
1218 source: BindingPath::parse("$step1").unwrap(),
1219 binding_type: BindingType::Any,
1220 default: None,
1221 transform: None,
1222 };
1223 assert_eq!(binding.get_value(), None);
1224 }
1225
1226 #[test]
1227 fn lazy_binding_get_value_complex_value() {
1228 let complex = json!({"nested": {"value": 42}, "array": [1, 2, 3]});
1229 let binding = LazyBinding::Resolved(complex.clone());
1230 assert_eq!(binding.get_value(), Some(&complex));
1231 }
1232
1233 #[test]
1238 fn new_creates_empty_bindings() {
1239 let bindings = ResolvedBindings::new();
1240 assert!(bindings.is_empty());
1241 assert_eq!(bindings.get("anything"), None);
1242 }
1243
1244 #[test]
1245 fn default_creates_empty_bindings() {
1246 let bindings = ResolvedBindings::default();
1247 assert!(bindings.is_empty());
1248 }
1249
1250 #[test]
1255 fn set_multiple_values() {
1256 let mut bindings = ResolvedBindings::new();
1257 bindings.set("key1", json!("value1"));
1258 bindings.set("key2", json!(42));
1259 bindings.set("key3", json!({"nested": true}));
1260
1261 assert_eq!(bindings.get("key1"), Some(&json!("value1")));
1262 assert_eq!(bindings.get("key2"), Some(&json!(42)));
1263 assert_eq!(bindings.get("key3"), Some(&json!({"nested": true})));
1264 }
1265
1266 #[test]
1267 fn set_overwrites_previous_value() {
1268 let mut bindings = ResolvedBindings::new();
1269 bindings.set("key", json!("old"));
1270 bindings.set("key", json!("new"));
1271
1272 assert_eq!(bindings.get("key"), Some(&json!("new")));
1273 }
1274
1275 #[test]
1276 fn set_with_string_into() {
1277 let mut bindings = ResolvedBindings::new();
1278 bindings.set("literal", json!("value"));
1279 assert_eq!(bindings.get("literal"), Some(&json!("value")));
1280 }
1281
1282 #[test]
1283 fn set_null_value() {
1284 let mut bindings = ResolvedBindings::new();
1285 bindings.set("nullable", json!(null));
1286 assert_eq!(bindings.get("nullable"), Some(&json!(null)));
1287 }
1288
1289 #[test]
1290 fn set_array_value() {
1291 let mut bindings = ResolvedBindings::new();
1292 let arr = json!([1, 2, 3, "mixed", {"obj": true}]);
1293 bindings.set("array", arr.clone());
1294 assert_eq!(bindings.get("array"), Some(&arr));
1295 }
1296
1297 #[test]
1302 fn get_nonexistent_returns_none() {
1303 let bindings = ResolvedBindings::new();
1304 assert_eq!(bindings.get("nonexistent"), None);
1305 }
1306
1307 #[test]
1308 fn get_does_not_resolve_lazy() {
1309 let store = RunContext::new();
1310 store.insert(
1311 Arc::from("task"),
1312 TaskResult::success(json!({"value": "result"}), Duration::from_secs(1)),
1313 );
1314
1315 let mut spec = BindingSpec::default();
1316 spec.insert(
1317 "lazy_bind".to_string(),
1318 BindingEntry::lazy_with_default("task.value", json!("default")),
1319 );
1320
1321 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1322 assert_eq!(bindings.get("lazy_bind"), None);
1324 }
1325
1326 #[test]
1331 fn get_resolved_eager_binding() {
1332 let store = RunContext::new();
1333 store.insert(
1334 Arc::from("task"),
1335 TaskResult::success(json!({"value": "result"}), Duration::from_secs(1)),
1336 );
1337
1338 let mut spec = BindingSpec::default();
1339 spec.insert("eager".to_string(), BindingEntry::new("task.value"));
1340
1341 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1342 let result = bindings.get_resolved("eager", &store).unwrap();
1343 assert_eq!(result, json!("result"));
1344 }
1345
1346 #[test]
1347 fn get_resolved_lazy_binding() {
1348 let store = RunContext::new();
1349 store.insert(
1350 Arc::from("task"),
1351 TaskResult::success(json!({"value": "lazy_result"}), Duration::from_secs(1)),
1352 );
1353
1354 let mut spec = BindingSpec::default();
1355 spec.insert("lazy".to_string(), BindingEntry::new_lazy("task.value"));
1356
1357 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1358 let result = bindings.get_resolved("lazy", &store).unwrap();
1359 assert_eq!(result, json!("lazy_result"));
1360 }
1361
1362 #[test]
1363 fn get_resolved_nonexistent_binding() {
1364 let store = RunContext::new();
1365 let bindings = ResolvedBindings::new();
1366 let result = bindings.get_resolved("missing", &store);
1367 assert!(result.is_err());
1368 assert!(result.unwrap_err().to_string().contains("NIKA-042")); }
1370
1371 #[test]
1372 fn get_resolved_lazy_with_default() {
1373 let store = RunContext::new();
1374 let mut spec = BindingSpec::default();
1377 spec.insert(
1378 "lazy_default".to_string(),
1379 BindingEntry::lazy_with_default("missing.path", json!("fallback")),
1380 );
1381
1382 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1383 let result = bindings.get_resolved("lazy_default", &store).unwrap();
1384 assert_eq!(result, json!("fallback"));
1385 }
1386
1387 #[test]
1388 fn get_resolved_re_resolves_on_each_call() {
1389 let store = RunContext::new();
1390 store.insert(
1391 Arc::from("task"),
1392 TaskResult::success(json!({"counter": 1}), Duration::from_secs(1)),
1393 );
1394
1395 let mut spec = BindingSpec::default();
1396 spec.insert("lazy".to_string(), BindingEntry::new_lazy("task.counter"));
1397
1398 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1399
1400 let result1 = bindings.get_resolved("lazy", &store).unwrap();
1402 assert_eq!(result1, json!(1));
1403
1404 store.insert(
1406 Arc::from("task"),
1407 TaskResult::success(json!({"counter": 2}), Duration::from_secs(1)),
1408 );
1409
1410 let result2 = bindings.get_resolved("lazy", &store).unwrap();
1412 assert_eq!(result2, json!(2));
1413 }
1414
1415 #[test]
1420 fn is_lazy_for_eager_binding() {
1421 let store = RunContext::new();
1422 store.insert(
1423 Arc::from("task"),
1424 TaskResult::success(json!({"value": "test"}), Duration::from_secs(1)),
1425 );
1426
1427 let mut spec = BindingSpec::default();
1428 spec.insert("eager".to_string(), BindingEntry::new("task.value"));
1429
1430 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1431 assert!(!bindings.is_lazy("eager"));
1432 }
1433
1434 #[test]
1435 fn is_lazy_for_lazy_binding() {
1436 let store = RunContext::new();
1437 let mut spec = BindingSpec::default();
1438 spec.insert("lazy".to_string(), BindingEntry::new_lazy("task.value"));
1439
1440 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1441 assert!(bindings.is_lazy("lazy"));
1442 }
1443
1444 #[test]
1445 fn is_lazy_for_nonexistent_binding() {
1446 let bindings = ResolvedBindings::new();
1447 assert!(!bindings.is_lazy("missing"));
1448 }
1449
1450 #[test]
1451 fn is_lazy_after_resolution() {
1452 let store = RunContext::new();
1453 store.insert(
1454 Arc::from("task"),
1455 TaskResult::success(json!({"value": "result"}), Duration::from_secs(1)),
1456 );
1457
1458 let mut spec = BindingSpec::default();
1459 spec.insert("lazy".to_string(), BindingEntry::new_lazy("task.value"));
1460
1461 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1462 let _ = bindings.get_resolved("lazy", &store);
1464 assert!(bindings.is_lazy("lazy"));
1465 }
1466
1467 #[test]
1472 fn iter_empty_bindings() {
1473 let bindings = ResolvedBindings::new();
1474 let count = bindings.iter().count();
1475 assert_eq!(count, 0);
1476 }
1477
1478 #[test]
1479 fn iter_only_resolved_bindings() {
1480 let store = RunContext::new();
1481 store.insert(
1482 Arc::from("task"),
1483 TaskResult::success(json!({"value": "result"}), Duration::from_secs(1)),
1484 );
1485
1486 let mut spec = BindingSpec::default();
1487 spec.insert("eager".to_string(), BindingEntry::new("task.value"));
1488 spec.insert("lazy".to_string(), BindingEntry::new_lazy("task.value"));
1489
1490 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1491
1492 let items: Vec<_> = bindings.iter().collect();
1494 assert_eq!(items.len(), 1);
1495 assert_eq!(items[0].0, "eager");
1496 assert_eq!(items[0].1, &json!("result"));
1497 }
1498
1499 #[test]
1500 fn iter_multiple_resolved_bindings() {
1501 let mut bindings = ResolvedBindings::new();
1502 bindings.set("first", json!(1));
1503 bindings.set("second", json!(2));
1504 bindings.set("third", json!(3));
1505
1506 let items: Vec<_> = bindings.iter().collect();
1507 assert_eq!(items.len(), 3);
1508
1509 let aliases: Vec<_> = items.iter().map(|(alias, _)| *alias).collect();
1511 assert!(aliases.contains(&"first"));
1512 assert!(aliases.contains(&"second"));
1513 assert!(aliases.contains(&"third"));
1514 }
1515
1516 #[test]
1517 fn iter_with_various_value_types() {
1518 let mut bindings = ResolvedBindings::new();
1519 bindings.set("str", json!("text"));
1520 bindings.set("num", json!(42));
1521 bindings.set("obj", json!({"key": "value"}));
1522 bindings.set("arr", json!([1, 2, 3]));
1523 bindings.set("bool", json!(true));
1524
1525 let items: Vec<_> = bindings.iter().collect();
1526 assert_eq!(items.len(), 5);
1527
1528 for (alias, value) in &items {
1530 match *alias {
1531 "str" => assert_eq!(*value, &json!("text")),
1532 "num" => assert_eq!(*value, &json!(42)),
1533 "obj" => assert_eq!(*value, &json!({"key": "value"})),
1534 "arr" => assert_eq!(*value, &json!([1, 2, 3])),
1535 "bool" => assert_eq!(*value, &json!(true)),
1536 _ => panic!("unexpected alias: {}", alias),
1537 }
1538 }
1539 }
1540
1541 #[test]
1546 fn to_value_with_lazy_bindings() {
1547 let mut bindings = ResolvedBindings::new();
1548 bindings.set("eager", json!("eager_value"));
1549
1550 bindings.bindings.insert(
1552 "lazy".to_string(),
1553 LazyBinding::Pending {
1554 path: "task.path".to_string(),
1555 default: Some(json!("lazy_default")),
1556 },
1557 );
1558
1559 let value = bindings.to_value();
1560 assert!(value.is_object());
1561
1562 let obj = value.as_object().unwrap();
1563 assert_eq!(obj["eager"], json!("eager_value"));
1564
1565 let lazy_marker = &obj["lazy"];
1567 assert!(lazy_marker.is_object());
1568 assert_eq!(lazy_marker["__lazy__"], true);
1569 assert_eq!(lazy_marker["path"], "task.path");
1570 }
1571
1572 #[test]
1573 fn to_value_with_pending_with_entry() {
1574 let mut bindings = ResolvedBindings::new();
1575 bindings.set("eager", json!("eager_value"));
1576
1577 bindings.bindings.insert(
1579 "lazy_new".to_string(),
1580 LazyBinding::PendingWithEntry {
1581 source: BindingPath::parse("$step1.data").unwrap(),
1582 binding_type: BindingType::Object,
1583 default: None,
1584 transform: None,
1585 },
1586 );
1587
1588 let value = bindings.to_value();
1589 let obj = value.as_object().unwrap();
1590
1591 let lazy_marker = &obj["lazy_new"];
1592 assert_eq!(lazy_marker["__lazy__"], true);
1593 assert_eq!(lazy_marker["path"], "$step1.data");
1594 }
1595
1596 #[test]
1601 fn from_binding_spec_eager_missing_path() {
1602 let store = RunContext::new();
1603 let mut spec = BindingSpec::default();
1604 spec.insert("x".to_string(), BindingEntry::new("nonexistent.path"));
1605
1606 let result = ResolvedBindings::from_binding_spec(Some(&spec), &store);
1607 assert!(result.is_err());
1608 }
1609
1610 #[test]
1611 fn from_binding_spec_lazy_does_not_fail_on_missing() {
1612 let store = RunContext::new();
1613 let mut spec = BindingSpec::default();
1614 spec.insert("x".to_string(), BindingEntry::new_lazy("nonexistent.path"));
1615
1616 let result = ResolvedBindings::from_binding_spec(Some(&spec), &store);
1618 assert!(result.is_ok());
1619 }
1620
1621 #[test]
1622 fn from_binding_spec_preserves_all_entries() {
1623 let store = RunContext::new();
1624 store.insert(
1625 Arc::from("task1"),
1626 TaskResult::success(json!({"a": 1}), Duration::from_secs(1)),
1627 );
1628 store.insert(
1629 Arc::from("task2"),
1630 TaskResult::success(json!({"b": 2}), Duration::from_secs(1)),
1631 );
1632
1633 let mut spec = BindingSpec::default();
1634 spec.insert("binding1".to_string(), BindingEntry::new("task1.a"));
1635 spec.insert("binding2".to_string(), BindingEntry::new_lazy("task2.b"));
1636
1637 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1638
1639 assert_eq!(bindings.get("binding1"), Some(&json!(1)));
1641 assert!(bindings.is_lazy("binding2"));
1642 }
1643
1644 #[test]
1649 fn mixed_eager_and_lazy_workflow() {
1650 let store = RunContext::new();
1651 store.insert(
1652 Arc::from("quick"),
1653 TaskResult::success(json!({"result": "fast"}), Duration::from_secs(1)),
1654 );
1655 store.insert(
1656 Arc::from("slow"),
1657 TaskResult::success(json!({"result": "slow_value"}), Duration::from_secs(5)),
1658 );
1659
1660 let mut spec = BindingSpec::default();
1661 spec.insert("quick_bind".to_string(), BindingEntry::new("quick.result"));
1662 spec.insert(
1663 "slow_bind".to_string(),
1664 BindingEntry::new_lazy("slow.result"),
1665 );
1666
1667 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1668
1669 assert_eq!(bindings.get("quick_bind"), Some(&json!("fast")));
1671
1672 assert!(bindings.is_lazy("slow_bind"));
1674 assert_eq!(bindings.get("slow_bind"), None);
1675
1676 let resolved = bindings.get_resolved("slow_bind", &store).unwrap();
1678 assert_eq!(resolved, json!("slow_value"));
1679 }
1680
1681 #[test]
1686 fn binding_with_empty_string() {
1687 let mut bindings = ResolvedBindings::new();
1688 bindings.set("empty", json!(""));
1689 assert_eq!(bindings.get("empty"), Some(&json!("")));
1690 }
1691
1692 #[test]
1693 fn binding_with_zero() {
1694 let mut bindings = ResolvedBindings::new();
1695 bindings.set("zero", json!(0));
1696 assert_eq!(bindings.get("zero"), Some(&json!(0)));
1697 }
1698
1699 #[test]
1700 fn binding_with_false() {
1701 let mut bindings = ResolvedBindings::new();
1702 bindings.set("falsy", json!(false));
1703 assert_eq!(bindings.get("falsy"), Some(&json!(false)));
1704 }
1705
1706 #[test]
1707 fn binding_with_empty_array() {
1708 let mut bindings = ResolvedBindings::new();
1709 bindings.set("empty_arr", json!([]));
1710 assert_eq!(bindings.get("empty_arr"), Some(&json!([])));
1711 }
1712
1713 #[test]
1714 fn binding_with_empty_object() {
1715 let mut bindings = ResolvedBindings::new();
1716 bindings.set("empty_obj", json!({}));
1717 assert_eq!(bindings.get("empty_obj"), Some(&json!({})));
1718 }
1719
1720 #[test]
1725 fn resolve_inputs_simple() {
1726 use rustc_hash::FxHashMap;
1727
1728 let store = RunContext::new();
1729
1730 let mut inputs = FxHashMap::default();
1731 inputs.insert(
1732 "topic".to_string(),
1733 json!({
1734 "type": "string",
1735 "default": "AI trends 2025"
1736 }),
1737 );
1738 store.set_inputs(inputs);
1739
1740 let mut spec = BindingSpec::default();
1741 spec.insert("topic_val".to_string(), BindingEntry::new("inputs.topic"));
1742
1743 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1744 assert_eq!(bindings.get("topic_val"), Some(&json!("AI trends 2025")));
1745 }
1746
1747 #[test]
1748 fn resolve_inputs_nested_field() {
1749 use rustc_hash::FxHashMap;
1750
1751 let store = RunContext::new();
1752
1753 let mut inputs = FxHashMap::default();
1754 inputs.insert(
1755 "config".to_string(),
1756 json!({
1757 "type": "object",
1758 "default": {
1759 "theme": "dark",
1760 "version": 2,
1761 "nested": {
1762 "deep": "value"
1763 }
1764 }
1765 }),
1766 );
1767 store.set_inputs(inputs);
1768
1769 let mut spec = BindingSpec::default();
1770 spec.insert(
1771 "theme".to_string(),
1772 BindingEntry::new("inputs.config.theme"),
1773 );
1774 spec.insert(
1775 "deep".to_string(),
1776 BindingEntry::new("inputs.config.nested.deep"),
1777 );
1778
1779 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1780 assert_eq!(bindings.get("theme"), Some(&json!("dark")));
1781 assert_eq!(bindings.get("deep"), Some(&json!("value")));
1782 }
1783
1784 #[test]
1785 fn resolve_inputs_with_default_on_missing() {
1786 let store = RunContext::new();
1787
1788 let mut spec = BindingSpec::default();
1789 spec.insert(
1790 "fallback".to_string(),
1791 BindingEntry::with_default("inputs.missing", json!("default_value")),
1792 );
1793
1794 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1795 assert_eq!(bindings.get("fallback"), Some(&json!("default_value")));
1796 }
1797
1798 #[test]
1799 fn resolve_inputs_missing_no_default() {
1800 let store = RunContext::new();
1801
1802 let mut spec = BindingSpec::default();
1803 spec.insert("missing".to_string(), BindingEntry::new("inputs.missing"));
1804
1805 let result = ResolvedBindings::from_binding_spec(Some(&spec), &store);
1806 assert!(result.is_err());
1807 assert!(result.unwrap_err().to_string().contains("NIKA-052")); }
1809
1810 #[test]
1811 fn resolve_inputs_lazy_binding() {
1812 use rustc_hash::FxHashMap;
1813
1814 let store = RunContext::new();
1815
1816 let mut inputs = FxHashMap::default();
1817 inputs.insert(
1818 "lazy_input".to_string(),
1819 json!({
1820 "type": "string",
1821 "default": "lazy_value"
1822 }),
1823 );
1824 store.set_inputs(inputs);
1825
1826 let mut spec = BindingSpec::default();
1827 spec.insert(
1828 "lazy_alias".to_string(),
1829 BindingEntry::new_lazy("inputs.lazy_input"),
1830 );
1831
1832 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1833
1834 assert!(bindings.is_lazy("lazy_alias"));
1835 assert_eq!(bindings.get("lazy_alias"), None);
1836
1837 let resolved = bindings.get_resolved("lazy_alias", &store).unwrap();
1838 assert_eq!(resolved, json!("lazy_value"));
1839 }
1840
1841 #[test]
1842 fn resolve_inputs_mixed_with_task_outputs() {
1843 use rustc_hash::FxHashMap;
1844
1845 let store = RunContext::new();
1846
1847 let mut inputs = FxHashMap::default();
1848 inputs.insert(
1849 "topic".to_string(),
1850 json!({
1851 "type": "string",
1852 "default": "AI"
1853 }),
1854 );
1855 store.set_inputs(inputs);
1856
1857 store.insert(
1858 Arc::from("step1"),
1859 TaskResult::success(json!({"result": "generated"}), Duration::from_secs(1)),
1860 );
1861
1862 let mut spec = BindingSpec::default();
1863 spec.insert("from_input".to_string(), BindingEntry::new("inputs.topic"));
1864 spec.insert("from_task".to_string(), BindingEntry::new("step1.result"));
1865
1866 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1867
1868 assert_eq!(bindings.get("from_input"), Some(&json!("AI")));
1869 assert_eq!(bindings.get("from_task"), Some(&json!("generated")));
1870 }
1871
1872 #[test]
1873 fn resolve_inputs_array_value() {
1874 use rustc_hash::FxHashMap;
1875
1876 let store = RunContext::new();
1877
1878 let mut inputs = FxHashMap::default();
1879 inputs.insert(
1880 "items".to_string(),
1881 json!({
1882 "type": "array",
1883 "default": ["a", "b", "c"]
1884 }),
1885 );
1886 store.set_inputs(inputs);
1887
1888 let mut spec = BindingSpec::default();
1889 spec.insert("all_items".to_string(), BindingEntry::new("inputs.items"));
1890
1891 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
1892 assert_eq!(bindings.get("all_items"), Some(&json!(["a", "b", "c"])));
1893 }
1894
1895 #[test]
1900 fn with_spec_task_simple() {
1901 let store = RunContext::new();
1902 store.insert(
1903 Arc::from("step1"),
1904 TaskResult::success(json!({"title": "Hello"}), Duration::from_secs(1)),
1905 );
1906
1907 let mut spec = WithSpec::default();
1908 spec.insert(
1909 "title".to_string(),
1910 WithEntry::simple(BindingPath::parse("$step1.title").unwrap()),
1911 );
1912
1913 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
1914 assert_eq!(bindings.get("title"), Some(&json!("Hello")));
1915 }
1916
1917 #[test]
1918 fn with_spec_task_entire_output() {
1919 let store = RunContext::new();
1920 store.insert(
1921 Arc::from("step1"),
1922 TaskResult::success(json!({"a": 1, "b": 2}), Duration::from_secs(1)),
1923 );
1924
1925 let mut spec = WithSpec::default();
1926 spec.insert(
1927 "data".to_string(),
1928 WithEntry::simple(BindingPath::parse("$step1").unwrap()),
1929 );
1930
1931 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
1932 assert_eq!(bindings.get("data"), Some(&json!({"a": 1, "b": 2})));
1933 }
1934
1935 #[test]
1936 fn with_spec_task_nested_path() {
1937 let store = RunContext::new();
1938 store.insert(
1939 Arc::from("step1"),
1940 TaskResult::success(
1941 json!({"data": {"items": [{"name": "first"}]}}),
1942 Duration::from_secs(1),
1943 ),
1944 );
1945
1946 let mut spec = WithSpec::default();
1947 spec.insert(
1948 "first_name".to_string(),
1949 WithEntry::simple(BindingPath::parse("$step1.data.items[0].name").unwrap()),
1950 );
1951
1952 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
1953 assert_eq!(bindings.get("first_name"), Some(&json!("first")));
1954 }
1955
1956 #[test]
1957 fn with_spec_task_with_default_on_missing() {
1958 let store = RunContext::new();
1959 let mut spec = WithSpec::default();
1962 spec.insert(
1963 "result".to_string(),
1964 WithEntry::with_default(
1965 BindingPath::parse("$step1.data").unwrap(),
1966 json!("fallback"),
1967 ),
1968 );
1969
1970 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
1971 assert_eq!(bindings.get("result"), Some(&json!("fallback")));
1972 }
1973
1974 #[test]
1975 fn with_spec_task_with_default_on_null() {
1976 let store = RunContext::new();
1977 store.insert(
1978 Arc::from("step1"),
1979 TaskResult::success(json!({"data": null}), Duration::from_secs(1)),
1980 );
1981
1982 let mut spec = WithSpec::default();
1983 spec.insert(
1984 "result".to_string(),
1985 WithEntry::with_default(
1986 BindingPath::parse("$step1.data").unwrap(),
1987 json!("fallback"),
1988 ),
1989 );
1990
1991 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
1992 assert_eq!(bindings.get("result"), Some(&json!("fallback")));
1993 }
1994
1995 #[test]
1996 fn with_spec_task_missing_no_default_error() {
1997 let store = RunContext::new();
1998
1999 let mut spec = WithSpec::default();
2000 spec.insert(
2001 "result".to_string(),
2002 WithEntry::simple(BindingPath::parse("$step1.data").unwrap()),
2003 );
2004
2005 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2006 assert!(result.is_err());
2007 let err_str = result.unwrap_err().to_string();
2008 assert!(err_str.contains("NIKA-052")); }
2010
2011 #[test]
2012 fn with_spec_task_null_no_default_error() {
2013 let store = RunContext::new();
2014 store.insert(
2015 Arc::from("step1"),
2016 TaskResult::success(json!({"data": null}), Duration::from_secs(1)),
2017 );
2018
2019 let mut spec = WithSpec::default();
2020 spec.insert(
2021 "result".to_string(),
2022 WithEntry::simple(BindingPath::parse("$step1.data").unwrap()),
2023 );
2024
2025 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2026 assert!(result.is_err());
2027 let err_str = result.unwrap_err().to_string();
2028 assert!(err_str.contains("NIKA-072")); }
2030
2031 #[test]
2036 fn with_spec_input_simple() {
2037 use rustc_hash::FxHashMap;
2038
2039 let store = RunContext::new();
2040 let mut inputs = FxHashMap::default();
2041 inputs.insert(
2042 "topic".to_string(),
2043 json!({"type": "string", "default": "AI trends"}),
2044 );
2045 store.set_inputs(inputs);
2046
2047 let mut spec = WithSpec::default();
2048 spec.insert(
2049 "topic".to_string(),
2050 WithEntry::simple(BindingPath::parse("$inputs.topic").unwrap()),
2051 );
2052
2053 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2054 assert_eq!(bindings.get("topic"), Some(&json!("AI trends")));
2055 }
2056
2057 #[test]
2058 fn with_spec_input_nested() {
2059 use rustc_hash::FxHashMap;
2060
2061 let store = RunContext::new();
2062 let mut inputs = FxHashMap::default();
2063 inputs.insert(
2064 "config".to_string(),
2065 json!({"type": "object", "default": {"theme": "dark", "nested": {"deep": "val"}}}),
2066 );
2067 store.set_inputs(inputs);
2068
2069 let mut spec = WithSpec::default();
2070 spec.insert(
2071 "theme".to_string(),
2072 WithEntry::simple(BindingPath::parse("$inputs.config.theme").unwrap()),
2073 );
2074 spec.insert(
2075 "deep".to_string(),
2076 WithEntry::simple(BindingPath::parse("$inputs.config.nested.deep").unwrap()),
2077 );
2078
2079 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2080 assert_eq!(bindings.get("theme"), Some(&json!("dark")));
2081 assert_eq!(bindings.get("deep"), Some(&json!("val")));
2082 }
2083
2084 #[test]
2085 fn with_spec_input_missing_with_default() {
2086 let store = RunContext::new();
2087 let mut spec = WithSpec::default();
2090 spec.insert(
2091 "fallback".to_string(),
2092 WithEntry::with_default(
2093 BindingPath::parse("$inputs.missing").unwrap(),
2094 json!("default_val"),
2095 ),
2096 );
2097
2098 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2099 assert_eq!(bindings.get("fallback"), Some(&json!("default_val")));
2100 }
2101
2102 #[test]
2107 fn with_spec_env_existing_var() {
2108 std::env::set_var("NIKA_TEST_VAR_8A", "test_value_8a");
2110
2111 let store = RunContext::new();
2112 let mut spec = WithSpec::default();
2113 spec.insert(
2114 "my_var".to_string(),
2115 WithEntry::simple(BindingPath::parse("$env.NIKA_TEST_VAR_8A").unwrap()),
2116 );
2117
2118 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2119 assert_eq!(bindings.get("my_var"), Some(&json!("test_value_8a")));
2120
2121 std::env::remove_var("NIKA_TEST_VAR_8A");
2122 }
2123
2124 #[test]
2125 fn with_spec_env_missing_with_default() {
2126 let store = RunContext::new();
2127 let mut spec = WithSpec::default();
2128 spec.insert(
2129 "missing_env".to_string(),
2130 WithEntry::with_default(
2131 BindingPath::parse("$env.NIKA_NONEXISTENT_VAR_XYZ").unwrap(),
2132 json!("fallback_env"),
2133 ),
2134 );
2135
2136 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2137 assert_eq!(bindings.get("missing_env"), Some(&json!("fallback_env")));
2138 }
2139
2140 #[test]
2141 fn with_spec_env_missing_no_default_error() {
2142 let store = RunContext::new();
2143 let mut spec = WithSpec::default();
2144 spec.insert(
2145 "missing".to_string(),
2146 WithEntry::simple(BindingPath::parse("$env.NIKA_NONEXISTENT_VAR_ABC").unwrap()),
2147 );
2148
2149 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2150 assert!(result.is_err());
2151 }
2152
2153 #[test]
2158 fn with_spec_context_file() {
2159 use crate::store::LoadedContext;
2160 let store = RunContext::new();
2161 let mut ctx = LoadedContext::new();
2162 ctx.files
2163 .insert("brand".to_string(), json!("Brand Guidelines v2"));
2164 store.set_context(ctx);
2165
2166 let mut spec = WithSpec::default();
2167 spec.insert(
2168 "brand".to_string(),
2169 WithEntry::simple(BindingPath::parse("$context.files.brand").unwrap()),
2170 );
2171
2172 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2173 assert_eq!(bindings.get("brand"), Some(&json!("Brand Guidelines v2")));
2174 }
2175
2176 #[test]
2177 fn with_spec_context_session() {
2178 use crate::store::LoadedContext;
2179 let store = RunContext::new();
2180 let mut ctx = LoadedContext::new();
2181 ctx.session = Some(json!({"last_run": "2025-01-01"}));
2182 store.set_context(ctx);
2183
2184 let mut spec = WithSpec::default();
2185 spec.insert(
2186 "session".to_string(),
2187 WithEntry::simple(BindingPath::parse("$context.session").unwrap()),
2188 );
2189
2190 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2191 assert_eq!(
2192 bindings.get("session"),
2193 Some(&json!({"last_run": "2025-01-01"}))
2194 );
2195 }
2196
2197 #[test]
2198 fn with_spec_context_missing_with_default() {
2199 let store = RunContext::new();
2200 let mut spec = WithSpec::default();
2203 spec.insert(
2204 "brand".to_string(),
2205 WithEntry::with_default(
2206 BindingPath::parse("$context.files.brand").unwrap(),
2207 json!("no brand"),
2208 ),
2209 );
2210
2211 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2212 assert_eq!(bindings.get("brand"), Some(&json!("no brand")));
2213 }
2214
2215 #[test]
2220 fn with_spec_lazy_does_not_fail_on_missing() {
2221 let store = RunContext::new();
2222
2223 let mut spec = WithSpec::default();
2224 let mut entry = WithEntry::simple(BindingPath::parse("$step1.data").unwrap());
2225 entry.lazy = true;
2226 spec.insert("lazy_val".to_string(), entry);
2227
2228 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2229 assert!(result.is_ok());
2230 let bindings = result.unwrap();
2231 assert!(bindings.is_lazy("lazy_val"));
2232 assert_eq!(bindings.get("lazy_val"), None);
2233 }
2234
2235 #[test]
2236 fn with_spec_lazy_resolve_on_demand() {
2237 let store = RunContext::new();
2238 store.insert(
2239 Arc::from("step1"),
2240 TaskResult::success(json!({"data": "deferred"}), Duration::from_secs(1)),
2241 );
2242
2243 let mut spec = WithSpec::default();
2244 let mut entry = WithEntry::simple(BindingPath::parse("$step1.data").unwrap());
2245 entry.lazy = true;
2246 spec.insert("lazy_val".to_string(), entry);
2247
2248 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2249 assert!(bindings.is_lazy("lazy_val"));
2250
2251 let resolved = bindings.get_resolved("lazy_val", &store).unwrap();
2252 assert_eq!(resolved, json!("deferred"));
2253 }
2254
2255 #[test]
2256 fn with_spec_lazy_re_resolves() {
2257 let store = RunContext::new();
2258 store.insert(
2259 Arc::from("step1"),
2260 TaskResult::success(json!({"counter": 1}), Duration::from_secs(1)),
2261 );
2262
2263 let mut spec = WithSpec::default();
2264 let mut entry = WithEntry::simple(BindingPath::parse("$step1.counter").unwrap());
2265 entry.lazy = true;
2266 spec.insert("counter".to_string(), entry);
2267
2268 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2269
2270 let v1 = bindings.get_resolved("counter", &store).unwrap();
2271 assert_eq!(v1, json!(1));
2272
2273 store.insert(
2275 Arc::from("step1"),
2276 TaskResult::success(json!({"counter": 42}), Duration::from_secs(1)),
2277 );
2278
2279 let v2 = bindings.get_resolved("counter", &store).unwrap();
2280 assert_eq!(v2, json!(42));
2281 }
2282
2283 #[test]
2288 fn with_spec_with_transform() {
2289 let store = RunContext::new();
2290 store.insert(
2291 Arc::from("step1"),
2292 TaskResult::success(json!({"name": " Hello World "}), Duration::from_secs(1)),
2293 );
2294
2295 let mut spec = WithSpec::default();
2296 let mut entry = WithEntry::simple(BindingPath::parse("$step1.name").unwrap());
2297 entry.transform = Some(TransformExpr::parse("trim | upper").unwrap());
2298 spec.insert("name".to_string(), entry);
2299
2300 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2301 assert_eq!(bindings.get("name"), Some(&json!("HELLO WORLD")));
2302 }
2303
2304 #[test]
2305 fn with_spec_transform_with_default_on_null() {
2306 let store = RunContext::new();
2307 store.insert(
2308 Arc::from("step1"),
2309 TaskResult::success(json!({"name": null}), Duration::from_secs(1)),
2310 );
2311
2312 let mut spec = WithSpec::default();
2313 let mut entry =
2314 WithEntry::with_default(BindingPath::parse("$step1.name").unwrap(), json!("DEFAULT"));
2315 entry.transform = Some(TransformExpr::parse("upper").unwrap());
2316 spec.insert("name".to_string(), entry);
2317
2318 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2321 assert_eq!(bindings.get("name"), Some(&json!("DEFAULT")));
2322 }
2323
2324 #[test]
2325 fn with_spec_transform_chain() {
2326 let store = RunContext::new();
2327 store.insert(
2328 Arc::from("step1"),
2329 TaskResult::success(json!({"items": [3, 1, 4, 1, 5, 9]}), Duration::from_secs(1)),
2330 );
2331
2332 let mut spec = WithSpec::default();
2333 let mut entry = WithEntry::simple(BindingPath::parse("$step1.items").unwrap());
2334 entry.transform = Some(TransformExpr::parse("sort | unique | length").unwrap());
2335 spec.insert("unique_count".to_string(), entry);
2336
2337 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2338 assert_eq!(bindings.get("unique_count"), Some(&json!(5)));
2340 }
2341
2342 #[test]
2347 fn with_spec_type_string_valid() {
2348 let store = RunContext::new();
2349 store.insert(
2350 Arc::from("step1"),
2351 TaskResult::success(json!({"name": "text"}), Duration::from_secs(1)),
2352 );
2353
2354 let mut spec = WithSpec::default();
2355 let mut entry = WithEntry::simple(BindingPath::parse("$step1.name").unwrap());
2356 entry.binding_type = BindingType::String;
2357 spec.insert("name".to_string(), entry);
2358
2359 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2360 assert_eq!(bindings.get("name"), Some(&json!("text")));
2361 }
2362
2363 #[test]
2364 fn with_spec_type_string_invalid() {
2365 let store = RunContext::new();
2366 store.insert(
2367 Arc::from("step1"),
2368 TaskResult::success(json!({"count": 42}), Duration::from_secs(1)),
2369 );
2370
2371 let mut spec = WithSpec::default();
2372 let mut entry = WithEntry::simple(BindingPath::parse("$step1.count").unwrap());
2373 entry.binding_type = BindingType::String;
2374 spec.insert("count".to_string(), entry);
2375
2376 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2377 assert!(result.is_err());
2378 let err_str = result.unwrap_err().to_string();
2379 assert!(err_str.contains("NIKA-043")); }
2381
2382 #[test]
2383 fn with_spec_type_array_valid() {
2384 let store = RunContext::new();
2385 store.insert(
2386 Arc::from("step1"),
2387 TaskResult::success(json!({"items": [1, 2, 3]}), Duration::from_secs(1)),
2388 );
2389
2390 let mut spec = WithSpec::default();
2391 let mut entry = WithEntry::simple(BindingPath::parse("$step1.items").unwrap());
2392 entry.binding_type = BindingType::Array;
2393 spec.insert("items".to_string(), entry);
2394
2395 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2396 assert_eq!(bindings.get("items"), Some(&json!([1, 2, 3])));
2397 }
2398
2399 #[test]
2400 fn with_spec_type_any_accepts_all() {
2401 let store = RunContext::new();
2402 store.insert(
2403 Arc::from("step1"),
2404 TaskResult::success(json!({"val": [1, "mixed"]}), Duration::from_secs(1)),
2405 );
2406
2407 let mut spec = WithSpec::default();
2408 let mut entry = WithEntry::simple(BindingPath::parse("$step1.val").unwrap());
2409 entry.binding_type = BindingType::Any;
2410 spec.insert("val".to_string(), entry);
2411
2412 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2413 assert_eq!(bindings.get("val"), Some(&json!([1, "mixed"])));
2414 }
2415
2416 #[test]
2417 fn with_spec_type_object_valid() {
2418 let store = RunContext::new();
2419 store.insert(
2420 Arc::from("step1"),
2421 TaskResult::success(json!({"cfg": {"debug": true}}), Duration::from_secs(1)),
2422 );
2423
2424 let mut spec = WithSpec::default();
2425 let mut entry = WithEntry::simple(BindingPath::parse("$step1.cfg").unwrap());
2426 entry.binding_type = BindingType::Object;
2427 spec.insert("cfg".to_string(), entry);
2428
2429 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2430 assert_eq!(bindings.get("cfg"), Some(&json!({"debug": true})));
2431 }
2432
2433 #[test]
2434 fn with_spec_type_number_valid() {
2435 let store = RunContext::new();
2436 store.insert(
2437 Arc::from("step1"),
2438 TaskResult::success(json!({"temp": 25.5}), Duration::from_secs(1)),
2439 );
2440
2441 let mut spec = WithSpec::default();
2442 let mut entry = WithEntry::simple(BindingPath::parse("$step1.temp").unwrap());
2443 entry.binding_type = BindingType::Number;
2444 spec.insert("temp".to_string(), entry);
2445
2446 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2447 assert_eq!(bindings.get("temp"), Some(&json!(25.5)));
2448 }
2449
2450 #[test]
2451 fn with_spec_type_integer_valid() {
2452 let store = RunContext::new();
2453 store.insert(
2454 Arc::from("step1"),
2455 TaskResult::success(json!({"count": 42}), Duration::from_secs(1)),
2456 );
2457
2458 let mut spec = WithSpec::default();
2459 let mut entry = WithEntry::simple(BindingPath::parse("$step1.count").unwrap());
2460 entry.binding_type = BindingType::Integer;
2461 spec.insert("count".to_string(), entry);
2462
2463 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2464 assert_eq!(bindings.get("count"), Some(&json!(42)));
2465 }
2466
2467 #[test]
2468 fn with_spec_type_integer_rejects_float() {
2469 let store = RunContext::new();
2470 store.insert(
2471 Arc::from("step1"),
2472 TaskResult::success(json!({"val": 3.12}), Duration::from_secs(1)),
2473 );
2474
2475 let mut spec = WithSpec::default();
2476 let mut entry = WithEntry::simple(BindingPath::parse("$step1.val").unwrap());
2477 entry.binding_type = BindingType::Integer;
2478 spec.insert("val".to_string(), entry);
2479
2480 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2481 assert!(result.is_err());
2482 assert!(result.unwrap_err().to_string().contains("NIKA-043"));
2483 }
2484
2485 #[test]
2486 fn with_spec_type_boolean_valid() {
2487 let store = RunContext::new();
2488 store.insert(
2489 Arc::from("step1"),
2490 TaskResult::success(json!({"flag": true}), Duration::from_secs(1)),
2491 );
2492
2493 let mut spec = WithSpec::default();
2494 let mut entry = WithEntry::simple(BindingPath::parse("$step1.flag").unwrap());
2495 entry.binding_type = BindingType::Boolean;
2496 spec.insert("flag".to_string(), entry);
2497
2498 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2499 assert_eq!(bindings.get("flag"), Some(&json!(true)));
2500 }
2501
2502 #[test]
2507 fn with_spec_mixed_sources() {
2508 use rustc_hash::FxHashMap;
2509
2510 let store = RunContext::new();
2511
2512 store.insert(
2514 Arc::from("step1"),
2515 TaskResult::success(json!({"result": "task_val"}), Duration::from_secs(1)),
2516 );
2517
2518 let mut inputs = FxHashMap::default();
2520 inputs.insert(
2521 "topic".to_string(),
2522 json!({"type": "string", "default": "AI"}),
2523 );
2524 store.set_inputs(inputs);
2525
2526 {
2528 use crate::store::LoadedContext;
2529 let mut ctx = LoadedContext::new();
2530 ctx.files.insert("brand".to_string(), json!("Brand Text"));
2531 store.set_context(ctx);
2532 }
2533
2534 std::env::set_var("NIKA_TEST_MIXED_8A", "env_val");
2536
2537 let mut spec = WithSpec::default();
2538 spec.insert(
2539 "from_task".to_string(),
2540 WithEntry::simple(BindingPath::parse("$step1.result").unwrap()),
2541 );
2542 spec.insert(
2543 "from_input".to_string(),
2544 WithEntry::simple(BindingPath::parse("$inputs.topic").unwrap()),
2545 );
2546 spec.insert(
2547 "from_context".to_string(),
2548 WithEntry::simple(BindingPath::parse("$context.files.brand").unwrap()),
2549 );
2550 spec.insert(
2551 "from_env".to_string(),
2552 WithEntry::simple(BindingPath::parse("$env.NIKA_TEST_MIXED_8A").unwrap()),
2553 );
2554
2555 let bindings = ResolvedBindings::from_with_spec(Some(&spec), &store).unwrap();
2556
2557 assert_eq!(bindings.get("from_task"), Some(&json!("task_val")));
2558 assert_eq!(bindings.get("from_input"), Some(&json!("AI")));
2559 assert_eq!(bindings.get("from_context"), Some(&json!("Brand Text")));
2560 assert_eq!(bindings.get("from_env"), Some(&json!("env_val")));
2561
2562 std::env::remove_var("NIKA_TEST_MIXED_8A");
2563 }
2564
2565 #[test]
2570 fn with_spec_loop_var_errors() {
2571 let store = RunContext::new();
2572
2573 let mut spec = WithSpec::default();
2574 spec.insert(
2575 "item".to_string(),
2576 WithEntry::simple(BindingPath {
2577 source: BindingSource::LoopVar(Arc::from("item")),
2578 segments: vec![],
2579 }),
2580 );
2581
2582 let result = ResolvedBindings::from_with_spec(Some(&spec), &store);
2583 assert!(result.is_err());
2584 let err_str = result.unwrap_err().to_string();
2585 assert!(err_str.contains("loop variable"));
2586 }
2587
2588 #[test]
2593 fn navigate_segments_empty() {
2594 let value = json!({"hello": "world"});
2595 let result = navigate_segments(&value, &[]).unwrap();
2596 assert_eq!(result, Some(json!({"hello": "world"})));
2597 }
2598
2599 #[test]
2600 fn navigate_segments_field() {
2601 let value = json!({"name": "Nika"});
2602 let segments = vec![PathSegment::Field(Arc::from("name"))];
2603 let result = navigate_segments(&value, &segments).unwrap();
2604 assert_eq!(result, Some(json!("Nika")));
2605 }
2606
2607 #[test]
2608 fn navigate_segments_deep_field() {
2609 let value = json!({"a": {"b": {"c": 42}}});
2610 let segments = vec![
2611 PathSegment::Field(Arc::from("a")),
2612 PathSegment::Field(Arc::from("b")),
2613 PathSegment::Field(Arc::from("c")),
2614 ];
2615 let result = navigate_segments(&value, &segments).unwrap();
2616 assert_eq!(result, Some(json!(42)));
2617 }
2618
2619 #[test]
2620 fn navigate_segments_array_index() {
2621 let value = json!({"items": ["a", "b", "c"]});
2622 let segments = vec![
2623 PathSegment::Field(Arc::from("items")),
2624 PathSegment::Index(1),
2625 ];
2626 let result = navigate_segments(&value, &segments).unwrap();
2627 assert_eq!(result, Some(json!("b")));
2628 }
2629
2630 #[test]
2631 fn navigate_segments_mixed() {
2632 let value = json!({"data": [{"name": "first"}, {"name": "second"}]});
2633 let segments = vec![
2634 PathSegment::Field(Arc::from("data")),
2635 PathSegment::Index(1),
2636 PathSegment::Field(Arc::from("name")),
2637 ];
2638 let result = navigate_segments(&value, &segments).unwrap();
2639 assert_eq!(result, Some(json!("second")));
2640 }
2641
2642 #[test]
2643 fn navigate_segments_missing_field() {
2644 let value = json!({"a": 1});
2645 let segments = vec![PathSegment::Field(Arc::from("missing"))];
2646 let result = navigate_segments(&value, &segments).unwrap();
2647 assert_eq!(result, None);
2648 }
2649
2650 #[test]
2651 fn navigate_segments_out_of_bounds() {
2652 let value = json!([1, 2, 3]);
2653 let segments = vec![PathSegment::Index(10)];
2654 let result = navigate_segments(&value, &segments).unwrap();
2655 assert_eq!(result, None);
2656 }
2657
2658 #[test]
2659 fn navigate_segments_field_on_non_object() {
2660 let value = json!("string_value");
2661 let segments = vec![PathSegment::Field(Arc::from("field"))];
2662 let result = navigate_segments(&value, &segments).unwrap();
2663 assert_eq!(result, None);
2664 }
2665
2666 #[test]
2667 fn navigate_segments_index_on_non_array() {
2668 let value = json!({"key": "val"});
2669 let segments = vec![PathSegment::Index(0)];
2670 let result = navigate_segments(&value, &segments).unwrap();
2671 assert_eq!(result, None);
2672 }
2673
2674 #[test]
2675 fn navigate_segments_json_string_auto_parse() {
2676 let value = json!(r#"{"name":"Nika","version":"0.30"}"#);
2678 let segments = vec![PathSegment::Field(Arc::from("name"))];
2679 let result = navigate_segments(&value, &segments).unwrap();
2680 assert_eq!(result, Some(json!("Nika")));
2681 }
2682
2683 #[test]
2684 fn navigate_segments_json_string_deep_access() {
2685 let value = json!(r#"{"a":{"b":{"c":"deep"}}}"#);
2686 let segments = vec![
2687 PathSegment::Field(Arc::from("a")),
2688 PathSegment::Field(Arc::from("b")),
2689 PathSegment::Field(Arc::from("c")),
2690 ];
2691 let result = navigate_segments(&value, &segments).unwrap();
2692 assert_eq!(result, Some(json!("deep")));
2693 }
2694
2695 #[test]
2696 fn navigate_segments_plain_string_returns_none() {
2697 let value = json!("hello world");
2699 let segments = vec![PathSegment::Field(Arc::from("name"))];
2700 let result = navigate_segments(&value, &segments).unwrap();
2701 assert_eq!(result, None);
2702 }
2703
2704 #[test]
2709 fn validate_type_any_accepts_all() {
2710 validate_binding_type(&json!("str"), BindingType::Any, "a", "p").unwrap();
2711 validate_binding_type(&json!(42), BindingType::Any, "a", "p").unwrap();
2712 validate_binding_type(&json!(true), BindingType::Any, "a", "p").unwrap();
2713 validate_binding_type(&json!([]), BindingType::Any, "a", "p").unwrap();
2714 validate_binding_type(&json!({}), BindingType::Any, "a", "p").unwrap();
2715 validate_binding_type(&json!(null), BindingType::Any, "a", "p").unwrap();
2716 }
2717
2718 #[test]
2719 fn validate_type_string_rejects_number() {
2720 let result = validate_binding_type(&json!(42), BindingType::String, "a", "p");
2721 assert!(result.is_err());
2722 }
2723
2724 #[test]
2725 fn validate_type_number_accepts_int_and_float() {
2726 validate_binding_type(&json!(42), BindingType::Number, "a", "p").unwrap();
2727 validate_binding_type(&json!(3.12), BindingType::Number, "a", "p").unwrap();
2728 }
2729
2730 #[test]
2731 fn validate_type_integer_rejects_float() {
2732 let result = validate_binding_type(&json!(3.12), BindingType::Integer, "a", "p");
2733 assert!(result.is_err());
2734 }
2735
2736 #[test]
2737 fn validate_type_boolean_rejects_string() {
2738 let result = validate_binding_type(&json!("true"), BindingType::Boolean, "a", "p");
2739 assert!(result.is_err());
2740 }
2741
2742 #[test]
2747 fn json_type_names() {
2748 assert_eq!(json_type_name(&json!(null)), "null");
2749 assert_eq!(json_type_name(&json!(true)), "boolean");
2750 assert_eq!(json_type_name(&json!(42)), "number");
2751 assert_eq!(json_type_name(&json!("str")), "string");
2752 assert_eq!(json_type_name(&json!([])), "array");
2753 assert_eq!(json_type_name(&json!({})), "object");
2754 }
2755
2756 #[test]
2759 fn source_task_id_set_with_source() {
2760 let mut bindings = ResolvedBindings::new();
2761 bindings.set_with_source("img", json!("output text"), "gen_img");
2762
2763 assert_eq!(bindings.source_task_id("img"), Some("gen_img"));
2764 assert_eq!(bindings.source_task_id("nonexistent"), None);
2765
2766 assert_eq!(bindings.get("img"), Some(&json!("output text")));
2768 }
2769
2770 #[test]
2771 fn source_task_id_plain_set_has_no_tracking() {
2772 let mut bindings = ResolvedBindings::new();
2773 bindings.set("img", json!("output text"));
2774
2775 assert_eq!(bindings.source_task_id("img"), None);
2777 }
2778
2779 #[test]
2780 fn source_task_id_from_binding_spec() {
2781 use crate::binding::BindingEntry;
2782
2783 let mut spec = BindingSpec::default();
2784 spec.insert("forecast".to_string(), BindingEntry::new("weather.summary"));
2785
2786 let store = RunContext::new();
2787 store.insert(
2788 std::sync::Arc::from("weather"),
2789 crate::store::TaskResult::success(
2790 json!({"summary": "Sunny"}),
2791 std::time::Duration::from_secs(1),
2792 ),
2793 );
2794
2795 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2796
2797 assert_eq!(bindings.source_task_id("forecast"), Some("weather"));
2799 }
2800
2801 fn store_with_media_chain() -> RunContext {
2816 let store = RunContext::new();
2817
2818 let gen_media = vec![crate::media::MediaRef {
2820 hash: "blake3:abc123def456".to_string(),
2821 mime_type: "image/png".to_string(),
2822 size_bytes: 1048576,
2823 path: std::path::PathBuf::from("/tmp/cas/ab/c123def456"),
2824 extension: "png".to_string(),
2825 created_by: "gen".to_string(),
2826 metadata: {
2827 let mut m = serde_json::Map::new();
2828 m.insert("width".to_string(), json!(1024));
2829 m.insert("height".to_string(), json!(768));
2830 m
2831 },
2832 }];
2833 store.insert(
2834 Arc::from("gen"),
2835 TaskResult::success(json!({"prompt": "a sunset photo"}), Duration::from_secs(3))
2836 .with_media(gen_media),
2837 );
2838
2839 store.insert(
2841 Arc::from("thumb"),
2842 TaskResult::success_str(
2843 r#"{"hash":"blake3:thumb_999","mime_type":"image/png","size_bytes":2048,"metadata":{"width":256,"height":192}}"#,
2844 Duration::from_millis(100),
2845 ),
2846 );
2847
2848 store
2849 }
2850
2851 #[test]
2854 fn binding_spec_resolves_invoke_output_hash() {
2855 use crate::binding::BindingEntry;
2856
2857 let store = store_with_media_chain();
2858 let mut spec = BindingSpec::default();
2859 spec.insert("thumb_hash".to_string(), BindingEntry::new("thumb.hash"));
2861
2862 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2863 let value = bindings.get_resolved("thumb_hash", &store).unwrap();
2864 assert_eq!(value, json!("blake3:thumb_999"));
2865 }
2866
2867 #[test]
2868 fn binding_spec_resolves_invoke_output_nested_metadata() {
2869 use crate::binding::BindingEntry;
2870
2871 let store = store_with_media_chain();
2872 let mut spec = BindingSpec::default();
2873 spec.insert(
2875 "thumb_width".to_string(),
2876 BindingEntry::new("thumb.metadata.width"),
2877 );
2878
2879 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2880 let value = bindings.get_resolved("thumb_width", &store).unwrap();
2881 assert_eq!(value, json!(256));
2882 }
2883
2884 #[test]
2885 fn binding_spec_resolves_invoke_output_mime_type() {
2886 use crate::binding::BindingEntry;
2887
2888 let store = store_with_media_chain();
2889 let mut spec = BindingSpec::default();
2890 spec.insert(
2891 "thumb_mime".to_string(),
2892 BindingEntry::new("thumb.mime_type"),
2893 );
2894
2895 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2896 let value = bindings.get_resolved("thumb_mime", &store).unwrap();
2897 assert_eq!(value, json!("image/png"));
2898 }
2899
2900 #[test]
2903 fn binding_spec_resolves_media_ref_hash() {
2904 use crate::binding::BindingEntry;
2905
2906 let store = store_with_media_chain();
2907 let mut spec = BindingSpec::default();
2908 spec.insert(
2910 "gen_hash".to_string(),
2911 BindingEntry::new("gen.media[0].hash"),
2912 );
2913
2914 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2915 let value = bindings.get_resolved("gen_hash", &store).unwrap();
2916 assert_eq!(value, json!("blake3:abc123def456"));
2917 }
2918
2919 #[test]
2920 fn binding_spec_resolves_media_ref_enriched_width() {
2921 use crate::binding::BindingEntry;
2922
2923 let store = store_with_media_chain();
2924 let mut spec = BindingSpec::default();
2925 spec.insert(
2927 "gen_width".to_string(),
2928 BindingEntry::new("gen.media[0].metadata.width"),
2929 );
2930
2931 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2932 let value = bindings.get_resolved("gen_width", &store).unwrap();
2933 assert_eq!(value, json!(1024));
2934 }
2935
2936 #[test]
2937 fn binding_spec_resolves_media_ref_mime_type() {
2938 use crate::binding::BindingEntry;
2939
2940 let store = store_with_media_chain();
2941 let mut spec = BindingSpec::default();
2942 spec.insert(
2943 "gen_mime".to_string(),
2944 BindingEntry::new("gen.media[0].mime_type"),
2945 );
2946
2947 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2948 let value = bindings.get_resolved("gen_mime", &store).unwrap();
2949 assert_eq!(value, json!("image/png"));
2950 }
2951
2952 #[test]
2953 fn binding_spec_resolves_media_full_array() {
2954 use crate::binding::BindingEntry;
2955
2956 let store = store_with_media_chain();
2957 let mut spec = BindingSpec::default();
2958 spec.insert("all_media".to_string(), BindingEntry::new("gen.media"));
2960
2961 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2962 let value = bindings.get_resolved("all_media", &store).unwrap();
2963 let arr = value.as_array().expect("media should be an array");
2964 assert_eq!(arr.len(), 1);
2965 assert_eq!(arr[0]["hash"], "blake3:abc123def456");
2966 }
2967
2968 #[test]
2971 fn binding_spec_chained_gen_media_and_thumb_output() {
2972 use crate::binding::BindingEntry;
2973
2974 let store = store_with_media_chain();
2975 let mut spec = BindingSpec::default();
2976
2977 spec.insert(
2979 "source_hash".to_string(),
2980 BindingEntry::new("gen.media[0].hash"),
2981 );
2982 spec.insert("thumb_hash".to_string(), BindingEntry::new("thumb.hash"));
2983 spec.insert(
2984 "thumb_width".to_string(),
2985 BindingEntry::new("thumb.metadata.width"),
2986 );
2987
2988 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
2989
2990 assert_eq!(
2992 bindings.get_resolved("source_hash", &store).unwrap(),
2993 json!("blake3:abc123def456"),
2994 "gen.media[0].hash should resolve via media side-channel"
2995 );
2996 assert_eq!(
2997 bindings.get_resolved("thumb_hash", &store).unwrap(),
2998 json!("blake3:thumb_999"),
2999 "thumb.hash should resolve via JSON-string auto-parse"
3000 );
3001 assert_eq!(
3002 bindings.get_resolved("thumb_width", &store).unwrap(),
3003 json!(256),
3004 "thumb.metadata.width should resolve via nested JSON-string traversal"
3005 );
3006 }
3007
3008 #[test]
3009 fn binding_spec_lazy_media_ref_resolves_on_access() {
3010 use crate::binding::BindingEntry;
3011
3012 let store = store_with_media_chain();
3013 let mut spec = BindingSpec::default();
3014 spec.insert(
3016 "lazy_hash".to_string(),
3017 BindingEntry {
3018 path: "gen.media[0].hash".to_string(),
3019 default: None,
3020 lazy: true,
3021 },
3022 );
3023
3024 let bindings = ResolvedBindings::from_binding_spec(Some(&spec), &store).unwrap();
3025
3026 assert!(bindings.is_lazy("lazy_hash"));
3028
3029 let value = bindings.get_resolved("lazy_hash", &store).unwrap();
3031 assert_eq!(value, json!("blake3:abc123def456"));
3032 }
3033}