use turnframe_core::error::ExecutionError;
use turnframe_core::flow::WorkflowExecutor;
use super::fixtures::{
batch, ensure, ensure_commit, ensure_eq, ensure_refused, ensure_unchanged, seed, snapshot,
};
use super::{ConformanceFailure, ExecutorFactory};
pub async fn check_repeated_key_replays_the_outcome<F: ExecutorFactory>(
factory: &F,
) -> Result<(), ConformanceFailure> {
const CHECK: &str = "check_repeated_key_replays_the_outcome";
let seeded = seed(CHECK, factory).await?;
let work = batch::<F>(
CHECK,
&seeded,
"idempotency.batch",
&seeded.case_ref,
&[seeded.first.clone(), seeded.second.clone()],
)?;
let first = ensure_commit(
CHECK,
"the batch, executed for the first time",
seeded.executor.execute(work.clone()).await,
)?;
ensure(
CHECK,
!first.idempotency_replay,
"a batch whose keys the executor has never seen must not be reported as a replay",
)?;
let committed = snapshot(CHECK, "after the batch committed", &seeded).await?;
for attempt in ["the second time", "the third time"] {
let again = ensure_commit(
CHECK,
&format!("the same batch, executed {attempt}"),
seeded.executor.execute(work.clone()).await,
)?;
ensure(
CHECK,
again.idempotency_replay,
format!(
"the batch executed {attempt} came back without `idempotency_replay`: \
the caller cannot tell a fresh commit from a replay"
),
)?;
ensure_eq(
CHECK,
&format!("the revision reported {attempt}"),
&again.new_revision,
&first.new_revision,
)?;
ensure_eq(
CHECK,
&format!("the number of events reported {attempt}"),
&again.events.len(),
&first.events.len(),
)?;
ensure_unchanged(
CHECK,
&format!("after the batch was executed {attempt}"),
&seeded,
&committed,
)
.await?;
}
Ok(())
}
pub async fn check_repeated_key_with_another_command_is_refused<F: ExecutorFactory>(
factory: &F,
) -> Result<(), ConformanceFailure> {
const CHECK: &str = "check_repeated_key_with_another_command_is_refused";
let seeded = seed(CHECK, factory).await?;
let original = batch::<F>(
CHECK,
&seeded,
"idempotency.mismatch",
&seeded.case_ref,
std::slice::from_ref(&seeded.first),
)?;
ensure_commit(
CHECK,
"the original batch",
seeded.executor.execute(original.clone()).await,
)?;
let committed = snapshot(CHECK, "after the original batch", &seeded).await?;
let mut forged = original.clone();
let target = forged
.envelopes
.first()
.map(|e| e.command_id)
.ok_or_else(|| {
ConformanceFailure::new(CHECK, "the suite built a batch with no envelopes")
})?;
let first_json = serde_json::to_value(&seeded.first).ok();
let second_json = serde_json::to_value(&seeded.second).ok();
ensure(
CHECK,
first_json.is_some() && first_json != second_json,
"the factory's `first` and `second` commands must serialize differently, \
otherwise reusing a key with `second` is not a different command at all",
)?;
if let Some(envelope) = forged.envelopes.first_mut() {
envelope.command = seeded.second.clone();
}
for attempt in ["the first time", "the second time"] {
let refused = ensure_refused(
CHECK,
&format!("a known key carrying a different command, {attempt}"),
seeded.executor.execute(forged.clone()).await,
)?;
match refused {
ExecutionError::IdempotencyMismatch { command_id } => {
ensure_eq(
CHECK,
&format!("the envelope the mismatch names {attempt}"),
&command_id,
&target,
)?;
}
other => {
return Err(ConformanceFailure::new(
CHECK,
format!(
"a key reused with another command must be an IdempotencyMismatch \
{attempt}, got {other:?}"
),
));
}
}
ensure_unchanged(
CHECK,
&format!("after the mismatch was refused {attempt}"),
&seeded,
&committed,
)
.await?;
}
Ok(())
}