Contract-suite scaffold: alkstore-contract-suite crate + ADR-022 (suite layout decision), engine dev-dep edges (ADR-017 §4.2 discharged, ADR-012 §2 mirror)

This commit is contained in:
glm-5.3-flash committed 2026-10-07 16:03:00 +00:00
1 parent eefee9ec1d
commit 621415cc47
15 files changed
+1028 -7

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
[package]
name = "alkstore-contract-suite"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
[dependencies]
alkstore = { version = "0.1", path = "../alkstore" }
serde_json = "1"
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }
+44
View File
@@ -0,0 +1,44 @@
//! The factory abstraction the suite is parameterized over (ADR-022).
//!
//! One shape, deliberately minimal: produce a fresh store over an
//! isolated backing store, plus teardown. Engines implement this in
//! their dev-dependency wiring — a fresh file (SQLite) or fresh schema
//! (Postgres) per call, so properties never observe one another's
//! rows.
//!
//! The factory shape may evolve as engines adopt the suite (wave 3/4
//! feedback) — ADR-022 records the *layout* decision, not a frozen
//! API. Changes here are test-side non-events (ADR-017 §2 class 4:
//! the suite tests the contract, it is not contract surface).
use alkstore::{BoxedFuture, Result, Store};
/// Produces fresh, isolated [`Store`]s for the suite's properties.
///
/// Implementers — engine crates, in their dev-dependency wiring —
/// yield a [`Box<dyn Store>`](Store) over an isolated backing store (a
/// fresh file or fresh schema per call, so a property's rows never
/// outlive its run into another's) plus a teardown disposing of the
/// backing store. Implementations must be `Send + Sync` so a factory
/// can be shared across property runs.
///
/// Rows call [`StoreFactory::open`], drive their assertions, and call
/// [`StoreFactory::teardown`] before returning; a rollback-less early
/// exit (a failing assertion panics the row) is the caller's harness
/// concern, which is why teardown is also required to be safe to run
/// against an abandoned store (drop, close, delete).
///
/// The async surface rides the same boxed-future posture as the
/// contract traits (`BoxedFuture`), so implementers need no macros.
pub trait StoreFactory: Send + Sync {
/// Open a fresh store over an isolated backing store.
fn open(&self) -> BoxedFuture<'_, Result<Box<dyn Store>>>;
/// Tear down the backing store behind this factory instance.
///
/// Idempotent per instance. The engine owns what teardown means —
/// deleting a temp file, dropping a schema, closing a pool — and
/// owns the isolated-ness guarantee (no two `open` calls share
/// rows).
fn teardown(&self) -> BoxedFuture<'_, Result<()>>;
}
+40
View File
@@ -0,0 +1,40 @@
//! alkstore-contract-suite — the contract suite crate (ADR-022).
//!
//! The suite is the [verification
//! backlog](../../docs/architecture/core-contract.md) property set
//! parameterized over a [`StoreFactory`] — one normative owner per
//! property (the one-owner rule, ADR-012 §2 applied to test
//! artifacts); each engine crate consumes the suite as a
//! *dev*-dependency and drives its rows against its own factory. The
//! crate is internal and unpublished (`publish = false`, depends on
//! `alkstore` only); ADR-017 §4.2's "layout decided at implementation"
//! deferral is discharged by
//! [ADR-022](../../docs/architecture/decisions/022-contract-suite-layout.md).
//!
//! # Row structure
//!
//! Each backlog row is one named, independently-runnable property — a
//! public async function taking `&dyn StoreFactory` — with a
//! **version stamp** per the suite's [version-stamp
//! convention](version_stamp): a doc-comment line citing the contract
//! change (ADR §) that added the row (ADR-017 §4.2's no-silent-change
//! discipline applied to the suite itself). Wave 3/4 engine tasks run
//! the existing rows against their engines; wave 5 fills the
//! cross-engine equivalence rows.
//!
//! # Crate posture
//!
//! This is a test-side artifact (ADR-001 §4's posture), not contract
//! surface: rows assert with panics on violation — a failing row is a
//! test failure naming the entry point and expectation — so the
//! family-wide no-panic rule is relaxed here by the crate's purpose.
//! Suite-side changes are contract-non-events (ADR-017 §2 class 4:
//! the suite tests the contract, it is not the contract); a row added
//! for a *contract* addition is stamped with that addition's ADR.
pub mod factory;
pub mod properties;
pub mod version_stamp;
pub use factory::StoreFactory;
pub use properties::name_validation_rejects_empty_and_reserved;
+276
View File
@@ -0,0 +1,276 @@
//! The suite's property rows — one named, independently-runnable
//! property per backlog row, version-stamped per the
//! [convention](crate::version_stamp).
//!
//! Assertion posture: rows panic with a label naming the entry point
//! and the broken expectation — a failing row is a test failure, which
//! is the suite's purpose as a test-side artifact (relaxing the
//! family-wide no-panic rule here only).
use serde_json::json;
use alkstore::{BoxedFuture, Error, RESERVED_PREFIX, Result, validate_shared_name};
use crate::factory::StoreFactory;
fn expect_invalid<T>(label: &str, out: alkstore::Result<T>) {
match out {
Err(Error::InvalidName { .. }) => {}
Err(other) => {
panic!("{label}: expected `InvalidName`, got {other:?} — entry-point validation broke")
}
Ok(_) => panic!("{label}: expected `InvalidName` rejection, got Ok"),
}
}
fn expect_reserved<T>(label: &str, out: alkstore::Result<T>) {
match out {
Err(Error::ReservedName { .. }) => {}
Err(other) => {
panic!("{label}: expected `ReservedName`, got {other:?} — entry-point validation broke")
}
Ok(_) => panic!("{label}: expected `ReservedName` rejection, got Ok"),
}
}
fn expect_ok<T>(label: &str, out: alkstore::Result<T>) {
if let Err(other) = out {
panic!("{label}: expected success (no validation error), got {other:?}");
}
}
/// **Exemplar row** — name validation at every entry point,
/// auto-commit and tx paths alike.
///
/// Every name-bearing entry point on the [`Store`](alkstore::Store)
/// surface — and every name-bearing `*_tx` twin on the
/// [`TxHandle`](alkstore::TxHandle) — rejects, before any engine round
/// trip: the empty (or whitespace-only) name with
/// [`Error::InvalidName`], and the reserved-prefix name
/// (`__alkstore_...`) with [`Error::ReservedName`], on the
/// shared-namespace kinds — channels, streams, queues, outboxes,
/// locks, schedule names, and `schedule()`'s queue argument.
/// Consumer-local identifiers take the non-empty rule only: `owner`
/// reserved-prefixed is legal, `owner` empty is not. The tx path is
/// exercised through `begin_tx` and `with_tx` (the provided wrapper):
/// validation must not be bypassable by the commit-atomic path.
///
/// Contract stamp: ADR-008 §4 (reserved namespace + entry-point
/// validation); ADR-021 §3 (the schedule queue argument); ADR-015 §2
/// (the keyed publish's non-empty-key rule).
///
/// Engine-independent, pure contract: runnable against any factory.
/// The happy-path spot-checks prove the rejections are the
/// validation's doing, not a store-wide failure — valid names
/// construct handles without a validation error, and a
/// reserved-prefixed *local* identifier is accepted.
pub async fn name_validation_rejects_empty_and_reserved(factory: &dyn StoreFactory) {
let store = factory.open().await.expect("factory opens a store");
// Auto-commit paths — empty names.
expect_invalid("notify(empty channel)", store.notify("", json!(null)).await);
expect_invalid("listen(empty channel)", store.listen("").await);
expect_invalid("stream(empty name)", store.stream("").await);
expect_invalid(
"queue(empty name)",
store.queue("", alkstore::QueueOpts::default()).await,
);
expect_invalid("outbox(empty name)", store.outbox("").await);
expect_invalid(
"try_lock(empty name)",
store.try_lock("", "worker", 60).await,
);
expect_invalid(
"try_lock(empty owner)",
store.try_lock("lock", "", 60).await,
);
expect_invalid(
"schedule(empty name)",
store
.schedule("", "@every 1s", "work", json!(null), Default::default())
.await,
);
expect_invalid(
"schedule(empty queue argument)",
store
.schedule("tick", "@every 1s", "", json!(null), Default::default())
.await,
);
// Auto-commit paths — reserved-prefix names.
expect_reserved(
"notify(reserved channel)",
store.notify(RESERVED_PREFIX, json!(null)).await,
);
expect_reserved(
"listen(reserved channel)",
store.listen(RESERVED_PREFIX).await,
);
expect_reserved("stream(reserved name)", store.stream(RESERVED_PREFIX).await);
expect_reserved(
"queue(reserved name)",
store
.queue(RESERVED_PREFIX, alkstore::QueueOpts::default())
.await,
);
expect_reserved("outbox(reserved name)", store.outbox(RESERVED_PREFIX).await);
expect_reserved(
"try_lock(reserved name)",
store.try_lock(RESERVED_PREFIX, "worker", 60).await,
);
expect_reserved(
"schedule(reserved name)",
store
.schedule(
RESERVED_PREFIX,
"@every 1s",
"work",
json!(null),
Default::default(),
)
.await,
);
expect_reserved(
"schedule(reserved queue argument)",
store
.schedule(
"tick",
"@every 1s",
RESERVED_PREFIX,
json!(null),
Default::default(),
)
.await,
);
// Happy-path spot-checks: valid names pass validation; a
// reserved-prefixed owner (consumer-local identifier) is legal.
expect_ok("stream(valid name)", store.stream("work").await);
expect_ok(
"queue(valid name)",
store.queue("work", alkstore::QueueOpts::default()).await,
);
expect_ok(
"try_lock(reserved-prefixed owner is legal)",
store.try_lock("lock", RESERVED_PREFIX, 60).await,
);
// Tx paths — the twins validate identically (begin_tx route).
let mut tx = store.begin_tx().await.expect("begin_tx opens");
expect_invalid(
"enqueue_tx(empty queue)",
tx.enqueue_tx("", Default::default(), json!(null)).await,
);
expect_reserved(
"enqueue_tx(reserved queue)",
tx.enqueue_tx(RESERVED_PREFIX, Default::default(), json!(null))
.await,
);
expect_invalid(
"publish_tx(empty stream)",
tx.publish_tx("", json!(null)).await,
);
expect_reserved(
"publish_tx(reserved stream)",
tx.publish_tx(RESERVED_PREFIX, json!(null)).await,
);
expect_invalid(
"notify_tx(empty channel)",
tx.notify_tx("", json!(null)).await,
);
expect_reserved(
"notify_tx(reserved channel)",
tx.notify_tx(RESERVED_PREFIX, json!(null)).await,
);
expect_invalid(
"publish_with_key_tx(empty stream)",
tx.publish_with_key_tx("", None, json!(null)).await,
);
expect_reserved(
"publish_with_key_tx(reserved stream)",
tx.publish_with_key_tx(RESERVED_PREFIX, None, json!(null))
.await,
);
expect_invalid(
"publish_with_key_tx(empty key)",
tx.publish_with_key_tx("work", Some(String::new()), json!(null))
.await,
);
expect_invalid(
"publish_with_key_tx(whitespace key)",
tx.publish_with_key_tx("work", Some(" ".to_string()), json!(null))
.await,
);
expect_invalid(
"save_offset_tx(empty stream)",
tx.save_offset_tx("", "consumer", 0).await,
);
expect_reserved(
"save_offset_tx(reserved stream)",
tx.save_offset_tx(RESERVED_PREFIX, "consumer", 0).await,
);
expect_invalid(
"save_offset_tx(empty consumer)",
tx.save_offset_tx("work", "", 0).await,
);
expect_invalid("get_job_tx(empty queue)", tx.get_job_tx("", 1).await);
expect_reserved(
"get_job_tx(reserved queue)",
tx.get_job_tx(RESERVED_PREFIX, 1).await,
);
expect_invalid(
"get_offset_tx(empty stream)",
tx.get_offset_tx("", "consumer").await,
);
expect_reserved(
"get_offset_tx(reserved stream)",
tx.get_offset_tx(RESERVED_PREFIX, "consumer").await,
);
expect_invalid(
"read_since_tx(empty stream)",
tx.read_since_tx("", 0, 10).await,
);
expect_reserved(
"read_since_tx(reserved stream)",
tx.read_since_tx(RESERVED_PREFIX, 0, 10).await,
);
expect_invalid(
"read_from_consumer_tx(empty stream)",
tx.read_from_consumer_tx("", "consumer", 10).await,
);
expect_reserved(
"read_from_consumer_tx(reserved stream)",
tx.read_from_consumer_tx(RESERVED_PREFIX, "consumer", 10)
.await,
);
expect_invalid(
"read_from_consumer_tx(empty consumer)",
tx.read_from_consumer_tx("work", "", 10).await,
);
expect_invalid(
"outbox_enqueue_tx(empty outbox)",
tx.outbox_enqueue_tx("", Default::default(), json!(null))
.await,
);
expect_reserved(
"outbox_enqueue_tx(reserved outbox)",
tx.outbox_enqueue_tx(RESERVED_PREFIX, Default::default(), json!(null))
.await,
);
// Tx path via `with_tx` (the provided wrapper): a validation error
// inside the closure propagates as the wrapper's `Err` (and the
// disposition rolls back) — the commit-atomic path cannot bypass
// validation either.
let result = store
.with_tx(Box::new(|tx| {
Box::pin(async move {
validate_shared_name("")?;
tx.notify_tx("work", json!(null)).await
}) as BoxedFuture<'_, Result<()>>
}))
.await;
expect_invalid("with_tx(inner empty-name rejection)", result);
factory.teardown().await.expect("factory teardown");
}
@@ -0,0 +1,41 @@
//! The version-stamp convention (ADR-017 §4.2, adopted here as the
//! suite's row discipline — ADR-022).
//!
//! Each property row is version-stamped with the contract change that
//! added it — ADR-017 §4.2's requirement that makes the suite
//! auditable against the core crate's changelog (whose entries cite
//! their ADRs by §2's no-silent-change rule).
//!
//! # The convention
//!
//! A row's stamp is a doc-comment list item beginning
//! **`Contract stamp:`** on the row's doc text, citing the ADR § (or
//! the backlog row) that pinned the behavior being tested:
//!
//! ```text
//! /// <one-line statement of the property>
//! ///
//! /// Contract stamp: ADR-008 §4 (reserved namespace, entry-point
//! /// validation); ADR-021 §3 (the schedule queue argument).
//! pub async fn row_name(factory: &dyn StoreFactory) { ... }
//! ```
//!
//! Rules:
//!
//! - The stamp cites the ADR § that pins the property — never a bare
//! "v1" or a date. A row may carry multiple stamps when it tests
//! several changes' behavior together (the exemplar row does).
//! - A row added to pin a new contract addition is stamped with that
//! addition's ADR; a row whose property predates the suite is
//! stamped with the ADR that pinned the behavior it tests.
//! - When a later contract change *amends* a row's property, the
//! row's stamp list gains the amending ADR (rows accumulate stamps;
//! the ADRs' own texts record the row history).
//! - `Contract stamp:` is greppable: `grep -r "Contract stamp:"
//! alkstore-contract-suite/` lists the suite's stamp inventory.
//!
//! Version 0 of the suite: this module *is* the convention's
//! normative statement; every later row cites its stamp against it.
/// The doc-comment marker introducing a row's version stamp.
pub const STAMP_MARKER: &str = "Contract stamp:";
@@ -0,0 +1,369 @@
//! The harness: runs the suite's property rows against a supplied
//! [`StoreFactory`]. This test target proves the harness end-to-end
//! with a trivial in-crate mock store — the acceptance criterion's
//! "exemplar property green against a trivial in-crate mock" — and is
//! the shape each engine crate's dev-dependency wiring copies.
use std::time::Duration;
use serde_json::json;
use alkstore::{
BoxedFuture, Delivery, EnqueueOpts, Error, EventReceiver, Job, JobHandle, Lock, Outbox, Queue,
QueueOpts, Result, Schedule, StopToken, Store, StreamEvent, StreamHandle, TxHandle, Wake,
WakeReceiver,
};
use alkstore_contract_suite::StoreFactory;
use alkstore_contract_suite::properties::name_validation_rejects_empty_and_reserved;
/// Trivial mock `TxHandle` — validation is the *engine's* job in real
/// engines; the mock leans on the shared helpers exactly as an engine
/// would, at each entry point.
struct MockTx;
impl TxHandle for MockTx {
fn enqueue_tx<'a>(
&'a mut self,
queue: &str,
_opts: EnqueueOpts,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<i64>> {
let r = alkstore::validate_shared_name(queue).map(|_| 1);
Box::pin(std::future::ready(r))
}
fn publish_tx<'a>(
&'a mut self,
stream: &str,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<i64>> {
let r = alkstore::validate_shared_name(stream).map(|_| 1);
Box::pin(std::future::ready(r))
}
fn publish_with_key_tx<'a>(
&'a mut self,
stream: &str,
key: Option<String>,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<i64>> {
let r = alkstore::validate_shared_name(stream).and_then(|_| match key {
Some(k) if k.trim().is_empty() => Err(Error::InvalidName { name: k }),
_ => Ok(1),
});
Box::pin(std::future::ready(r))
}
fn notify_tx<'a>(
&'a mut self,
channel: &str,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(std::future::ready(alkstore::validate_shared_name(channel)))
}
fn save_offset_tx<'a>(
&'a mut self,
stream: &str,
consumer: &str,
_offset: i64,
) -> BoxedFuture<'a, Result<()>> {
let r = alkstore::validate_shared_name(stream)
.and_then(|_| alkstore::validate_local_name(consumer));
Box::pin(std::future::ready(r))
}
fn get_job_tx<'a>(
&'a mut self,
queue: &str,
_job_id: i64,
) -> BoxedFuture<'a, Result<Option<Job>>> {
let r = alkstore::validate_shared_name(queue).map(|_| None);
Box::pin(std::future::ready(r))
}
fn get_offset_tx<'a>(
&'a mut self,
stream: &str,
_consumer: &str,
) -> BoxedFuture<'a, Result<i64>> {
let r = alkstore::validate_shared_name(stream).map(|_| 0);
Box::pin(std::future::ready(r))
}
fn read_since_tx<'a>(
&'a mut self,
stream: &str,
_offset: i64,
_limit: i64,
) -> BoxedFuture<'a, Result<Vec<StreamEvent>>> {
let r = alkstore::validate_shared_name(stream).map(|_| Vec::new());
Box::pin(std::future::ready(r))
}
fn read_from_consumer_tx<'a>(
&'a mut self,
stream: &str,
consumer: &str,
_limit: i64,
) -> BoxedFuture<'a, Result<Vec<StreamEvent>>> {
let r = alkstore::validate_shared_name(stream)
.and_then(|_| alkstore::validate_local_name(consumer))
.map(|_| Vec::new());
Box::pin(std::future::ready(r))
}
fn outbox_enqueue_tx<'a>(
&'a mut self,
outbox: &str,
_opts: EnqueueOpts,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<i64>> {
let r = alkstore::validate_shared_name(outbox).map(|_| 1);
Box::pin(std::future::ready(r))
}
fn commit(self: Box<Self>) -> BoxedFuture<'static, Result<()>> {
Box::pin(std::future::ready(Ok(())))
}
}
struct MockQueue;
impl Queue for MockQueue {
fn name(&self) -> &str {
"mock"
}
fn enqueue<'a>(
&'a self,
_payload: serde_json::Value,
_opts: EnqueueOpts,
) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(1)))
}
fn claim_one<'a>(
&'a self,
_worker_id: &str,
) -> BoxedFuture<'a, Result<Option<Box<dyn JobHandle>>>> {
Box::pin(std::future::ready(Ok(None)))
}
fn claim_batch<'a>(
&'a self,
_worker_id: &str,
_n: i64,
) -> BoxedFuture<'a, Result<Vec<Box<dyn JobHandle>>>> {
Box::pin(std::future::ready(Ok(Vec::new())))
}
fn ack_batch<'a>(&'a self, _ids: &[i64]) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(0)))
}
fn cancel<'a>(&'a self, _job_id: i64) -> BoxedFuture<'a, Result<bool>> {
Box::pin(std::future::ready(Ok(false)))
}
fn get_job<'a>(&'a self, _job_id: i64) -> BoxedFuture<'a, Result<Option<Job>>> {
Box::pin(std::future::ready(Ok(None)))
}
fn sweep_expired<'a>(&'a self) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(0)))
}
}
struct MockStream;
impl StreamHandle for MockStream {
fn name(&self) -> &str {
"mock"
}
fn publish<'a>(&'a self, _payload: serde_json::Value) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(1)))
}
fn publish_with_key<'a>(
&'a self,
_key: Option<String>,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(1)))
}
fn read_since<'a>(
&'a self,
_offset: i64,
_limit: i64,
) -> BoxedFuture<'a, Result<Vec<StreamEvent>>> {
Box::pin(std::future::ready(Ok(Vec::new())))
}
fn read_from_consumer<'a>(
&'a self,
_consumer: &str,
_limit: i64,
) -> BoxedFuture<'a, Result<Vec<StreamEvent>>> {
Box::pin(std::future::ready(Ok(Vec::new())))
}
fn save_offset<'a>(&'a self, _consumer: &str, _offset: i64) -> BoxedFuture<'a, Result<()>> {
Box::pin(std::future::ready(Ok(())))
}
fn get_offset<'a>(&'a self, _consumer: &str) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(0)))
}
fn trim_to<'a>(&'a self, _horizon: i64) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(0)))
}
fn subscribe<'a>(&'a self, _consumer: &str) -> BoxedFuture<'a, Result<Box<dyn EventReceiver>>> {
Box::pin(std::future::ready(Err(Error::Database("mock".into()))))
}
}
struct MockWakeReceiver;
impl WakeReceiver for MockWakeReceiver {
fn recv<'a>(&'a mut self) -> BoxedFuture<'a, Option<Wake>> {
Box::pin(std::future::ready(None))
}
fn try_recv(&mut self) -> Result<Option<Wake>> {
Ok(None)
}
fn recv_timeout<'a>(&'a mut self, _timeout: Duration) -> BoxedFuture<'a, Result<Option<Wake>>> {
Box::pin(std::future::ready(Ok(None)))
}
}
/// `Schedule` is `#[non_exhaustive]` (ADR-017 §3) — the mock cannot
/// struct-construct it, so the happy-path `schedule()` spot-check is
/// delegated to core's own tests; the mock's `schedule()` returns a
/// `Database` stub and the row only asserts its *validation* outcomes
/// (which precede any construction).
struct MockStore;
impl Store for MockStore {
fn begin_tx(&self) -> BoxedFuture<'_, Result<Box<dyn TxHandle + Send>>> {
Box::pin(std::future::ready(Ok(
Box::new(MockTx) as Box<dyn TxHandle + Send>
)))
}
fn notify<'a>(
&'a self,
channel: &str,
_payload: serde_json::Value,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(std::future::ready(alkstore::validate_shared_name(channel)))
}
fn listen<'a>(&'a self, channel: &str) -> BoxedFuture<'a, Result<Box<dyn WakeReceiver>>> {
let r = alkstore::validate_shared_name(channel)
.map(|_| Box::new(MockWakeReceiver) as Box<dyn WakeReceiver>);
Box::pin(std::future::ready(r))
}
fn stream<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result<Box<dyn StreamHandle>>> {
let r = alkstore::validate_shared_name(name)
.map(|_| Box::new(MockStream) as Box<dyn StreamHandle>);
Box::pin(std::future::ready(r))
}
fn queue<'a>(
&'a self,
name: &str,
_opts: QueueOpts,
) -> BoxedFuture<'a, Result<Box<dyn Queue>>> {
let r = alkstore::validate_shared_name(name).map(|_| Box::new(MockQueue) as Box<dyn Queue>);
Box::pin(std::future::ready(r))
}
fn outbox<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result<Box<dyn Outbox>>> {
let r =
alkstore::validate_shared_name(name).map(|_| Box::new(MockOutbox) as Box<dyn Outbox>);
Box::pin(std::future::ready(r))
}
fn try_lock<'a>(
&'a self,
name: &str,
owner: &str,
_ttl: i64,
) -> BoxedFuture<'a, Result<Option<Box<dyn Lock>>>> {
let r = alkstore::validate_shared_name(name)
.and_then(|_| alkstore::validate_local_name(owner))
.map(|_| None);
Box::pin(std::future::ready(r))
}
fn schedule<'a>(
&'a self,
name: &str,
_spec: &str,
queue: &str,
_payload: serde_json::Value,
_opts: alkstore::ScheduleOpts,
) -> BoxedFuture<'a, Result<Schedule>> {
let r = alkstore::validate_shared_name(name)
.and_then(|_| alkstore::validate_shared_name(queue))
.and_then(|_| {
Err(Error::Database(
"mock: schedule read-back not exercised".into(),
))
});
Box::pin(std::future::ready(r))
}
fn unschedule<'a>(&'a self, _name: &str) -> BoxedFuture<'a, Result<bool>> {
Box::pin(std::future::ready(Ok(false)))
}
fn run_schedules<'a>(&'a self, _stop: StopToken) -> BoxedFuture<'a, Result<()>> {
Box::pin(std::future::ready(Ok(())))
}
}
struct MockOutbox;
impl Outbox for MockOutbox {
fn name(&self) -> &str {
"mock"
}
fn enqueue<'a>(
&'a self,
_payload: serde_json::Value,
_opts: EnqueueOpts,
) -> BoxedFuture<'a, Result<i64>> {
Box::pin(std::future::ready(Ok(1)))
}
fn run_once<'a>(
&'a mut self,
_worker_id: &str,
_delivery: &'a mut dyn Delivery,
) -> BoxedFuture<'a, Result<bool>> {
Box::pin(std::future::ready(Ok(false)))
}
}
/// The mock factory: yields a fresh store over an isolated (in-memory,
/// stateless) backing store. Isolation is trivially true — the mock
/// holds no rows.
struct MockFactory;
impl StoreFactory for MockFactory {
fn open(&self) -> BoxedFuture<'_, Result<Box<dyn Store>>> {
Box::pin(std::future::ready(
Ok(Box::new(MockStore) as Box<dyn Store>),
))
}
fn teardown(&self) -> BoxedFuture<'_, Result<()>> {
Box::pin(std::future::ready(Ok(())))
}
}
#[tokio::test]
async fn exemplar_row_green_against_mock_factory() {
let factory = MockFactory;
name_validation_rejects_empty_and_reserved(&factory).await;
}
/// Happy-path spot-check behind the exemplar row: valid names construct
/// handles — proving the row's rejections are the validation's doing
/// (the mock's `schedule` happy path is a `Database` stub because
/// `Schedule` is `#[non_exhaustive]`; its validation outcomes are what
/// the row asserts).
#[tokio::test]
async fn mock_store_happy_paths_construct() {
let factory = MockFactory;
let store = factory.open().await.unwrap();
store.stream("work").await.expect("stream handle");
store
.queue("work", QueueOpts::default())
.await
.expect("queue handle");
store.outbox("work").await.expect("outbox handle");
factory.teardown().await.expect("teardown");
}
#[test]
fn stamp_marker_and_json_shape() {
assert_eq!(
alkstore_contract_suite::version_stamp::STAMP_MARKER,
"Contract stamp:"
);
let _ = json!(null);
}