#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod support;
use futures::executor::block_on;
use proptest::prelude::*;
use turnframe_core::case::CaseRef;
use turnframe_core::command::{CommandOrigin, origin_satisfies};
use turnframe_core::error::ExecutionError;
use turnframe_core::flow::{WorkflowDefinition, WorkflowExecutor, check_view};
use turnframe_core::hash::Digest;
use turnframe_core::ids::{CaseId, CaseRevision};
use turnframe_core::turn::TurnInput;
use turnframe_core::understanding::Understanding;
use turnframe_test::assertions::{no_high_risk_without_trusted_origin, origin_satisfies_policy};
use turnframe_test::explore::{SimulatedTransition, WorkflowModel};
use turnframe_test::strategies;
use turnframe_test::workflows::traveler::{TravelerCommand, TravelerWorkflow};
use turnframe_test::workflows::trip::{TripCommand, TripModel, TripState, TripWorkflow};
fn walk_trip(choices: &[u8]) -> Option<TripState> {
let model = TripModel::default();
let mut state: Option<TripState> = None;
for choice in choices {
let candidates = model.candidate_commands(state.as_ref());
if candidates.is_empty() {
break;
}
let command = &candidates[usize::from(*choice) % candidates.len()];
if let SimulatedTransition::Applied { state: next, .. } =
model.simulate(state.as_ref(), command)
{
state = next;
}
}
state
}
fn text_and_understanding() -> impl Strategy<Value = (String, Understanding)> {
strategies::user_text().prop_flat_map(|text| {
let understanding = strategies::grounded_understanding(&text);
(Just(text), understanding)
})
}
fn turn_and_understanding() -> impl Strategy<Value = (TurnInput, Understanding)> {
strategies::turn_input().prop_flat_map(|turn| {
let understanding =
strategies::grounded_understanding(turn.text.as_deref().unwrap_or_default());
(Just(turn), understanding)
})
}
fn ranges_are_words_of(text: &str, understanding: &Understanding) -> bool {
let words = turnframe_understand::Words::split(text);
let ranges = understanding
.acts
.iter()
.map(|act| act.words)
.chain(understanding.questions.iter().map(|q| q.words))
.chain(understanding.constraints.iter().map(|c| c.words));
ranges.into_iter().all(|range| {
words
.range(turnframe_understand::Span::new(range.first, range.last))
.is_ok_and(|found| found == range)
})
}
proptest! {
#[test]
fn grounded_understandings_point_at_words((text, understanding) in text_and_understanding()) {
prop_assert!(ranges_are_words_of(&text, &understanding));
}
#[test]
fn grounded_understandings_fit_their_turn((turn, understanding) in turn_and_understanding()) {
prop_assert_eq!(turn.validate_shape(), Ok(()));
prop_assert!(ranges_are_words_of(turn.text.as_deref().unwrap_or_default(), &understanding));
}
#[test]
fn generated_interactions_are_answerable(interaction in strategies::interaction()) {
prop_assert!(interaction.verify_payload_hash().unwrap());
prop_assert!(interaction.payload.validate_for(interaction.kind).is_ok());
}
#[test]
fn the_origin_check_agrees_with_core(
policy in strategies::command_policy(),
origin in strategies::command_origin(),
) {
let case_ref = CaseRef::new("trip", "trip-1", CaseRevision::ZERO);
let batch = support::batch(
support::turn(0),
&case_ref,
&origin,
vec![TripCommand::Open],
);
let envelope = &batch.envelopes[0];
prop_assert_eq!(
origin_satisfies_policy(envelope, &policy).is_ok(),
origin_satisfies(&origin, &policy)
);
}
#[test]
fn a_trusted_origin_is_never_a_direct_user_act(origin in strategies::trusted_origin()) {
prop_assert!(origin.is_trusted());
let is_direct = matches!(origin, CommandOrigin::DirectSafeUserAct { .. });
prop_assert!(!is_direct);
}
#[test]
fn trip_projection_is_deterministic(
choices in proptest::collection::vec(any::<u8>(), 0..8),
case_id in strategies::case_id(),
revision in strategies::case_revision(),
) {
let workflow = TripWorkflow::default();
let state = walk_trip(&choices);
let case_ref = CaseRef::new("trip", case_id, revision);
let first = workflow.project(case_ref.clone(), state.as_ref());
let second = workflow.project(case_ref, state.as_ref());
prop_assert_eq!(&first, &second);
prop_assert!(check_view(&workflow, &first).is_ok());
let ownership = workflow.phase_ownership(&first.phase);
prop_assert_eq!(
first.erase(ownership).unwrap(),
second.erase(ownership).unwrap()
);
}
#[test]
fn the_executor_replays_a_key_and_conflicts_on_a_stale_revision(
choices in proptest::collection::vec(any::<u8>(), 1..6),
) {
let workflow = TripWorkflow::default();
let model = TripModel::default();
let executor = turnframe_test::workflows::trip::TripExecutor::default();
let origin = support::confirmed_click();
let case_id = CaseId::from("trip-1");
let mut state: Option<TripState> = None;
let mut revision = CaseRevision::ZERO;
let mut last = None;
for (step, choice) in choices.iter().enumerate() {
let candidates = model.candidate_commands(state.as_ref());
if candidates.is_empty() {
break;
}
let command = candidates[usize::from(*choice) % candidates.len()].clone();
if workflow.validate_command(state.as_ref(), &command).is_err() {
continue;
}
let case_ref = CaseRef::new("trip", case_id.clone(), revision);
let batch = support::batch(
support::turn(u8::try_from(step).unwrap_or(0)),
&case_ref,
&origin,
vec![command],
);
let commit = block_on(executor.execute(batch.clone())).unwrap();
prop_assert!(!commit.idempotency_replay);
prop_assert_eq!(commit.new_revision, revision.next());
revision = commit.new_revision;
state.clone_from(&commit.state);
last = Some((batch, commit));
}
let Some((batch, commit)) = last else {
return Ok(());
};
let replay = block_on(executor.execute(batch)).unwrap();
prop_assert!(replay.idempotency_replay);
prop_assert_eq!(&replay.events, &commit.events);
prop_assert_eq!(replay.new_revision, commit.new_revision);
prop_assert_eq!(executor.revision_of(&support::account(), &case_id), revision);
let stale = support::batch(
support::turn(u8::MAX),
&CaseRef::new("trip", case_id, CaseRevision::ZERO),
&origin,
vec![TripCommand::Open],
);
prop_assert!(matches!(
block_on(executor.execute(stale)),
Err(ExecutionError::RevisionConflict(_))
));
}
}
#[test]
fn a_consequential_sample_command_needs_a_trusted_origin() {
let direct = CommandOrigin::DirectSafeUserAct {
evidence_digest: Digest::of_bytes(b"the user said so"),
};
let trip_case = CaseRef::new("trip", "trip-1", CaseRevision(4));
let untrusted = support::batch(
support::turn(1),
&trip_case,
&direct,
vec![TripCommand::Rebook],
);
let confirmed = support::batch(
support::turn(1),
&trip_case,
&support::confirmed_click(),
vec![TripCommand::Rebook],
);
assert!(
no_high_risk_without_trusted_origin(&TripWorkflow::default(), None, &untrusted).is_err(),
"a rebooking sent to the airline must not run on the user's words alone"
);
assert!(
no_high_risk_without_trusted_origin(&TripWorkflow::default(), None, &confirmed).is_ok()
);
let traveler_case = CaseRef::new("traveler", "trav-1", CaseRevision(2));
let deletion = support::batch(
support::turn(2),
&traveler_case,
&direct,
vec![TravelerCommand::Delete],
);
assert!(
no_high_risk_without_trusted_origin(&TravelerWorkflow::new().with_cards(), None, &deletion)
.is_err(),
"a destructive deletion must not run on the user's words alone"
);
}