#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod support;
use turnframe_core::case::CaseRef;
use turnframe_core::command::CommandPolicy;
use turnframe_core::event::ReceiptEvent;
use turnframe_core::flow::{WorkflowDefinition, WorkflowExecutor};
use turnframe_core::ids::{AccountId, BlockId, CaseRevision, ConversationId, TurnId};
use turnframe_core::locale::Locale;
use turnframe_core::policy::PolicySnapshot;
use turnframe_core::reduce::CommandRef;
use turnframe_core::replay::{CommandOutcome, CommandOutcomeRecord, ReplayRecord};
use turnframe_core::response::{
AssistantTurn, GeneratedTransition, NarratableFact, ReceiptBlock, ReplayToken, ResponseBlock,
};
use turnframe_core::understanding::Understanding;
use turnframe_provider::prelude::{Message, ModelPurpose, ModelRequest, OutputSpec};
use turnframe_provider::provider::ModelProvider;
use turnframe_test::providers::{
ScriptViolation, ScriptedProvider, ScriptedReply, UnderstandingBuilder,
};
use turnframe_test::replay::{ReplayDivergence, ReplayEvidence, TurnExecution, same_turn};
use turnframe_test::workflows::trip::{
SAMPLE_NAME, TripCommand, TripEvent, TripExecutor, TripWorkflow, operations,
};
const TEXT: &str = "Cambia il nome in Lisbona e dimmi quando parto";
fn understanding() -> Understanding {
UnderstandingBuilder::of(TEXT)
.apply(
operations::SET_NAME,
"tok_trip_1",
serde_json::json!({"value": SAMPLE_NAME}),
"Cambia il nome in Lisbona",
)
.ask("dimmi quando parto")
.build()
.expect("the quotes come from the text")
}
fn segmentation() -> serde_json::Value {
serde_json::json!({
"analysis": "Names the trip, then asks when the flight leaves.",
"units": [
{"kind": "request", "words": {"from": 1, "to": 5}, "workflow": "trip"},
{"kind": "question", "words": {"from": 7, "to": 9}, "workflow": "trip",
"basis": "current_committed_state", "continues_previous": false}
]
})
}
fn segment_request() -> ModelRequest {
ModelRequest::new(ModelPurpose::Segment)
.with_system("Split the user's message into units.")
.with_message(Message::user(TEXT))
.with_output(OutputSpec::json(
"segment",
serde_json::json!({
"type": "object",
"properties": {"workflow": {"enum": ["trip", "unknown"]}}
}),
))
}
#[tokio::test]
async fn a_scripted_turn_makes_exactly_the_calls_the_script_declares() {
let provider = ScriptedProvider::builder("scripted", "m")
.reply_to(ModelPurpose::Segment, ScriptedReply::Json(segmentation()))
.reply_to(
ModelPurpose::Acknowledge,
ScriptedReply::text("Ho cambiato il nome."),
)
.build();
let first = provider.generate(segment_request()).await.unwrap();
let units: serde_json::Value = serde_json::from_str(&first.text()).unwrap();
assert_eq!(units, segmentation());
let second = provider
.generate(ModelRequest::new(ModelPurpose::Acknowledge).with_message(Message::user(TEXT)))
.await
.unwrap();
assert_eq!(second.text(), "Ho cambiato il nome.");
let calls = provider.calls();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].schema_name(), Some("segment"));
assert!(calls[0].schema_mentions("trip"));
assert!(!calls[0].schema_mentions("traveler"));
assert_eq!(calls[1].purpose(), ModelPurpose::Acknowledge);
assert_eq!(calls[1].user_text(), TEXT);
provider.verify().expect("the script was followed exactly");
}
#[tokio::test]
async fn one_model_call_too_many_is_reported_even_when_it_is_swallowed() {
let provider = ScriptedProvider::builder("scripted", "m")
.reply_to(ModelPurpose::Segment, ScriptedReply::Json(segmentation()))
.build();
provider.generate(segment_request()).await.unwrap();
let swallowed = provider.generate(segment_request()).await;
assert!(swallowed.is_err());
assert_eq!(
provider.verify().unwrap_err(),
ScriptViolation::UnexpectedCall {
call_index: 1,
purpose: "segment",
}
);
assert_eq!(provider.call_count(), 2);
}
async fn run(executor: &TripExecutor, command: TripCommand) -> Vec<ReceiptEvent<TripEvent>> {
let case_ref = CaseRef::new("trip", "trip-1", CaseRevision(0));
let batch = support::batch(
support::turn(1),
&case_ref,
&support::confirmed_click(),
vec![command],
);
executor
.execute(batch)
.await
.expect("the command commits")
.receipt_events()
}
async fn execution(executor: &TripExecutor, command: TripCommand) -> TurnExecution {
let workflow = TripWorkflow::default();
let case_ref = CaseRef::new("trip", "trip-1", CaseRevision(0));
let batch = support::batch(
support::turn(1),
&case_ref,
&support::confirmed_click(),
vec![command.clone()],
);
let commit = executor
.execute(batch.clone())
.await
.expect("the command commits");
let receipts = workflow.receipts(&commit.receipt_events(), &Locale::from("it-IT"));
let envelope = &batch.envelopes[0];
let command_ref = CommandRef {
batch_id: batch.batch_id,
command_id: envelope.command_id,
};
let mut record = ReplayRecord::received(
support::turn(1),
ConversationId::nil(),
AccountId::from(support::ACCOUNT),
commit
.events
.first()
.map_or_else(chrono::Utc::now, |event| event.occurred_at),
);
record.understanding = Some(understanding());
record
.policy_decisions
.push(PolicySnapshot::conservative().decide(
command_ref,
&workflow.command_policy(None, &command),
&envelope.origin,
));
record.command_outcomes.push(CommandOutcomeRecord {
command_ref,
idempotency_key: envelope.idempotency_key.clone(),
case_ref,
origin: Some(envelope.origin.clone()),
outcome: CommandOutcome::Committed {
new_revision: commit.new_revision,
event_ids: commit.event_ids(),
},
});
record.event_ids = commit.event_ids();
let mut blocks: Vec<ResponseBlock> = receipts
.iter()
.map(|receipt| {
ResponseBlock::Receipt(ReceiptBlock {
block_id: BlockId::new(format!("receipt-{}", receipt.receipt_id)),
receipt: receipt.clone(),
})
})
.collect();
if let Some(last) = receipts.last() {
blocks.push(ResponseBlock::Transition(GeneratedTransition {
block_id: BlockId::from("transition-1"),
text: "Ecco cosa ho fatto.".to_owned(),
facts_used: vec![NarratableFact::OperationalOutcome {
receipt_id: last.receipt_id,
event_ids: last.event_ids.clone(),
status_code: last.status_code.clone(),
}],
}));
}
record.response_block_ids = blocks
.iter()
.map(|block| block.block_id().clone())
.collect();
let response = AssistantTurn {
turn_id: support::turn(1),
conversation_id: ConversationId::nil(),
blocks,
subjects: Vec::new(),
expectations: Vec::new(),
replay_token: ReplayToken::from("replay-1"),
done: Vec::new(),
};
TurnExecution::new(record, response)
}
#[tokio::test]
async fn two_runs_of_the_same_turn_produce_the_same_artefacts() {
let first = execution(&TripExecutor::default(), TripCommand::Open).await;
let second = execution(&TripExecutor::default(), TripCommand::Open).await;
same_turn(&first, &second).expect("the same turn twice produces the same artefacts");
assert!(!first.record.event_ids.is_empty(), "the turn did something");
}
#[tokio::test]
async fn a_turn_that_did_something_else_is_reported() {
let first = execution(&TripExecutor::default(), TripCommand::Open).await;
let mut second = first.clone();
second.record.understanding = Some(
UnderstandingBuilder::of(TEXT)
.start("trip", "Cambia")
.build()
.unwrap(),
);
assert!(matches!(
same_turn(&first, &second).unwrap_err(),
ReplayDivergence::PlanDiffers { .. }
));
}
#[tokio::test]
async fn a_record_built_from_a_real_commit_explains_its_turn() {
let executor = TripExecutor::default();
let workflow = TripWorkflow::default();
let case_ref = CaseRef::new("trip", "trip-1", CaseRevision(0));
let batch = support::batch(
support::turn(1),
&case_ref,
&support::confirmed_click(),
vec![TripCommand::Open],
);
let commit = executor.execute(batch.clone()).await.unwrap();
let receipts = workflow.receipts(&commit.receipt_events(), &Locale::from("it-IT"));
let execution = execution(&TripExecutor::default(), TripCommand::Open).await;
ReplayEvidence::new(&execution.record)
.with_batch(&batch)
.with_receipts(&receipts)
.explains_its_turn()
.expect("every command has a decision, an origin and its events");
let gaps = ReplayEvidence::new(&execution.record)
.with_receipts(&receipts)
.explains_its_turn()
.unwrap_err();
assert!(gaps.to_string().contains("has no origin"), "{gaps}");
}
#[tokio::test]
async fn a_receipt_that_outruns_the_ledger_is_reported() {
let executor = TripExecutor::default();
let workflow = TripWorkflow::default();
let events = run(&executor, TripCommand::Open).await;
let receipts = workflow.receipts(&events, &Locale::from("it-IT"));
let mut execution = execution(&TripExecutor::default(), TripCommand::Open).await;
execution.record.event_ids.clear();
execution.record.command_outcomes.clear();
let gaps = ReplayEvidence::new(&execution.record)
.with_receipts(&receipts)
.explains_its_turn()
.unwrap_err();
assert!(
gaps.to_string().contains("which the record does not list"),
"{gaps}"
);
}
#[test]
fn a_low_risk_command_needs_no_confirmation_for_the_record_to_hold() {
let workflow = TripWorkflow::default();
assert_eq!(
workflow.command_policy(None, &TripCommand::Open),
CommandPolicy::low_risk()
);
assert_eq!(TurnId::nil(), TurnId::nil());
}