The lockstep coordinator, its phase machine and a synthetic composition, as a new workspace member. Every worker method is a Flybus RPC to an incarnation-pinned service; the domain request ids and result caches of ipc-v1 section 5 sit in front of every mutation. - coordinator: the step-v1 section 3 transaction in order -- prepare every agent concurrently, run one executor per agent in sorted agent-id order, assemble all port controls in descriptor order, send exactly one Environment.Advance, evaluate the task once, commit every agent concurrently, and move the committed boundary only when all of them succeeded. Sequential, concurrent and reversed dispatch are selectable and must agree. - phase: the section 2 machine as an explicit edge table, Paused and Failed included, with a committed-boundary predicate that gates pauses, captures and publication. - clock: checked rational accumulation. A 60 Hz world with a 1 ms model tick runs 16, 17, 17 ticks and comes back to a remainder of exactly zero. - dedup: operation keys, the cached reply with its own artifact holds, CONFLICT, IN_PROGRESS, RESULT_EXPIRED, STALE_STEP, the lifecycle bound and Worker.Acknowledge. - worker: the dispatch shell. Admission order and the result cache live here; the endpoint mutex is the worker's simulation lock, so one mutation runs at a time while Worker.Status answers from a separate cell. - agent, environment, task: a fake model with an explicit seed and a mutation counter the tests read, a fixed readout stub, a counter arena that seals one immutable frame per boundary, a deterministic task and the identity executor. - fly_session_types: the CONTRACT-01 domain types as a local stand-in, reconciled with the shared crate in a following commit. Tests run twice, over the in-memory transport and over a Unix socket, through the same router: the SESSION-01 acceptance bullets and every section 4 failure-injection row that applies to this slice.
96 lines
2.6 KiB
Rust
96 lines
2.6 KiB
Rust
//! Shared test fixture: the synthetic session on a temporary store, over either transport.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use std::time::Duration;
|
|
|
|
use fly_session::harness::{HarnessConfig, SessionHarness, Via};
|
|
use fly_session::types::Id;
|
|
|
|
pub const WAIT: Duration = Duration::from_secs(20);
|
|
|
|
/// Generates one test per transport from an `async fn name(via: Via)`.
|
|
#[macro_export]
|
|
macro_rules! both_transports {
|
|
($($name:ident),* $(,)?) => {
|
|
mod in_memory {
|
|
$(
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn $name() {
|
|
super::$name($crate::common::via_memory()).await
|
|
}
|
|
)*
|
|
}
|
|
mod unix_socket {
|
|
$(
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn $name() {
|
|
super::$name($crate::common::via_unix()).await
|
|
}
|
|
)*
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn via_memory() -> Via {
|
|
Via::Memory
|
|
}
|
|
|
|
pub fn via_unix() -> Via {
|
|
Via::Unix
|
|
}
|
|
|
|
/// A started session plus the temporary directory its store lives in.
|
|
pub struct Fixture {
|
|
pub dir: tempfile::TempDir,
|
|
pub harness: SessionHarness,
|
|
}
|
|
|
|
impl Fixture {
|
|
pub async fn shutdown(self) {
|
|
let Fixture { dir, harness } = self;
|
|
harness.shutdown().await;
|
|
drop(dir);
|
|
}
|
|
}
|
|
|
|
pub async fn fixture(via: Via, config: HarnessConfig) -> Fixture {
|
|
let dir = tempfile::tempdir().expect("a temporary directory");
|
|
let harness = SessionHarness::start(via, dir.path(), config)
|
|
.await
|
|
.expect("the synthetic session starts");
|
|
Fixture { dir, harness }
|
|
}
|
|
|
|
/// The default two-agent composition: 60 Hz world, 1 ms model tick.
|
|
pub async fn default_fixture(via: Via) -> Fixture {
|
|
fixture(via, HarnessConfig::default()).await
|
|
}
|
|
|
|
pub fn fly_a() -> Id {
|
|
Id::lit("fly-a")
|
|
}
|
|
|
|
pub fn fly_b() -> Id {
|
|
Id::lit("fly-b")
|
|
}
|
|
|
|
/// The index of an audit entry, or a panic naming what was missing.
|
|
pub fn at(audit: &[String], what: &str) -> usize {
|
|
audit
|
|
.iter()
|
|
.position(|entry| entry == what)
|
|
.unwrap_or_else(|| panic!("the audit has no {what:?}: {audit:?}"))
|
|
}
|
|
|
|
pub fn count(audit: &[String], what: &str) -> usize {
|
|
audit.iter().filter(|entry| *entry == what).count()
|
|
}
|
|
|
|
/// Fails the test rather than hanging, so a missed reply is a failure and not a stuck job.
|
|
pub async fn within<T>(what: &str, f: impl std::future::Future<Output = T>) -> T {
|
|
match tokio::time::timeout(WAIT, f).await {
|
|
Ok(v) => v,
|
|
Err(_) => panic!("{what}: timed out"),
|
|
}
|
|
}
|