#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod support;
use support::{Harness, token_for};
use turnframe_core::ids::TurnId;
use turnframe_core::locale::Locale;
use turnframe_core::response::{AssistantTurn, ResponseBlock};
use turnframe_core::understanding::{ActTarget, Understanding};
use turnframe_runtime::reduce::{NoticeCopy, rejection};
use turnframe_test::providers::UnderstandingBuilder;
use turnframe_test::workflows::trip::{incomplete_case, operations};
fn turn_one() -> TurnId {
TurnId::from(uuid::Uuid::from_u128(1))
}
fn english() -> Locale {
Locale::from("en-GB")
}
const CANCEL_THAT: &str = "cancel that";
fn cancelling_a_new_record() -> Understanding {
UnderstandingBuilder::of(CANCEL_THAT)
.open(
operations::WITHDRAW,
"trip",
serde_json::json!(null),
CANCEL_THAT,
)
.build()
.unwrap()
}
fn notices(turn: &AssistantTurn, locale: &Locale) -> Vec<(String, String)> {
turn.blocks
.iter()
.filter_map(|block| match block {
ResponseBlock::Notice(notice) => {
Some((notice.code.clone(), notice.text.resolve(locale).to_owned()))
}
_ => None,
})
.collect()
}
async fn aiming_a_cancellation_at_a_record_that_does_not_exist(
copy: Option<NoticeCopy>,
locale: Locale,
) -> Vec<(String, String)> {
let turn = turn_one();
let mut builder = Harness::builder()
.trip("trip-1", "Ferri", 3, incomplete_case())
.understands(cancelling_a_new_record())
.without_narration();
if let Some(copy) = copy {
builder = builder.notice_copy(copy);
}
let harness = builder.build().await;
let mut input = harness.turn(turn, CANCEL_THAT);
input.locale = locale.clone();
let answered = harness.handle(input).await.unwrap();
notices(&answered, &locale)
}
#[tokio::test]
async fn a_runtime_refusal_says_which_refusal_it_is() {
let raised = Box::pin(aiming_a_cancellation_at_a_record_that_does_not_exist(
None,
english(),
))
.await;
let (_, text) = raised
.iter()
.find(|(code, _)| code == rejection::TARGET_POLICY_MISMATCH)
.expect("the refusal is reported under its own code");
assert!(
text.contains("which record"),
"and in words about this refusal rather than about refusals in general: {text}"
);
assert!(
!raised
.iter()
.any(|(_, text)| text.contains("could not be done")),
"the catch-all is not what the user reads: {raised:?}"
);
}
#[tokio::test]
async fn a_runtime_refusal_can_be_translated() {
let mut copy = NoticeCopy::english();
copy.target_policy_mismatch = copy.target_policy_mismatch.with(
Locale::from("it-IT"),
"Non ho capito su quale pratica dovevo scrivere.",
);
let raised = Box::pin(aiming_a_cancellation_at_a_record_that_does_not_exist(
Some(copy),
Locale::from("it-IT"),
))
.await;
assert!(
raised
.iter()
.any(|(_, text)| text == "Non ho capito su quale pratica dovevo scrivere."),
"{raised:?}"
);
}
#[tokio::test]
async fn an_untranslated_locale_keeps_the_shipped_sentence() {
let raised = Box::pin(aiming_a_cancellation_at_a_record_that_does_not_exist(
None,
Locale::from("it-IT"),
))
.await;
assert!(
raised
.iter()
.any(|(code, text)| code == rejection::TARGET_POLICY_MISMATCH && !text.is_empty()),
"{raised:?}"
);
}
#[tokio::test]
async fn two_different_refusals_reach_the_user_as_two_sentences() {
let turn = turn_one();
let text = "Set the name on the Bianchi trip to Lisbon, and yes to the question";
let understanding = UnderstandingBuilder::of(text)
.apply_to(
operations::SET_NAME,
ActTarget::NotListed {
workflow: "trip".into(),
words: None,
},
serde_json::json!({"value": "Lisbon"}),
"Set the name on the Bianchi trip to Lisbon",
)
.open(
operations::WITHDRAW,
"trip",
serde_json::json!(null),
"yes to the question",
)
.build()
.unwrap();
let harness = Harness::builder()
.trip("trip-1", "Ferri", 3, incomplete_case())
.understands(understanding)
.without_narration()
.build()
.await;
let answered = harness.handle(harness.turn(turn, text)).await.unwrap();
let raised = notices(&answered, &english());
let codes: Vec<&str> = raised.iter().map(|(code, _)| code.as_str()).collect();
assert!(codes.contains(&rejection::TARGET_MISSING), "{codes:?}");
assert!(
codes.contains(&rejection::TARGET_POLICY_MISMATCH),
"{codes:?}"
);
let texts: std::collections::BTreeSet<&str> =
raised.iter().map(|(_, text)| text.as_str()).collect();
assert_eq!(
texts.len(),
raised.len(),
"two failures, two different sentences: {raised:?}"
);
}
#[tokio::test]
async fn a_domain_rejection_keeps_its_own_notice_code() {
let turn = turn_one();
let text = "Set the name to nothing at all";
let understanding = UnderstandingBuilder::of(text)
.apply(
operations::SET_NAME,
token_for(turn, "trip", "trip-1"),
serde_json::json!({"value": ""}),
text,
)
.build()
.unwrap();
let harness = Harness::builder()
.trip("trip-1", "Ferri", 3, incomplete_case())
.understands(understanding)
.without_narration()
.build()
.await;
let answered = harness.handle(harness.turn(turn, text)).await.unwrap();
let raised = notices(&answered, &english());
assert!(
raised
.iter()
.any(|(code, _)| code == "turnframe.notice.act_refused"),
"{raised:?}"
);
assert!(
raised
.iter()
.all(|(code, _)| !code.starts_with("turnframe.target.")),
"a domain refusal is not filed as one of the runtime's own: {raised:?}"
);
}