#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod support;
use std::sync::Arc;
use support::{Harness, token_for};
use turnframe_core::error::{DomainRejection, ExecutionError};
use turnframe_core::ids::TurnId;
use turnframe_core::locale::{Locale, LocalizedText};
use turnframe_core::response::{NarratableFact, ResponseBlock};
use turnframe_provider::purpose::ModelPurpose;
use turnframe_test::providers::{ScriptedProvider, UnderstandingBuilder};
use turnframe_test::workflows::trip::{incomplete_case, operations};
fn turn_one() -> TurnId {
TurnId::from(uuid::Uuid::from_u128(1))
}
async fn refused_at_execution() -> (
turnframe_core::response::AssistantTurn,
Arc<ScriptedProvider>,
) {
let turn = turn_one();
let text = "a mario ferri";
let provider = noting();
let harness = Harness::builder()
.trip("trip-1", "Trip 1", 3, incomplete_case())
.understands(naming_a_traveler(turn, text))
.trip_fails(ExecutionError::Rejected(
DomainRejection::new("trip.recipient_unresolved", "recipient_unresolved")
.with_explanation(LocalizedText::new(
"No traveler of that name is in the registry.",
)),
))
.provider(Arc::clone(&provider))
.build()
.await;
let answered = harness.handle(harness.turn(turn, text)).await.unwrap();
(answered, provider)
}
fn naming_a_traveler(turn: TurnId, text: &str) -> turnframe_core::understanding::Understanding {
UnderstandingBuilder::of(text)
.apply(
operations::SET_NAME,
token_for(turn, "trip", "trip-1"),
serde_json::json!({"value": "mario ferri"}),
text,
)
.build()
.unwrap()
}
fn noting() -> Arc<ScriptedProvider> {
ScriptedProvider::builder("scripted", "model-1")
.acknowledging("Noted.")
.build_shared()
}
#[tokio::test]
async fn a_refusal_at_execution_reaches_the_writer() {
let (_, provider) = refused_at_execution().await;
let brief = provider
.calls_for(ModelPurpose::Acknowledge)
.into_iter()
.next()
.expect("the writing stage ran")
.request
.messages
.iter()
.flat_map(|message| message.content.iter())
.filter_map(|part| match part {
turnframe_provider::request::ContentPart::Text { text } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
assert!(
brief.contains("\"not_done\": [\n \""),
"the fact that stops the prose claiming the write: {brief}"
);
assert!(
brief.contains("No traveler of that name is in the registry."),
"and the domain's own sentence, which used to be dropped: {brief}"
);
}
#[tokio::test]
async fn the_reader_is_given_the_domain_s_own_reason() {
let (answered, _) = refused_at_execution().await;
let notices: Vec<String> = answered
.blocks
.iter()
.filter_map(|block| match block {
ResponseBlock::Notice(notice) => {
Some(notice.text.resolve(&Locale::from("en-GB")).to_owned())
}
_ => None,
})
.collect();
assert!(
notices
.iter()
.any(|text| text.contains("No traveler of that name")),
"{notices:?}"
);
}
#[tokio::test]
async fn a_rejection_with_no_explanation_keeps_the_generic_notice() {
let turn = turn_one();
let text = "a mario ferri";
let harness = Harness::builder()
.trip("trip-1", "Trip 1", 3, incomplete_case())
.understands(naming_a_traveler(turn, text))
.trip_fails(ExecutionError::Rejected(DomainRejection::new(
"trip.recipient_unresolved",
"recipient_unresolved",
)))
.provider(noting())
.build()
.await;
let answered = harness.handle(harness.turn(turn, text)).await.unwrap();
let codes: Vec<String> = answered
.blocks
.iter()
.filter_map(|block| match block {
ResponseBlock::Notice(notice) => Some(notice.code.clone()),
_ => None,
})
.collect();
assert!(
codes
.iter()
.any(|code| code == "turnframe.notice.command_failed"),
"{codes:?}"
);
let refused = answered.blocks.iter().any(|block| match block {
ResponseBlock::Transition(block) => block
.facts_used
.iter()
.any(|fact| matches!(fact, NarratableFact::ActRefused { .. })),
_ => false,
});
assert!(refused, "a refusal with no sentence is still a refusal");
}