use super::*;
use crate::core::config::e2e::CallConfig;
use crate::core::ir::{FieldDef, FunctionDef, TypeDef, TypeRef};
fn field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
optional,
..FieldDef::default()
}
}
fn config() -> E2eConfig {
E2eConfig {
call: CallConfig {
function: "interact".into(),
result_var: "result".into(),
..CallConfig::default()
},
result_fields: ["action_results".to_string()].into_iter().collect(),
..E2eConfig::default()
}
}
fn type_defs() -> Vec<TypeDef> {
vec![
TypeDef {
name: "InteractionResult".to_string(),
fields: vec![field(
"action_results",
TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named(
"ActionResult".to_string(),
))))),
true,
)],
..TypeDef::default()
},
TypeDef {
name: "ActionResult".to_string(),
fields: vec![field("action_type", TypeRef::String, false)],
..TypeDef::default()
},
TypeDef {
name: "UnrelatedBatch".to_string(),
fields: vec![field(
"action_results",
TypeRef::Vec(Box::new(TypeRef::Named("ActionResult".to_string()))),
false,
)],
..TypeDef::default()
},
]
}
fn returning(type_name: &str) -> Vec<FunctionDef> {
vec![FunctionDef {
name: "interact".to_string(),
return_type: TypeRef::Named(type_name.to_string()),
..FunctionDef::default()
}]
}
#[test]
fn an_iterate_over_a_namespace_prefixed_optional_array_field_gets_the_nullish_fallback() {
let fixture = docs_fixture_with_presentation(FixtureDocsOperation::Iterate {
path: "interaction.action_results".into(),
item: "action".into(),
fields: vec!["action_type".into()],
display: true,
optional: false,
});
let operations = resolve(
&fixture,
&config(),
"node",
&type_defs(),
&[],
&returning("InteractionResult"),
);
assert_eq!(
operations.len(),
1,
"expected exactly one iterate operation: {operations:?}"
);
assert!(
operations[0].optional,
"the anchored IR says `action_results` is `Option<Vec<T>>`; the namespace label in front \
of it must not hide that from the `?? []` guard: {operations:?}"
);
}
#[test]
fn a_show_into_an_element_of_a_namespace_prefixed_optional_array_field_gets_optional_chaining() {
let fixture = docs_fixture_with_presentation(FixtureDocsOperation::Show {
path: "interaction.action_results[0].action_type".into(),
display: false,
});
let operations = resolve(
&fixture,
&config(),
"node",
&type_defs(),
&[],
&returning("InteractionResult"),
);
assert_eq!(
operations.iter().map(|o| o.expression.as_str()).collect::<Vec<_>>(),
vec!["result.actionResults?.[0]?.actionType"],
"the array itself is optional, so both the index and the field past it must chain \
through `?.`"
);
}
#[test]
fn a_show_into_an_element_of_the_same_namespace_prefixed_optional_field_is_guarded_for_python_too() {
let fixture = docs_fixture_with_presentation(FixtureDocsOperation::Show {
path: "interaction.action_results[0].action_type".into(),
display: false,
});
let operations = resolve(
&fixture,
&config(),
"python",
&type_defs(),
&[],
&returning("InteractionResult"),
);
assert_eq!(
operations.iter().map(|o| o.expression.as_str()).collect::<Vec<_>>(),
vec!["(result.action_results[0].action_type if result.action_results else None)"],
"python's own conditional-expression guard must also fire once the anchored path is \
looked up under its stripped spelling: {operations:?}"
);
}
fn docs_fixture_with_presentation(operation: FixtureDocsOperation) -> Fixture {
serde_json::from_value(serde_json::json!({
"id": "interact_action_sequence",
"description": "Run an interaction and inspect its action results",
"input": {},
"docs": {
"topic": "interaction",
"stem": "action-sequence",
"presentation": {
"operations": [operation]
}
}
}))
.expect("fixture must parse")
}