Skip to main content

agent_block_core/knl/
event_store.rs

1//! The event-store SPI and its in-memory backend.
2//!
3//! An [`EventStore`] is one session's append-only log behind a trait, so
4//! the durable backend a later round adds (SQLite) can take the same
5//! calls the in-memory one does.  The SPI is scoped to a *single* stream:
6//! the session is the stream, so there is no `stream` parameter here —
7//! multiple streams are a durable-backend concern.
8//!
9//! Two calls are outside that scoping, and both are outside it because one
10//! transaction has to cover two streams — which is the one thing a caller
11//! cannot build on top of the SPI for itself.
12//! [`EventStore::append_if_many`] decides against this stream and writes to
13//! this one and one other (a parent's ledger entry beside the child's opening
14//! and grant), and [`EventStore::append_with_open_children`] scans the
15//! database for the streams that name this one as their parent and appends an
16//! event built from what it found.  Both take the kinds and the field names
17//! they work with as arguments: the vocabulary stays the caller's, and the
18//! backend only knows how to walk.  Both are answered by the durable backend
19//! and by nothing else — a store that keeps one stream has no second one to
20//! write ([`KnlError::Unsupported`]) and no children to find (an empty list).
21//!
22//! # Append-only is the shape, not a runtime check
23//!
24//! The trait has no `update`, `delete` or `overwrite`.  Immutability is
25//! guaranteed by what the trait *cannot* express, the same way [`History`]
26//! guarantees it by having no mutation API.
27//!
28//! # Store-assigned coordinates, returned inline
29//!
30//! A write returns a [`Committed`]: the `seq` and `epoch_ms` the store
31//! assigned.  A caller never has to read back to learn where its event
32//! landed, and never supplies those fields — they are the store's to give.
33//!
34//! # Appends land; decisions are taken inside the write
35//!
36//! [`EventStore::append`] records a fact, and the store — not the caller —
37//! decides where it lands.  It is *serialized per stream by the backend*
38//! (SQLite: an `IMMEDIATE` transaction with a bounded busy retry; the
39//! in-memory store: a single owner in a single process), so two handles on
40//! one stream both write and the log interleaves in arrival order.  An
41//! ordinary append is never refused for an out-of-date view of the head:
42//! that would be asking a fact to prove it knew the future.
43//!
44//! Facts that belong together are written with [`EventStore::append_many`],
45//! which the durable backend takes in one transaction: a session's opening
46//! and the grant it opened with either both land or neither does, so no
47//! reader ever meets a stream that opened without the quota it was opened
48//! under.
49//!
50//! A *command* with an invariant — "reserve n only if the balance covers
51//! it" — is the other case, and it is expressed by
52//! [`EventStore::append_if`]: the backend reads the stream, calls the
53//! caller's `decide` and appends what it returns, all inside the same
54//! serialized write.  The check therefore runs against the stream as it is
55//! at that instant, not against a head someone cached earlier.
56//!
57//! # Reads name the kinds they need
58//!
59//! [`EventStore::read_kinds`] takes the kinds a caller is folding over
60//! (`None` for all of them), and [`EventStore::append_if`] filters the
61//! decision's input the same way.  The kernel does not interpret the kinds
62//! here — the caller names them, because the caller is the one that knows
63//! what its fold reads: the balance folds the four `budget_*` kinds, a
64//! resume asks whether a `session_closed` is there.  A durable backend
65//! answers those off an index rather than walking the stream.
66//!
67//! # Stored shape change ⇒ upcaster, always — from the first release on
68//!
69//! Stored bytes are never rewritten.  Every change to the shape of a stored
70//! event ships in the same round as (a) a bump of
71//! [`CURRENT_SCHEMA_VERSION`], which every new event is stamped with, and
72//! (b) an [`Upcaster`] for the `n → n+1` step, registered in
73//! [`kernel_upcasters`] and applied at read time by [`CurrentStore`].  A
74//! round that renames a kind or a field without an upcaster is incomplete:
75//! an old log would be silently misread, which is the one failure an
76//! append-only store exists to prevent.
77//!
78//! That obligation starts at the first release.  Until then the stored shape
79//! is still being settled, there is no log anyone has to keep, and a rename
80//! is a rename — so [`kernel_upcasters`] returns an empty chain and
81//! [`CURRENT_SCHEMA_VERSION`] stays at `1`.  The seam is built and tested all
82//! the same, so the first step that is owed has one site to be registered at.
83//!
84//! # Only upcasted events reach the domain
85//!
86//! The seam is a *type*, not a habit.  [`EventStore`] deals in raw
87//! [`Value`]s — whatever shape the bytes were written in — while
88//! [`CurrentStore`], which is deliberately **not** an [`EventStore`], reads
89//! through the chain and hands back [`Current`]s.  Nothing else can build
90//! one: the constructor is private to this module.  So every fold the kernel
91//! runs takes `&[Current]`, and a read path that went round the chain does
92//! not type-check rather than quietly folding a stale shape.
93
94use std::ops::Deref;
95use std::sync::{Arc, Mutex, PoisonError};
96
97use async_trait::async_trait;
98use serde_json::{Map, Value};
99
100use super::event::{FIELD_KIND, FIELD_SEQ};
101// Only the `Vec`-backed test store below reads these — the durable backend
102// stamps and selects in SQL.
103#[cfg(test)]
104use super::event::{kind_of, seq_of, validate_event, FIELD_EPOCH_MS};
105#[cfg(test)]
106use super::History;
107use super::{KnlError, KnlResult};
108
109/// Reserved envelope key: the schema version an event was written under.
110///
111/// Read-time upcasting keys on it, so a stored event carries the version it
112/// was written with and the stored bytes are never rewritten.  The `_`
113/// prefix keeps it out of the caller's payload namespace, like the other
114/// kernel-owned envelope fields.
115pub const SCHEMA_VERSION_FIELD: &str = "_schema_version";
116
117/// The schema version new events are stamped with.
118///
119/// `1`: the stored shape has never been released, so nothing has been
120/// written under an older one and there is no step to take.  A shape change
121/// *after* the first release bumps this and registers the matching
122/// [`Upcaster`] — see the module docs.
123///
124/// **What counts as a shape change** is what an event *is*: a kind renamed, a
125/// field added, moved, retyped or dropped.  How the rows are stored beside
126/// that — an index added to the durable backend's DDL, a page size, a pragma
127/// — changes no event and bumps nothing: the same bytes read the same way
128/// afterwards, and a database an earlier build wrote picks the index up on
129/// the next open with nothing to upcast.
130pub const CURRENT_SCHEMA_VERSION: u64 = 1;
131
132/// The upcaster chain every session reads through, newest step last.
133///
134/// [`super::Session::open_on`] and [`super::Session::resume`] wrap their
135/// backend in a [`CurrentStore`] carrying this chain, so every read a
136/// session makes — the restore fold, the view folds, `events` — sees the
137/// current shape while the stored bytes stay exactly as they were written.
138///
139/// Empty until the first release: there is no released shape to read yet, so
140/// there is no step owed.  This is the one site a step is registered at, and
141/// the seam around it is exercised by the tests below with a chain of their
142/// own.
143///
144/// A step registered here that *renames a kind* owes one thing more: the
145/// kind-filtered reads select on the stored name ([`EventStore::read_kinds`]),
146/// so every caller that names that kind — [`super::Session::reserve`] and the
147/// balance fold name the `budget_*` ones, a resume names `session_closed` —
148/// has to name the old spelling beside the new one, or the older events fall
149/// out of the fold.
150pub fn kernel_upcasters() -> Vec<Arc<dyn Upcaster>> {
151    Vec::new()
152}
153
154/// Stamp [`CURRENT_SCHEMA_VERSION`] onto an event, overwriting any
155/// caller-supplied value (the version is the store's to assign, like `seq`).
156///
157/// Every backend calls this on append so future events carry the version an
158/// [`Upcaster`] dispatches on; `event.rs` owns `seq` / `epoch_ms` stamping
159/// and is left untouched, so the version is stamped here in the store layer.
160pub(super) fn stamp_schema_version(event: &mut Map<String, Value>) {
161    event.insert(
162        SCHEMA_VERSION_FIELD.to_string(),
163        Value::from(CURRENT_SCHEMA_VERSION),
164    );
165}
166
167/// A read-time transform from one event shape to the next.
168///
169/// Pure and infallible: an event whose version the upcaster does not
170/// recognise passes through unchanged.  Upcasting happens on *read* — the
171/// stored bytes are never rewritten — so an old log stays readable by new
172/// code without a migration pass.
173pub trait Upcaster: Send + Sync {
174    /// Transform one event, or return it unchanged when it does not apply.
175    fn upcast(&self, event: Value) -> Value;
176}
177
178/// Apply an upcaster `chain` to every event, in registration order.
179///
180/// The read-time application point: each event is folded through the chain
181/// front to back, so a two-step migration (`1 → 2`, then `2 → 3`) composes.
182/// An empty chain is the identity — the shape is fixed now even though no
183/// upcaster is registered yet.
184pub fn apply_upcasters(chain: &[Arc<dyn Upcaster>], events: Vec<Value>) -> Vec<Value> {
185    events
186        .into_iter()
187        .map(|event| chain.iter().fold(event, |event, up| up.upcast(event)))
188        .collect()
189}
190
191/// An event as the current shape: read through the upcaster chain.
192///
193/// The proof that a fold is looking at today's shape, carried in the type.
194/// A backend hands back what it stored, under whatever version it was
195/// written with; only [`CurrentStore`] turns those into `Current`s, because
196/// [`Current::from_upcasted`] is private to this module.  Every kernel fold
197/// takes `&[Current]`, so a read that skipped the seam cannot be handed to
198/// one — the call does not compile.
199///
200/// It derefs to the event's object, so a fold reads fields exactly as it did
201/// when it was handed a plain map; [`Current::into_inner`] gives that map up
202/// for a caller that has to own it (the Lua bridge, building tables).
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct Current(Map<String, Value>);
205
206impl Current {
207    /// Take an upcasted event as the current shape.  Private to the seam:
208    /// this is the one place a `Current` comes from.
209    ///
210    /// The debug assertion is the registration check.  Every stored event
211    /// carries the version it was written under, and the chain's job is to
212    /// bring it to [`CURRENT_SCHEMA_VERSION`]; an event that arrives here
213    /// still older than that is a step nobody registered, which is exactly
214    /// the mistake the seam exists to catch, and it is silent in every other
215    /// way.  It is a `debug_assert` because it is a check on the kernel's own
216    /// wiring, not on anything a caller passed in.
217    ///
218    /// A value that is not an object is corruption: the store's own writes
219    /// are objects (the validator refuses anything else), so a non-object
220    /// came back from the bytes rather than from a caller.
221    fn from_upcasted(event: Value) -> KnlResult<Self> {
222        let Value::Object(map) = event else {
223            return Err(KnlError::Corruption(format!(
224                "stored event is not an object: {event}"
225            )));
226        };
227        debug_assert_eq!(
228            map.get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
229            Some(CURRENT_SCHEMA_VERSION),
230            "the upcaster chain must bring every event to the current schema \
231             version; a step is missing from kernel_upcasters(): {map:?}"
232        );
233        Ok(Self(map))
234    }
235
236    /// Take a map as current *without* the chain — tests only.
237    ///
238    /// For the unit tests that drive a fold or a store directly, where there
239    /// is no seam to read through and the fixture is written in today's shape
240    /// by construction.  Not available outside `cfg(test)`, so it cannot
241    /// become a way round [`CurrentStore`].
242    #[cfg(test)]
243    pub fn assume_current(event: Value) -> Self {
244        match event {
245            Value::Object(map) => Self(map),
246            other => panic!("a test fixture event must be an object, got {other}"),
247        }
248    }
249
250    /// The `kind` of the event (empty when absent).
251    pub fn kind(&self) -> &str {
252        self.0.get(FIELD_KIND).and_then(Value::as_str).unwrap_or("")
253    }
254
255    /// The store-assigned `seq` of the event (`0` when absent).
256    pub fn seq(&self) -> u64 {
257        self.0.get(FIELD_SEQ).and_then(Value::as_u64).unwrap_or(0)
258    }
259
260    /// Give up the event's object — for a caller that has to own it.
261    pub fn into_inner(self) -> Map<String, Value> {
262        self.0
263    }
264}
265
266impl Deref for Current {
267    type Target = Map<String, Value>;
268
269    fn deref(&self) -> &Self::Target {
270        &self.0
271    }
272}
273
274impl std::fmt::Display for Current {
275    /// The event as JSON — what a failure message about an event wants to
276    /// show.  An object that will not serialise falls back to its debug
277    /// form rather than failing the formatter: this runs while something is
278    /// already being reported.
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match serde_json::to_string(&self.0) {
281            Ok(json) => f.write_str(&json),
282            Err(_) => write!(f, "{:?}", self.0),
283        }
284    }
285}
286
287/// What a command decides, given the stream it is decided against.
288///
289/// Handed the (kind-filtered) events in `seq` order and returning the event
290/// to record — or `None` to record nothing.  [`EventStore::append_if`] runs
291/// it inside the backend's serialization, which is what makes the decision
292/// and the write one step.
293///
294/// **Owned, `Send` and `'static`, and it takes its input by value.**  That is
295/// the whole shape of it, and it is what lets the decision travel to wherever
296/// the backend's serialization actually lives — the SQLite backend's own
297/// connection thread — so the read, the decision and the insert are one job
298/// rather than two parties waiting on each other across a channel with the
299/// write lock held.  A borrowed closure could not go anywhere, which is what
300/// that round trip was paying for.
301///
302/// `FnOnce`, because one call to `append_if` takes one decision: a backend
303/// that wanted to retry a contended transaction would have to be handed a
304/// fresh one, which is why neither backend retries this call.
305///
306/// The backend's own form, in raw [`Value`]s: the kernel's commands are
307/// written against [`CurrentDecision`] and [`CurrentStore`] projects between
308/// the two.
309pub type Decision = Box<dyn FnOnce(Vec<Value>) -> Option<Map<String, Value>> + Send + 'static>;
310
311/// [`Decision`] as the kernel writes one: decided on upcasted events.
312pub type CurrentDecision = Box<dyn FnOnce(Vec<Current>) -> Option<Map<String, Value>> + Send>;
313
314/// What a two-stream command writes, split by where each part lands.
315///
316/// The SPI is otherwise scoped to a single stream, and this is the one shape
317/// that is not: an allocation is a move *between* two ledgers, so the events
318/// that record it belong to two streams and either both land or neither may
319/// ([`EventStore::append_if_many`]).  Naming the two halves is what keeps
320/// that from becoming a list the backend has to guess the routing of.
321///
322/// `own` is this store's stream — the one it was opened on — and `other` is
323/// the stream named at the call.  Either may be empty: a refused allocation
324/// writes the refusal on the parent and opens nothing.
325///
326/// The same shape carries the decision's *input*
327/// ([`EventStore::append_if_many`]): what it is shown from this stream, and
328/// what it is shown from the other one.  One name for both directions, because
329/// the two streams are the same two streams either way.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct Split<T> {
332    /// What lands on this store's own stream.
333    pub own: Vec<T>,
334    /// What lands on the other stream of the same database.
335    pub other: Vec<T>,
336}
337
338impl<T> Split<T> {
339    /// A split that writes only this store's own stream.
340    pub fn own(own: Vec<T>) -> Self {
341        Self {
342            own,
343            other: Vec::new(),
344        }
345    }
346}
347
348/// A [`Decision`] that writes to two streams: the backend's form, in raw
349/// [`Value`]s.
350///
351/// It is shown a [`Split`] as well as returning one — `own` is this stream
352/// filtered by the call's `kinds`, `other` is at most the first event of the
353/// stream being written to ([`EventStore::append_if_many`]).
354pub type SplitDecision =
355    Box<dyn FnOnce(Split<Value>) -> Option<Split<Map<String, Value>>> + Send + 'static>;
356
357/// [`SplitDecision`] as the kernel writes one: decided on upcasted events.
358pub type CurrentSplitDecision =
359    Box<dyn FnOnce(Split<Current>) -> Option<Split<Map<String, Value>>> + Send>;
360
361/// What a closing event is built from once the store has found this stream's
362/// open children ([`EventStore::append_with_open_children`]).
363///
364/// Not an `Option`: the event is recorded whatever the scan found, because
365/// the children are something to *record* and never a reason to refuse a
366/// close.  The ids arrive in the order the scan produced them.
367pub type ChildrenDecision = Box<dyn FnOnce(Vec<String>) -> Map<String, Value> + Send + 'static>;
368
369/// How a backend recognises the streams that were opened *from* this one.
370///
371/// The kernel's kind vocabulary is not the store's — [`EventStore`] takes the
372/// kinds it reads as arguments everywhere else, and this is the same rule for
373/// a scan that has to look at other streams: the caller says which kind
374/// records an opening, which one records an ending, and which `data` field of
375/// the opening names the parent.  The backend does the walk and knows nothing
376/// about what the words mean.
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct ChildScan {
379    /// The kind that records a stream's opening.
380    pub opened: String,
381    /// The kind that records a stream's ending.
382    pub closed: String,
383    /// The `data` field of `opened` that names the parent's stream.
384    pub parent_field: String,
385}
386
387/// The coordinates a store assigns to an appended event.
388///
389/// Returned inline from every write so no follow-up read is needed to
390/// learn the `seq` / `epoch_ms` the store stamped.
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub struct Committed {
393    /// The store-assigned sequence number (gap-free, monotonic, from 1).
394    pub seq: u64,
395    /// The wall-clock append time the store stamped, in milliseconds.
396    pub epoch_ms: u64,
397}
398
399/// One session's append-only event log.
400///
401/// Scoped to a single stream: the session *is* the stream.  The trait is
402/// deliberately append-only — there is no mutation method, so a backend
403/// cannot offer one.
404///
405/// # Every call that can wait is `async`
406///
407/// A durable backend waits on something outside the process — a lock, a page,
408/// a disk — and the caller here is, in the end, the Lua VM's thread, which is
409/// the *only* worker of the runtime that also drives every other coroutine,
410/// timer and cancellation that VM owns.  A store method that blocked would
411/// stop all of them.  So the SPI is `async` and the waiting happens by
412/// yielding: [`super::SqliteEventStore`] sends each call to the thread that
413/// owns its connection and suspends on the answer.  The in-memory test store
414/// waits on nothing and is `async` only to fit the shape.
415///
416/// [`EventStore::detach_append`] is the single exception, and deliberately so:
417/// it is the drop backstop's path, where there is no caller left to wait.
418#[async_trait]
419pub trait EventStore: Send + Sync {
420    /// Validate, stamp and append an event, returning its coordinates.
421    ///
422    /// A rejected event leaves no trace and consumes no sequence number.
423    ///
424    /// **Serialized per stream by the backend.**  The store assigns the
425    /// `seq` and the ordering, and the append lands: two handles writing to
426    /// one stream interleave in arrival order rather than one of them being
427    /// refused for holding an out-of-date head.  SQLite takes an `IMMEDIATE`
428    /// transaction (with a bounded busy retry) around the head read and the
429    /// insert; the in-memory store is owned by one session in one process.
430    async fn append(&mut self, event: Map<String, Value>) -> KnlResult<Committed>;
431
432    /// Append `events` as one write, in the order given.
433    ///
434    /// For facts that are one fact: a session's `session_opened` and the
435    /// `budget_granted` it opened under are not two things that happened,
436    /// they are one opening, and a reader that can see the first without the
437    /// second is reading a stream that never existed.
438    ///
439    /// **All or nothing on a backend that can do it.**  SQLite takes a
440    /// single `IMMEDIATE` transaction, so a batch that fails part-way leaves
441    /// the stream exactly as it was.  The default below is the most a
442    /// backend with no transaction can offer — it appends one at a time, so
443    /// a failure part-way leaves what already landed — and the in-memory
444    /// store overrides it to validate the whole batch before writing any of
445    /// it, which is the only way its writes fail.
446    async fn append_many(&mut self, events: Vec<Map<String, Value>>) -> KnlResult<Vec<Committed>> {
447        let mut committed = Vec::with_capacity(events.len());
448        for event in events {
449            committed.push(self.append(event).await?);
450        }
451        Ok(committed)
452    }
453
454    /// Decide *inside* the store's serialization: read the stream, ask
455    /// `decide` what to write, and append its answer in the same write.
456    ///
457    /// `kinds` filters what the decision is shown, exactly as
458    /// [`EventStore::read_kinds`] filters a read (`None` = the whole
459    /// stream): a decision that folds the ledger asks for the `budget_*`
460    /// kinds and is handed those, in `seq` order.  What it *writes* is
461    /// unfiltered — the event it returns is appended whatever its kind.
462    ///
463    /// The form a command with an invariant takes — "reserve `n` only if the
464    /// balance covers it".  `decide` is handed the stream's events (in `seq`
465    /// order) as they are under the backend's lock, and returns the event to
466    /// record, or `None` to record nothing (`Ok(None)`, with the stream
467    /// untouched).  Because the read and the write share the transaction, the
468    /// decision cannot be raced by a concurrent writer — which a
469    /// compare-and-swap against a cached head could only detect afterwards.
470    ///
471    /// `decide` is called exactly once — it is a [`Decision`], which is
472    /// `FnOnce` — so neither backend retries a contended `append_if`: what a
473    /// second attempt would need is a second decision, and there is only one.
474    /// A contended write surfaces as [`KnlError::Busy`] instead, which is the
475    /// class that says another *call* is worth making.
476    ///
477    /// The kinds the decision asked for are read whole, which is what makes
478    /// the invariant exact; naming them is what keeps that from meaning the
479    /// whole stream.
480    async fn append_if(
481        &mut self,
482        kinds: Option<&[&str]>,
483        decide: Decision,
484    ) -> KnlResult<Option<Committed>>;
485
486    /// [`EventStore::append_if`] over two streams of one database: decide
487    /// against this stream, and write to this one *and* `other` in the same
488    /// transaction.
489    ///
490    /// The single-stream scoping above holds for everything else, and this is
491    /// the one operation that cannot live inside it.  An allocation moves
492    /// units from a parent's ledger to a child's: the reservation on one side
493    /// and the opening plus the grant on the other are three records of one
494    /// event, and a reader that could see either side alone would be reading
495    /// units that had left one ledger without arriving in another — or a
496    /// session that opened with a quota nobody paid for.
497    ///
498    /// `kinds` filters what the decision is shown *from this stream*, exactly
499    /// as it does for [`EventStore::append_if`], and it arrives as the `own`
500    /// half of a [`Split`].  A `None` decision writes nothing at all
501    /// (`Ok(None)`), and a [`Split`] with an empty `other` writes only this
502    /// stream — which is how a refusal is recorded without opening anything.
503    ///
504    /// **The other stream is read for one question**: whether it carries
505    /// anything at all.  A command that writes a stream's *first* events — an
506    /// allocation opens the child's log ([`super::Session::open_child`]) — has
507    /// an invariant about the target as well as about this ledger, and a
508    /// second `session_opened` landing on a stream that already had one is
509    /// exactly what asking beforehand would fail to catch.  So the decision is
510    /// shown the other stream's first event, unfiltered, in the `other` half of
511    /// its input: one row answers "is it empty", and reading more would be
512    /// paying for an answer nobody asked for.  A decision that does not care
513    /// ignores the field.
514    ///
515    /// **Both streams must be in one database**, which is the caller's to
516    /// arrange ([`EventStore::database`] says which one this store is on): a
517    /// backend has one connection and one transaction, so two databases have
518    /// no atomicity to offer.
519    ///
520    /// The default refuses.  A store that keeps a single stream has no other
521    /// stream to write to — nor to read the emptiness of — and the request
522    /// being well-formed while this backend cannot serve it is exactly
523    /// [`KnlError::Unsupported`] — the same answer [`EventStore::query`] gives
524    /// a store that is not a database.  The in-memory test store takes it as
525    /// it stands: it holds one stream, so there is nothing for it to override.
526    async fn append_if_many(
527        &mut self,
528        other: &str,
529        kinds: Option<&[&str]>,
530        decide: SplitDecision,
531    ) -> KnlResult<Option<Split<Committed>>> {
532        let _ = (other, kinds, decide);
533        Err(KnlError::Unsupported(
534            "this store keeps one stream, so it cannot write two in one transaction".to_string(),
535        ))
536    }
537
538    /// Append the event a decision builds from the ids of this stream's *open
539    /// children*, in one transaction with the scan that found them.
540    ///
541    /// The close path.  Which streams named this one as their parent, and
542    /// which of those have not ended, is a question about the whole database,
543    /// and asking it before the write would answer about a moment the write
544    /// does not happen in — a child could end, or a new one open, in between,
545    /// and the boundary would record something that was true just now.  So
546    /// the scan and the insert share the transaction, and the decision runs
547    /// between them.
548    ///
549    /// The decision is handed the ids and returns the event; there is no
550    /// `Option`, because open children are a fact to record and never a
551    /// reason to refuse a close ([`super::Session::close`]).
552    ///
553    /// The default is not a refusal but the truthful answer for a store that
554    /// keeps one stream: it has no other streams, therefore no children, so
555    /// the decision is shown an empty list and its event is appended
556    /// normally.
557    async fn append_with_open_children(
558        &mut self,
559        scan: &ChildScan,
560        decide: ChildrenDecision,
561    ) -> KnlResult<Committed> {
562        let _ = scan;
563        self.append(decide(Vec::new())).await
564    }
565
566    /// Which database this store's stream lives in, or `None` for a backend
567    /// that is not one.
568    ///
569    /// Two stores answer with the same string exactly when they are the same
570    /// database, which is the whole of what it is for: an allocation writes
571    /// two streams in one transaction, so the kernel checks that the child's
572    /// store is on the parent's database before it starts
573    /// ([`super::Session::open_child`]) rather than discovering it as a
574    /// half-written tree.  It is an identity, not a location a caller should
575    /// take apart.
576    fn database(&self) -> Option<&str> {
577        None
578    }
579
580    /// Events of `kinds` with `seq >= from_seq`, at most `limit`, cloned in
581    /// `seq` order.
582    ///
583    /// `None` reads every kind — the whole stream, as a plain range read.
584    /// `Some(kinds)` reads only those, and `limit` counts what came back
585    /// rather than what was skipped: a caller asking for two `budget_*`
586    /// events gets two, however much else is in between.  An empty slice
587    /// selects nothing.
588    ///
589    /// **The kind is the stored one.**  The selection happens in the backend,
590    /// before [`CurrentStore`] runs the upcaster chain, so an event is found
591    /// under the kind its bytes carry rather than the one it reads as.  While
592    /// no registered step renames a kind the two are the same word; a step
593    /// that *does* rename one obliges every filtered read of it to name both,
594    /// and [`kernel_upcasters`] is the site that has to say so.
595    ///
596    /// Fallible: a durable backend can hit a transient busy read or a row it
597    /// cannot decode, and those must surface rather than be silently dropped
598    /// (a dropped row would let [`super::Session::resume`] re-fold a truncated
599    /// log into the wrong state). The in-memory backend never errors.
600    async fn read_kinds(
601        &self,
602        kinds: Option<&[&str]>,
603        from_seq: u64,
604        limit: usize,
605    ) -> KnlResult<Vec<Value>>;
606
607    /// Every event with `seq >= from_seq`, at most `limit`: the unfiltered
608    /// [`EventStore::read_kinds`].
609    async fn read(&self, from_seq: u64, limit: usize) -> KnlResult<Vec<Value>> {
610        self.read_kinds(None, from_seq, limit).await
611    }
612
613    /// The last `n` events of the stream, in `seq` order.
614    ///
615    /// A read *from the end*, which the range reads above cannot express: they
616    /// start at a `seq` and count forward, so "the last five" could only be
617    /// asked for by reading the whole stream and throwing the front of it
618    /// away.  That is what the `tail` view used to do
619    /// ([`super::Session::view`]), and on a long log it is the whole log —
620    /// read, decoded and upcasted — to hand back five rows.
621    ///
622    /// The order handed back is `seq` ascending, like every other read: the
623    /// reversal is the backend's business, not the caller's.
624    ///
625    /// The default is the honest answer for a backend with no reverse read:
626    /// it reads the stream and keeps the end.  Both backends override it —
627    /// [`MemEventStore`] slices its `Vec`, and [`super::SqliteEventStore`]
628    /// asks SQLite for `ORDER BY seq DESC LIMIT n` and reverses what comes
629    /// back — so nothing in the product takes this path.
630    async fn read_last(&self, n: usize) -> KnlResult<Vec<Value>> {
631        let mut events = self.read(0, usize::MAX).await?;
632        let start = events.len().saturating_sub(n);
633        Ok(events.split_off(start))
634    }
635
636    /// The current head: the highest `seq`, or `None` for an empty stream.
637    ///
638    /// Fallible for the same reason as [`EventStore::read`]: a durable
639    /// backend can hit a transient busy read, and swallowing it would make
640    /// a populated stream look empty — the caller deciding open-vs-resume
641    /// (or a CAS comparing heads) must see the fault, not a wrong answer.
642    async fn head(&self) -> KnlResult<Option<u64>>;
643
644    /// Number of recorded events.  Fallible like [`EventStore::head`].
645    async fn len(&self) -> KnlResult<usize>;
646
647    /// Whether nothing has been recorded yet.
648    async fn is_empty(&self) -> KnlResult<bool> {
649        Ok(self.len().await? == 0)
650    }
651
652    /// Answer a caller's own SQL over the log ([`super::query`]).
653    ///
654    /// The read side of a store that keeps its events in a table it can be
655    /// asked about — which the product backend is, in both its file and its
656    /// in-memory form.  The `plan` has already been validated (one statement,
657    /// and it reads) and carries what the reserved parameters bind to; a
658    /// backend answers it on a connection that cannot write.
659    ///
660    /// The default refuses, because a store that is not a database has no
661    /// answer to give: the request is well-formed and this backend cannot
662    /// serve it, which is what [`KnlError::Unsupported`] says.  Only the test
663    /// doubles take it.
664    ///
665    /// **Queries read the stored shape, not the upcasted one.**  Every other
666    /// read a session makes goes through the upcaster seam
667    /// ([`CurrentStore`]); SQL runs against the bytes as they were written,
668    /// because the chain is Rust and the query is SQLite's.  A caller reading
669    /// across a schema change is reading the versions it finds — which is why
670    /// `schema_version` is a column.
671    async fn query(&self, plan: &super::query::QueryPlan) -> KnlResult<super::query::QueryRows> {
672        let _ = plan;
673        Err(KnlError::Unsupported(
674            "this store keeps no queryable table".to_string(),
675        ))
676    }
677
678    /// Submit `event` and do not wait for it to land — the drop backstop.
679    ///
680    /// The one synchronous call on this trait, because it is the one call with
681    /// no caller left: a handle nobody closed records its `session_closed`
682    /// from `Drop`, which cannot await and must not block (it runs inside a
683    /// Lua collection cycle, on the VM's thread).  So the event is handed to
684    /// whatever owns the writing, and whether it landed is reported to the log
685    /// rather than to anyone.
686    ///
687    /// The default says so and records nothing: a backend that cannot accept a
688    /// write without being awaited has nowhere to put this, and silently
689    /// dropping it would leave the stream looking open forever with no trace
690    /// of why.  [`super::SqliteEventStore`] overrides it.
691    fn detach_append(&self, event: Map<String, Value>) {
692        // Read out before the macro: `tracing`'s own `Value` trait is in
693        // scope inside the expansion and would shadow `serde_json`'s here.
694        let kind = event
695            .get(FIELD_KIND)
696            .and_then(Value::as_str)
697            .unwrap_or("")
698            .to_string();
699        tracing::warn!(
700            %kind,
701            "knl: this store cannot record a detached append; the event was not written"
702        );
703    }
704}
705
706/// A [`Vec`]-backed [`EventStore`] — **tests only**.
707///
708/// The product has one backend: a session's log is a SQLite table whether it
709/// is a file or an in-memory database ([`super::SqliteEventStore`]), because a
710/// log that cannot be queried cannot serve the view layer that reads it with
711/// SQL.  This one stays because the SPI, the upcasting seam and the folds are
712/// worth exercising without a database underneath, and because the failure
713/// injection the bridge's lifecycle tests need is easier to build on a
714/// [`Vec`] than on a connection.  It is `#[cfg(test)]` so it cannot become a
715/// second production backend by accident.
716#[cfg(test)]
717#[derive(Debug, Clone)]
718pub struct MemEventStore {
719    /// The append-only history that holds the events.
720    history: History,
721}
722
723#[cfg(test)]
724impl MemEventStore {
725    /// A fresh, empty in-memory store.
726    pub fn new() -> Self {
727        Self {
728            history: History::new(),
729        }
730    }
731
732    /// Borrow the inner history for the projection folds, which read a
733    /// `&History` directly.
734    pub fn history(&self) -> &History {
735        &self.history
736    }
737}
738
739#[cfg(test)]
740impl Default for MemEventStore {
741    /// The same fresh store as [`MemEventStore::new`] — note this is *not*
742    /// `History::default()`, whose `next_seq` would start at `0`.
743    fn default() -> Self {
744        Self::new()
745    }
746}
747
748#[cfg(test)]
749#[async_trait]
750impl EventStore for MemEventStore {
751    async fn append(&mut self, mut event: Map<String, Value>) -> KnlResult<Committed> {
752        // Stamp the schema version before the history validates and stamps
753        // `seq` / `epoch_ms`, so a stored event carries all three; a rejected
754        // event is dropped here and leaves no trace, as before.
755        stamp_schema_version(&mut event);
756        let seq = self.history.append(event)?;
757        // The append stamped `epoch_ms` on the event it just pushed; read
758        // it back off the tail (an O(1) index, not a round-trip) so the
759        // exact stored value is returned inline.
760        let epoch_ms = self
761            .history
762            .events()
763            .last()
764            .and_then(|event| event.get(FIELD_EPOCH_MS))
765            .and_then(Value::as_u64)
766            .unwrap_or(0);
767        Ok(Committed { seq, epoch_ms })
768    }
769
770    async fn append_many(&mut self, events: Vec<Map<String, Value>>) -> KnlResult<Vec<Committed>> {
771        // Validation is the only way an in-memory append fails, so checking
772        // the whole batch first is all this backend needs to make a batch
773        // all-or-nothing: past this loop every append below lands.
774        for event in &events {
775            validate_event(event)?;
776        }
777        let mut committed = Vec::with_capacity(events.len());
778        for event in events {
779            committed.push(self.append(event).await?);
780        }
781        Ok(committed)
782    }
783
784    async fn append_if(
785        &mut self,
786        kinds: Option<&[&str]>,
787        decide: Decision,
788    ) -> KnlResult<Option<Committed>> {
789        // One process, one owner: the read and the append below cannot be
790        // interleaved with another writer's, which is all the serialization
791        // this backend needs.
792        let events = self.read_kinds(kinds, 0, usize::MAX).await?;
793        match decide(events) {
794            Some(event) => self.append(event).await.map(Some),
795            None => Ok(None),
796        }
797    }
798
799    async fn read_kinds(
800        &self,
801        kinds: Option<&[&str]>,
802        from_seq: u64,
803        limit: usize,
804    ) -> KnlResult<Vec<Value>> {
805        // The in-memory history is infallible; the `Ok` is the SPI's shape,
806        // not a failure the mem backend can actually produce.
807        let mut events = self.history.since(from_seq);
808        if let Some(kinds) = kinds {
809            // Filtered before the cap, so `limit` counts what the caller
810            // asked for rather than what was skipped on the way.
811            events.retain(|event| kinds.contains(&kind_of(event)));
812        }
813        events.truncate(limit);
814        Ok(events)
815    }
816
817    async fn read_last(&self, n: usize) -> KnlResult<Vec<Value>> {
818        // A `Vec` in this process: the end of it is a slice, so the reverse
819        // read the durable backend does in SQL is a `split_off` here.
820        let mut events = self.history.since(0);
821        let start = events.len().saturating_sub(n);
822        Ok(events.split_off(start))
823    }
824
825    async fn head(&self) -> KnlResult<Option<u64>> {
826        // `seq` is monotonic and gap-free, so the last event carries the
827        // highest one.  Infallible in memory; the `Ok` is the SPI's shape.
828        Ok(self.history.events().last().map(seq_of))
829    }
830
831    async fn len(&self) -> KnlResult<usize> {
832        Ok(self.history.len())
833    }
834}
835
836/// The seam: the only way to read a stream as the current shape.
837///
838/// Wraps a backend and a `chain` of [`Upcaster`]s.  Reads fold the chain over
839/// the events and hand back [`Current`]s (read-time projection); every write
840/// passes straight through, so the stored bytes are never rewritten — the same
841/// old-log-stays-readable discipline [`Upcaster`] describes, established once
842/// here as the single site a future upcaster registers into.
843///
844/// **Not an [`EventStore`], on purpose.**  It offers the same calls, but its
845/// reads are `Vec<Current>` rather than `Vec<Value>`, so it cannot stand in
846/// for a backend and a backend cannot stand in for it.  [`super::Session`]
847/// holds one of these and never a bare `Box<dyn EventStore>`, which is what
848/// makes "the folds only ever see upcasted events" a property of the types
849/// instead of a rule someone has to remember.
850///
851/// An empty chain is a functional no-op, which is the state today: v1 has no
852/// upcaster, so the projection changes nothing, but a later shape change
853/// registers its `n → n+1` step here and every read path picks it up.
854pub struct CurrentStore {
855    /// The wrapped backend that actually holds the events.
856    inner: Box<dyn EventStore>,
857    /// The read-time upcaster chain, applied front to back on every read.
858    chain: Vec<Arc<dyn Upcaster>>,
859}
860
861impl CurrentStore {
862    /// Wrap `inner` so its reads are upcasted through `chain`.
863    ///
864    /// An empty `chain` projects the events unchanged — they are already the
865    /// current shape.
866    pub fn new(inner: Box<dyn EventStore>, chain: Vec<Arc<dyn Upcaster>>) -> Self {
867        Self { inner, chain }
868    }
869
870    /// Hand `event` to the backend without waiting for it to land
871    /// ([`EventStore::detach_append`]).
872    ///
873    /// Straight through and not upcasted, like every other write.
874    pub fn detach_append(&self, event: Map<String, Value>) {
875        self.inner.detach_append(event);
876    }
877
878    /// Fold `chain` over `events` and take the results as [`Current`].
879    ///
880    /// The one place a `Current` is minted, so every one of them has been
881    /// through the chain by construction.
882    fn project(chain: &[Arc<dyn Upcaster>], events: Vec<Value>) -> KnlResult<Vec<Current>> {
883        apply_upcasters(chain, events)
884            .into_iter()
885            .map(Current::from_upcasted)
886            .collect()
887    }
888
889    /// Validate, stamp and append an event ([`EventStore::append`]).
890    pub async fn append(&mut self, event: Map<String, Value>) -> KnlResult<Committed> {
891        // Write path untouched: upcasting is read-time only.
892        self.inner.append(event).await
893    }
894
895    /// Append events as one write ([`EventStore::append_many`]).
896    pub async fn append_many(
897        &mut self,
898        events: Vec<Map<String, Value>>,
899    ) -> KnlResult<Vec<Committed>> {
900        self.inner.append_many(events).await
901    }
902
903    /// Decide inside the store's serialization ([`EventStore::append_if`]),
904    /// on events projected to the current shape.
905    pub async fn append_if(
906        &mut self,
907        kinds: Option<&[&str]>,
908        decide: CurrentDecision,
909    ) -> KnlResult<Option<Committed>> {
910        // The decision is a read, so it is upcasted like every other read: the
911        // backend hands over the stored events, the chain projects them, and
912        // `decide` sees the current shape — a v1 log decides the same way a
913        // v2 one does.  The projection travels with the decision, because the
914        // decision travels: both run wherever the backend serializes its
915        // writes, which for the durable one is its connection thread.
916        let chain = self.chain.clone();
917        // A projection failure has nowhere to go through the backend's
918        // `Decision`, which answers with an event or nothing.  So it is parked
919        // in a cell both sides can reach and raised below: `decide` is not
920        // called, nothing is written, and the caller is told the read failed
921        // rather than being handed the `None` that would read as "the
922        // invariant said no".
923        let failure: Arc<Mutex<Option<KnlError>>> = Arc::new(Mutex::new(None));
924        let parked = Arc::clone(&failure);
925        let upcasted: Decision =
926            Box::new(
927                move |events: Vec<Value>| match Self::project(&chain, events) {
928                    Ok(current) => decide(current),
929                    Err(fault) => {
930                        *parked.lock().unwrap_or_else(PoisonError::into_inner) = Some(fault);
931                        None
932                    }
933                },
934            );
935        let committed = self.inner.append_if(kinds, upcasted).await;
936        let parked = failure
937            .lock()
938            .unwrap_or_else(PoisonError::into_inner)
939            .take();
940        match parked {
941            Some(fault) => Err(fault),
942            None => committed,
943        }
944    }
945
946    /// Decide inside the store's serialization and write two streams of one
947    /// database ([`EventStore::append_if_many`]), on events projected to the
948    /// current shape.
949    ///
950    /// The same projection [`CurrentStore::append_if`] makes, for the same
951    /// reason and with the same parking of a failure the backend's decision
952    /// has nowhere to report one through: a read that could not be projected
953    /// must not reach the decision as an empty stream, which is what a
954    /// balance would fold to zero from.
955    ///
956    /// **Both halves go through the chain.**  The other stream's first event
957    /// is a read like any other, and a decision that meets it unprojected
958    /// would be the one place in the kernel where a stored shape reaches a
959    /// fold as it was written.  A projection failure on either half parks the
960    /// same way and the decision is not called at all — which matters most
961    /// here, since an unreadable other stream would otherwise arrive as the
962    /// empty one it is being checked for.
963    pub async fn append_if_many(
964        &mut self,
965        other: &str,
966        kinds: Option<&[&str]>,
967        decide: CurrentSplitDecision,
968    ) -> KnlResult<Option<Split<Committed>>> {
969        let chain = self.chain.clone();
970        let failure: Arc<Mutex<Option<KnlError>>> = Arc::new(Mutex::new(None));
971        let parked = Arc::clone(&failure);
972        let upcasted: SplitDecision = Box::new(move |events: Split<Value>| {
973            let projected = Self::project(&chain, events.own).and_then(|own| {
974                let other = Self::project(&chain, events.other)?;
975                Ok(Split { own, other })
976            });
977            match projected {
978                Ok(current) => decide(current),
979                Err(fault) => {
980                    *parked.lock().unwrap_or_else(PoisonError::into_inner) = Some(fault);
981                    None
982                }
983            }
984        });
985        let committed = self.inner.append_if_many(other, kinds, upcasted).await;
986        let parked = failure
987            .lock()
988            .unwrap_or_else(PoisonError::into_inner)
989            .take();
990        match parked {
991            Some(fault) => Err(fault),
992            None => committed,
993        }
994    }
995
996    /// Append a closing event built from this stream's open children
997    /// ([`EventStore::append_with_open_children`]).
998    ///
999    /// Straight through, and nothing to project: the decision is shown stream
1000    /// *ids*, not events, so there is no stored shape here for the chain to
1001    /// bring forward.
1002    pub async fn append_with_open_children(
1003        &mut self,
1004        scan: &ChildScan,
1005        decide: ChildrenDecision,
1006    ) -> KnlResult<Committed> {
1007        self.inner.append_with_open_children(scan, decide).await
1008    }
1009
1010    /// Which database the backend's stream lives in ([`EventStore::database`]).
1011    pub fn database(&self) -> Option<&str> {
1012        self.inner.database()
1013    }
1014
1015    /// Events of `kinds` from `from_seq` on, as the current shape
1016    /// ([`EventStore::read_kinds`]).
1017    pub async fn read_kinds(
1018        &self,
1019        kinds: Option<&[&str]>,
1020        from_seq: u64,
1021        limit: usize,
1022    ) -> KnlResult<Vec<Current>> {
1023        // The single read-time application point: read from the backend, then
1024        // fold the chain over the events before handing them back.
1025        let events = self.inner.read_kinds(kinds, from_seq, limit).await?;
1026        Self::project(&self.chain, events)
1027    }
1028
1029    /// Every event from `from_seq` on, as the current shape.
1030    pub async fn read(&self, from_seq: u64, limit: usize) -> KnlResult<Vec<Current>> {
1031        self.read_kinds(None, from_seq, limit).await
1032    }
1033
1034    /// The last `n` events, as the current shape
1035    /// ([`EventStore::read_last`]).
1036    ///
1037    /// Through the same seam as every other read, so a `tail` taken off the
1038    /// end of an old log reads as what it means today — and only `n` events
1039    /// are put through the chain, which is the point of asking the backend
1040    /// for the end rather than for everything.
1041    pub async fn read_last(&self, n: usize) -> KnlResult<Vec<Current>> {
1042        let events = self.inner.read_last(n).await?;
1043        Self::project(&self.chain, events)
1044    }
1045
1046    /// The backend's head ([`EventStore::head`]).
1047    pub async fn head(&self) -> KnlResult<Option<u64>> {
1048        self.inner.head().await
1049    }
1050
1051    /// How many events the backend holds ([`EventStore::len`]).
1052    pub async fn len(&self) -> KnlResult<usize> {
1053        self.inner.len().await
1054    }
1055
1056    /// Whether the backend holds nothing yet.
1057    pub async fn is_empty(&self) -> KnlResult<bool> {
1058        self.inner.is_empty().await
1059    }
1060
1061    /// Answer a caller's SQL ([`EventStore::query`]).
1062    ///
1063    /// Straight through, and deliberately *not* upcasted: the chain is a Rust
1064    /// transform over whole events and a query selects columns, so there is
1065    /// nothing here to project.  A query reads the stored shape, which is why
1066    /// the version each row was written under is a column of the table.
1067    pub async fn query(
1068        &self,
1069        plan: &super::query::QueryPlan,
1070    ) -> KnlResult<super::query::QueryRows> {
1071        self.inner.query(plan).await
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078    use crate::knl::event::{KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT};
1079    use serde_json::json;
1080
1081    /// Object map for an event literal.
1082    fn obj(value: Value) -> Map<String, Value> {
1083        match value {
1084            Value::Object(map) => map,
1085            other => panic!("test fixture must be an object, got {other}"),
1086        }
1087    }
1088
1089    /// An open-kind event named `e{i}`.
1090    fn ev(i: usize) -> Map<String, Value> {
1091        obj(json!({ "kind": format!("e{i}") }))
1092    }
1093
1094    /// A decision as [`EventStore::append_if`] takes one: owned, and handed
1095    /// its input by value.
1096    fn decide(
1097        f: impl FnOnce(Vec<Value>) -> Option<Map<String, Value>> + Send + 'static,
1098    ) -> Decision {
1099        Box::new(f)
1100    }
1101
1102    /// The same, at the current shape ([`CurrentStore::append_if`]).
1103    fn decide_current(
1104        f: impl FnOnce(Vec<Current>) -> Option<Map<String, Value>> + Send + 'static,
1105    ) -> CurrentDecision {
1106        Box::new(f)
1107    }
1108
1109    #[tokio::test]
1110    async fn append_assigns_gap_free_monotonic_seq_from_one() {
1111        let mut store = MemEventStore::new();
1112        assert!(store.is_empty().await.expect("is_empty"));
1113        assert_eq!(store.len().await.expect("len"), 0);
1114
1115        let a = store.append(ev(1)).await.expect("append e1");
1116        let b = store.append(ev(2)).await.expect("append e2");
1117        let c = store.append(ev(3)).await.expect("append e3");
1118
1119        assert_eq!((a.seq, b.seq, c.seq), (1, 2, 3));
1120        assert!(a.epoch_ms >= 1 || a.epoch_ms == 0, "epoch is stamped");
1121        assert_eq!(store.len().await.expect("len"), 3);
1122        assert!(!store.is_empty().await.expect("is_empty"));
1123
1124        // The stamped epoch is what is stored.
1125        let stored = store.read(0, usize::MAX).await.expect("read");
1126        let stored_epoch = stored[0]
1127            .get(FIELD_EPOCH_MS)
1128            .and_then(Value::as_u64)
1129            .expect("epoch is on the stored event");
1130        assert_eq!(stored_epoch, a.epoch_ms);
1131    }
1132
1133    #[tokio::test]
1134    async fn a_rejected_append_records_nothing_and_burns_no_seq() {
1135        let mut store = MemEventStore::new();
1136        store
1137            .append(obj(json!({ "text": "no kind" })))
1138            .await
1139            .expect_err("kind is required");
1140        assert_eq!(store.len().await.expect("len"), 0);
1141        assert_eq!(store.append(ev(1)).await.expect("append").seq, 1);
1142    }
1143
1144    /// `append_if` decides on the stream the backend hands it and writes in
1145    /// the same step: a `Some` lands, a `None` writes nothing at all.
1146    #[tokio::test]
1147    async fn append_if_decides_on_the_stream_and_writes_only_a_some() {
1148        let mut store = MemEventStore::new();
1149        store.append(ev(1)).await.expect("seed");
1150
1151        // The decision sees the stream as it is, in seq order.  It is owned
1152        // now, so what it saw comes back through a shared cell rather than a
1153        // borrow of a local.
1154        let seen = Arc::new(Mutex::new(0_usize));
1155        let counted = Arc::clone(&seen);
1156        let committed = store
1157            .append_if(
1158                None,
1159                decide(move |events| {
1160                    *counted.lock().expect("not poisoned") = events.len();
1161                    Some(ev(2))
1162                }),
1163            )
1164            .await
1165            .expect("append_if");
1166        assert_eq!(
1167            *seen.lock().expect("not poisoned"),
1168            1,
1169            "decide was handed the whole stream"
1170        );
1171        assert_eq!(committed.map(|c| c.seq), Some(2));
1172
1173        // `None` is a decision too: nothing is written and no seq is burnt.
1174        let nothing = store
1175            .append_if(None, decide(|_| None))
1176            .await
1177            .expect("append_if");
1178        assert_eq!(nothing, None);
1179        assert_eq!(store.len().await.expect("len"), 2, "a None writes nothing");
1180        assert_eq!(store.append(ev(3)).await.expect("append").seq, 3);
1181    }
1182
1183    /// The event a decision returns is validated like any other: a malformed
1184    /// one is refused and the stream is untouched.
1185    #[tokio::test]
1186    async fn append_if_validates_the_event_the_decision_returns() {
1187        let mut store = MemEventStore::new();
1188        store
1189            .append_if(None, decide(|_| Some(obj(json!({ "text": "no kind" })))))
1190            .await
1191            .expect_err("kind is required");
1192        assert_eq!(store.len().await.expect("len"), 0);
1193    }
1194
1195    /// A decision names the kinds it folds, and is handed those and nothing
1196    /// else — in `seq` order, from the whole stream, however much else is in
1197    /// between.  What it *writes* is not filtered.
1198    #[tokio::test]
1199    async fn append_if_shows_the_decision_only_the_kinds_it_asked_for() {
1200        let mut store = MemEventStore::new();
1201        store
1202            .append(obj(
1203                json!({ "kind": KIND_BUDGET_GRANTED, "data": { "amount": 100 } }),
1204            ))
1205            .await
1206            .expect("the grant");
1207        store.append(ev(1)).await.expect("noise");
1208        store.append(ev(2)).await.expect("more noise");
1209
1210        let seen: Arc<Mutex<Vec<String>>> = Arc::default();
1211        let recorded = Arc::clone(&seen);
1212        let committed = store
1213            .append_if(
1214                Some(&[KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]),
1215                decide(move |events| {
1216                    *recorded.lock().expect("not poisoned") =
1217                        events.iter().map(|e| kind_of(e).to_string()).collect();
1218                    Some(obj(
1219                        json!({ "kind": KIND_BUDGET_SPENT, "data": { "amount": 10 } }),
1220                    ))
1221                }),
1222            )
1223            .await
1224            .expect("append_if");
1225        assert_eq!(
1226            *seen.lock().expect("not poisoned"),
1227            [KIND_BUDGET_GRANTED],
1228            "only the kinds asked for"
1229        );
1230        assert_eq!(
1231            committed.map(|c| c.seq),
1232            Some(4),
1233            "the write is not filtered"
1234        );
1235
1236        // The next decision sees what the last one wrote, since it is one of
1237        // the kinds it asked for.
1238        let recorded = Arc::clone(&seen);
1239        store
1240            .append_if(
1241                Some(&[KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]),
1242                decide(move |events| {
1243                    *recorded.lock().expect("not poisoned") =
1244                        events.iter().map(|e| kind_of(e).to_string()).collect();
1245                    None
1246                }),
1247            )
1248            .await
1249            .expect("append_if");
1250        assert_eq!(
1251            *seen.lock().expect("not poisoned"),
1252            [KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]
1253        );
1254    }
1255
1256    /// A batch is one write: the events land in order, and a batch with a
1257    /// bad event in it lands nothing at all — the stream is as it was.
1258    #[tokio::test]
1259    async fn append_many_records_the_batch_in_order_or_records_nothing() {
1260        let mut store = MemEventStore::new();
1261
1262        let committed = store
1263            .append_many(vec![ev(1), ev(2)])
1264            .await
1265            .expect("the batch");
1266        assert_eq!(
1267            committed.iter().map(|c| c.seq).collect::<Vec<_>>(),
1268            [1, 2],
1269            "the batch lands in the order it was given"
1270        );
1271        let stored = store.read(0, usize::MAX).await.expect("read");
1272        let kinds: Vec<&str> = stored.iter().map(kind_of).collect();
1273        assert_eq!(kinds, ["e1", "e2"]);
1274
1275        // The second event is malformed, so neither is recorded: a batch that
1276        // half-lands is the shape `append_many` exists to rule out.
1277        store
1278            .append_many(vec![ev(3), obj(json!({ "text": "no kind" }))])
1279            .await
1280            .expect_err("kind is required");
1281        assert_eq!(
1282            store.len().await.expect("len"),
1283            2,
1284            "a failed batch wrote nothing"
1285        );
1286        assert_eq!(
1287            store.append(ev(4)).await.expect("append").seq,
1288            3,
1289            "no seq burnt"
1290        );
1291    }
1292
1293    /// `read_kinds` selects by kind, pages by `from_seq` / `limit`, and reads
1294    /// nothing at all for an empty selection.
1295    #[tokio::test]
1296    async fn read_kinds_selects_by_kind_and_still_pages() {
1297        let mut store = MemEventStore::new();
1298        store
1299            .append(obj(
1300                json!({ "kind": KIND_BUDGET_GRANTED, "data": { "amount": 100 } }),
1301            ))
1302            .await
1303            .expect("grant");
1304        store.append(ev(1)).await.expect("noise");
1305        store
1306            .append(obj(
1307                json!({ "kind": KIND_BUDGET_SPENT, "data": { "amount": 10 } }),
1308            ))
1309            .await
1310            .expect("spend");
1311        store.append(ev(2)).await.expect("more noise");
1312
1313        let ledger = store
1314            .read_kinds(
1315                Some(&[KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]),
1316                0,
1317                usize::MAX,
1318            )
1319            .await
1320            .expect("read_kinds");
1321        let kinds: Vec<&str> = ledger.iter().map(kind_of).collect();
1322        assert_eq!(kinds, [KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]);
1323        assert_eq!(
1324            seq_of(&ledger[1]),
1325            3,
1326            "the seq is the stream's, not the fold's"
1327        );
1328
1329        // `from_seq` and `limit` still apply, and the cap counts what came
1330        // back rather than what was skipped.
1331        assert_eq!(
1332            store
1333                .read_kinds(Some(&[KIND_BUDGET_GRANTED]), 2, usize::MAX)
1334                .await
1335                .expect("read_kinds")
1336                .len(),
1337            0,
1338            "the grant is before from_seq"
1339        );
1340        assert_eq!(
1341            store
1342                .read_kinds(Some(&[KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]), 0, 1)
1343                .await
1344                .expect("read_kinds")
1345                .len(),
1346            1
1347        );
1348
1349        // An empty selection selects nothing; `None` is the whole stream.
1350        assert!(store
1351            .read_kinds(Some(&[]), 0, usize::MAX)
1352            .await
1353            .expect("read_kinds")
1354            .is_empty());
1355        assert_eq!(
1356            store
1357                .read_kinds(None, 0, usize::MAX)
1358                .await
1359                .expect("read_kinds")
1360                .len(),
1361            4,
1362            "read() is read_kinds(None, ..)"
1363        );
1364        assert_eq!(store.read(0, usize::MAX).await.expect("read").len(), 4);
1365    }
1366
1367    #[tokio::test]
1368    async fn read_pages_by_from_seq_and_limit() {
1369        let mut store = MemEventStore::new();
1370        for i in 1..=5 {
1371            store.append(ev(i)).await.expect("append");
1372        }
1373
1374        // from_seq filters, limit caps.
1375        assert_eq!(store.read(0, usize::MAX).await.expect("read").len(), 5);
1376        assert_eq!(store.read(1, usize::MAX).await.expect("read").len(), 5);
1377        assert_eq!(store.read(3, usize::MAX).await.expect("read").len(), 3);
1378        assert_eq!(store.read(6, usize::MAX).await.expect("read").len(), 0);
1379
1380        let page = store.read(2, 2).await.expect("read");
1381        assert_eq!(page.len(), 2);
1382        assert_eq!(kind_of(&page[0]), "e2");
1383        assert_eq!(kind_of(&page[1]), "e3");
1384
1385        // A zero limit returns nothing even when events match.
1386        assert!(store.read(0, 0).await.expect("read").is_empty());
1387    }
1388
1389    #[tokio::test]
1390    async fn head_is_none_when_empty_then_tracks_the_max() {
1391        let mut store = MemEventStore::new();
1392        assert_eq!(store.head().await.expect("head"), None);
1393
1394        store.append(ev(1)).await.expect("append");
1395        assert_eq!(store.head().await.expect("head"), Some(1));
1396        store.append(ev(2)).await.expect("append");
1397        assert_eq!(store.head().await.expect("head"), Some(2));
1398
1399        // A rejected append does not move the head.
1400        store
1401            .append(obj(json!({ "text": "no kind" })))
1402            .await
1403            .expect_err("kind is required");
1404        assert_eq!(store.head().await.expect("head"), Some(2));
1405    }
1406
1407    #[tokio::test]
1408    async fn default_matches_new_and_starts_seq_at_one() {
1409        let mut store = MemEventStore::default();
1410        assert!(store.is_empty().await.expect("is_empty"));
1411        // Guards against `History::default()` (next_seq == 0) leaking in.
1412        assert_eq!(store.append(ev(1)).await.expect("append").seq, 1);
1413    }
1414
1415    #[tokio::test]
1416    async fn reads_are_copies_so_the_store_cannot_be_reached_through_them() {
1417        let mut store = MemEventStore::new();
1418        store.append(ev(1)).await.expect("append");
1419        let mut copy = store.read(0, usize::MAX).await.expect("read");
1420        copy[0][FIELD_KIND] = Value::String("TAMPERED".into());
1421        let again = store.read(0, usize::MAX).await.expect("read");
1422        assert_eq!(kind_of(&again[0]), "e1");
1423    }
1424
1425    #[tokio::test]
1426    async fn append_stamps_the_current_schema_version() {
1427        let mut store = MemEventStore::new();
1428        store.append(ev(1)).await.expect("append");
1429        let stored = store.read(0, usize::MAX).await.expect("read");
1430        assert_eq!(
1431            stored[0].get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
1432            Some(CURRENT_SCHEMA_VERSION),
1433            "a stored event carries the version it was written under: {}",
1434            stored[0]
1435        );
1436    }
1437
1438    /// A store that keeps one stream has no second one to write, and no
1439    /// database to share with another store: both answers are "not this
1440    /// backend", said plainly, rather than a write that lands somewhere
1441    /// unexpected.
1442    #[tokio::test]
1443    async fn a_single_stream_store_cannot_write_two_streams() {
1444        let mut store = MemEventStore::new();
1445        assert_eq!(store.database(), None, "one stream, no database to share");
1446
1447        let err = store
1448            .append_if_many(
1449                "another-stream",
1450                None,
1451                Box::new(|_| {
1452                    panic!("the decision must not run: there is nowhere for its other half to go")
1453                }),
1454            )
1455            .await
1456            .expect_err("two streams are a durable backend's");
1457        assert_eq!(err.kind(), KnlError::UNSUPPORTED, "{err}");
1458        assert!(!err.is_retryable(), "asking again changes nothing: {err}");
1459        assert_eq!(
1460            store.len().await.expect("len"),
1461            0,
1462            "and nothing was written"
1463        );
1464    }
1465
1466    /// The same store has no *children* either — there are no other streams
1467    /// for one to be in — so a close over it is shown an empty list and its
1468    /// event is appended like any other.  That is the truthful answer, not a
1469    /// refusal: the question was asked and the answer is none.
1470    #[tokio::test]
1471    async fn a_single_stream_store_finds_no_children_and_appends_anyway() {
1472        let mut store = MemEventStore::new();
1473        let scan = ChildScan {
1474            opened: "session_opened".to_string(),
1475            closed: "session_closed".to_string(),
1476            parent_field: "parent".to_string(),
1477        };
1478        let seen: Arc<Mutex<Option<usize>>> = Arc::default();
1479        let counted = Arc::clone(&seen);
1480        let committed = store
1481            .append_with_open_children(
1482                &scan,
1483                Box::new(move |children| {
1484                    *counted.lock().expect("not poisoned") = Some(children.len());
1485                    ev(1)
1486                }),
1487            )
1488            .await
1489            .expect("the close lands");
1490
1491        assert_eq!(*seen.lock().expect("not poisoned"), Some(0));
1492        assert_eq!(committed.seq, 1);
1493        assert_eq!(store.len().await.expect("len"), 1);
1494    }
1495
1496    /// A store that is not a database says so, rather than answering a query
1497    /// with an empty result — which would read as "there is nothing there".
1498    /// The product backend is SQLite in every build; this is the answer the
1499    /// `Vec`-backed test store gives.
1500    #[tokio::test]
1501    async fn a_store_with_no_table_refuses_a_query() {
1502        use crate::knl::query::{plan, QueryOpts, QueryParams};
1503
1504        let store = MemEventStore::new();
1505        let asked = plan(
1506            "SELECT 1",
1507            QueryParams::None,
1508            &QueryOpts::default(),
1509            "a-stream",
1510        )
1511        .expect("a plan");
1512        let err = store
1513            .query(&asked)
1514            .await
1515            .expect_err("there is no table to query");
1516        assert_eq!(err.kind(), KnlError::UNSUPPORTED, "{err}");
1517        assert!(!err.is_retryable(), "asking again changes nothing: {err}");
1518    }
1519
1520    #[test]
1521    fn an_empty_upcaster_chain_is_the_identity() {
1522        let events = vec![
1523            json!({ "kind": "a", "seq": 1 }),
1524            json!({ "kind": "b", "seq": 2 }),
1525        ];
1526        assert_eq!(apply_upcasters(&[], events.clone()), events);
1527    }
1528
1529    #[test]
1530    fn upcasters_compose_per_event_in_registration_order() {
1531        use std::sync::Arc;
1532
1533        // Each upcaster pushes its label onto a `trace` array, so the final
1534        // order proves the chain walked front to back, once per event.
1535        struct Tag(&'static str);
1536        impl Upcaster for Tag {
1537            fn upcast(&self, mut event: Value) -> Value {
1538                let map = event.as_object_mut().expect("event is an object");
1539                let trace = map
1540                    .entry("trace")
1541                    .or_insert_with(|| Value::Array(Vec::new()));
1542                trace
1543                    .as_array_mut()
1544                    .expect("trace is an array")
1545                    .push(Value::from(self.0));
1546                event
1547            }
1548        }
1549
1550        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(Tag("first")), Arc::new(Tag("second"))];
1551        let out = apply_upcasters(&chain, vec![json!({ "kind": "x" }), json!({ "kind": "y" })]);
1552        assert_eq!(out[0]["trace"], json!(["first", "second"]));
1553        assert_eq!(out[1]["trace"], json!(["first", "second"]));
1554    }
1555
1556    /// (Fix 4) `CurrentStore` projects the chain on read while leaving the
1557    /// write path untouched: an appended event is stored raw, and the marker
1558    /// only appears in the read projection — it never accumulates, so the
1559    /// stored bytes carry no upcaster field.
1560    #[tokio::test]
1561    async fn the_seam_projects_on_read_and_leaves_writes_untouched() {
1562        // Pushes a marker onto a `trace` array, so a value upcasted twice would
1563        // show two entries — this distinguishes a read-time projection from a
1564        // stored rewrite.
1565        struct Mark;
1566        impl Upcaster for Mark {
1567            fn upcast(&self, mut event: Value) -> Value {
1568                let map = event.as_object_mut().expect("event is an object");
1569                map.entry("trace")
1570                    .or_insert_with(|| Value::Array(Vec::new()))
1571                    .as_array_mut()
1572                    .expect("trace is an array")
1573                    .push(Value::from("mark"));
1574                event
1575            }
1576        }
1577
1578        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(Mark)];
1579        let mut store = CurrentStore::new(Box::new(MemEventStore::new()), chain);
1580
1581        // Write path passes through: coordinates and counters are the backend's.
1582        let a = store.append(ev(1)).await.expect("append e1");
1583        assert_eq!(a.seq, 1);
1584        assert_eq!(store.head().await.expect("head"), Some(1));
1585        assert_eq!(store.len().await.expect("len"), 1);
1586        assert!(!store.is_empty().await.expect("is_empty"));
1587
1588        // read projects the marker on.
1589        let first = store.read(0, usize::MAX).await.expect("read");
1590        assert_eq!(first[0]["trace"], json!(["mark"]), "read applies the chain");
1591
1592        // A second read is consistent — the marker does not accumulate, proving
1593        // the append stored no marker (the write path is not upcasted).
1594        let second = store.read(0, usize::MAX).await.expect("read again");
1595        assert_eq!(
1596            second[0]["trace"],
1597            json!(["mark"]),
1598            "stored bytes carry no marker; read adds exactly one"
1599        );
1600
1601        // A freshly appended event reads back consistently under the same chain,
1602        // and head / len stay in step with the backend.
1603        let b = store.append(ev(2)).await.expect("append e2");
1604        assert_eq!(b.seq, 2);
1605        assert_eq!(store.head().await.expect("head"), Some(2));
1606        assert_eq!(store.len().await.expect("len"), 2);
1607        let both = store.read(0, usize::MAX).await.expect("read both");
1608        assert_eq!(both.len(), 2);
1609        assert_eq!(both[1].kind(), "e2");
1610        assert_eq!(both[1].seq(), 2, "a Current keeps its coordinates");
1611        assert_eq!(both[1]["trace"], json!(["mark"]));
1612    }
1613
1614    /// (Fix 4) An empty chain makes the seam an identity over its backend:
1615    /// reads return the events unchanged, with no upcaster-added field, and the
1616    /// coordinates track the backend exactly.
1617    #[tokio::test]
1618    async fn the_seam_with_an_empty_chain_returns_events_unchanged() {
1619        let mut store = CurrentStore::new(Box::new(MemEventStore::new()), Vec::new());
1620        assert!(store.is_empty().await.expect("is_empty"));
1621
1622        store.append(ev(1)).await.expect("append");
1623        let read = store.read(0, usize::MAX).await.expect("read");
1624        assert_eq!(read.len(), 1);
1625        assert_eq!(read[0].kind(), "e1", "the event passes through unchanged");
1626        assert!(
1627            read[0].get("trace").is_none(),
1628            "an empty chain adds nothing: {:?}",
1629            read[0]
1630        );
1631        assert_eq!(store.head().await.expect("head"), Some(1));
1632        assert_eq!(store.len().await.expect("len"), 1);
1633    }
1634
1635    /// The kind filter selects on the kind that is *stored*; the chain runs
1636    /// after it.  A step that renames a kind therefore has to be read for
1637    /// under its old name — which is what the note on
1638    /// [`EventStore::read_kinds`] obliges a future rename to do, and this
1639    /// pins the behaviour so it cannot change silently.
1640    #[tokio::test]
1641    async fn the_kind_filter_selects_on_the_stored_kind_and_the_chain_runs_after() {
1642        /// Renames `old_spent` to the kind the kernel knows today.
1643        struct RenameSpent;
1644        impl Upcaster for RenameSpent {
1645            fn upcast(&self, mut event: Value) -> Value {
1646                let Some(map) = event.as_object_mut() else {
1647                    return event;
1648                };
1649                if map.get(FIELD_KIND).and_then(Value::as_str) == Some("old_spent") {
1650                    map.insert(FIELD_KIND.to_string(), Value::from(KIND_BUDGET_SPENT));
1651                }
1652                event
1653            }
1654        }
1655
1656        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(RenameSpent)];
1657        let mut store = CurrentStore::new(Box::new(MemEventStore::new()), chain);
1658        store
1659            .append(obj(
1660                json!({ "kind": KIND_BUDGET_GRANTED, "data": { "amount": 100 } }),
1661            ))
1662            .await
1663            .expect("the grant");
1664        store
1665            .append(obj(
1666                json!({ "kind": "old_spent", "data": { "amount": 10 } }),
1667            ))
1668            .await
1669            .expect("a settlement under the older name");
1670
1671        // Asked for by the name it is stored under, it comes back projected.
1672        let renamed = store
1673            .read_kinds(Some(&["old_spent"]), 0, usize::MAX)
1674            .await
1675            .expect("read_kinds");
1676        assert_eq!(renamed.len(), 1);
1677        assert_eq!(
1678            renamed[0].kind(),
1679            KIND_BUDGET_SPENT,
1680            "the chain ran after the selection"
1681        );
1682
1683        // Asked for by the name it reads *as*, it is not selected at all.
1684        assert!(
1685            store
1686                .read_kinds(Some(&[KIND_BUDGET_SPENT]), 0, usize::MAX)
1687                .await
1688                .expect("read_kinds")
1689                .is_empty(),
1690            "the filter cannot see a kind the chain has not produced yet"
1691        );
1692
1693        // The decision is handed the projected events either way.
1694        let seen: Arc<Mutex<Vec<String>>> = Arc::default();
1695        let recorded = Arc::clone(&seen);
1696        store
1697            .append_if(
1698                None,
1699                decide_current(move |events| {
1700                    *recorded.lock().expect("not poisoned") =
1701                        events.iter().map(|e| e.kind().to_string()).collect();
1702                    None
1703                }),
1704            )
1705            .await
1706            .expect("append_if");
1707        assert_eq!(
1708            *seen.lock().expect("not poisoned"),
1709            [KIND_BUDGET_GRANTED, KIND_BUDGET_SPENT]
1710        );
1711    }
1712
1713    /// A test-local `1 → 2` step, standing in for a real one: it renames a
1714    /// kind and marks the projection with the version it produced.  The
1715    /// kernel chain is empty until the first release, so the mechanism is
1716    /// exercised with a chain the tests own.
1717    struct RenameOldKind;
1718
1719    impl Upcaster for RenameOldKind {
1720        fn upcast(&self, mut event: Value) -> Value {
1721            // Already at the newer shape — or not an object at all — so
1722            // there is nothing to do.  An upcaster is infallible: an event
1723            // it does not recognise comes back exactly as it went in.
1724            let version = event
1725                .get(SCHEMA_VERSION_FIELD)
1726                .and_then(Value::as_u64)
1727                .unwrap_or(1);
1728            if version >= 2 {
1729                return event;
1730            }
1731            let Some(map) = event.as_object_mut() else {
1732                return event;
1733            };
1734            if map.get(FIELD_KIND).and_then(Value::as_str) == Some("old_kind") {
1735                map.insert(FIELD_KIND.to_string(), Value::from("new_kind"));
1736            }
1737            map.insert(SCHEMA_VERSION_FIELD.to_string(), Value::from(2_u64));
1738            event
1739        }
1740    }
1741
1742    /// The chain a session reads through is empty until the first release,
1743    /// so a stored event reads back exactly as it was written.
1744    #[test]
1745    fn the_kernel_chain_is_empty_and_the_current_version_is_one() {
1746        assert_eq!(CURRENT_SCHEMA_VERSION, 1);
1747        assert!(
1748            kernel_upcasters().is_empty(),
1749            "no shape has been released, so no step is owed"
1750        );
1751
1752        let stored = json!({ "kind": "note", "seq": 1, SCHEMA_VERSION_FIELD: 1 });
1753        assert_eq!(
1754            apply_upcasters(&kernel_upcasters(), vec![stored.clone()]),
1755            vec![stored],
1756            "an empty chain reads the log back verbatim"
1757        );
1758    }
1759
1760    /// A stored event carries the version it was written under.
1761    #[tokio::test]
1762    async fn new_events_are_stamped_with_the_current_version() {
1763        let mut store = MemEventStore::new();
1764        store.append(ev(1)).await.expect("append");
1765        let stored = store.read(0, usize::MAX).await.expect("read");
1766        assert_eq!(
1767            stored[0].get(SCHEMA_VERSION_FIELD).and_then(Value::as_u64),
1768            Some(CURRENT_SCHEMA_VERSION),
1769            "{}",
1770            stored[0]
1771        );
1772    }
1773
1774    /// An upcaster is total: an event already at the version it produces, and
1775    /// a value it does not recognise at all, both come back unchanged rather
1776    /// than failing or being guessed at.
1777    #[test]
1778    fn an_upcaster_leaves_a_current_or_unrecognised_event_unchanged() {
1779        let chain: Vec<Arc<dyn Upcaster>> = vec![Arc::new(RenameOldKind)];
1780
1781        // Already at the newer shape: untouched, kind included.
1782        let current = json!({ "kind": "old_kind", "seq": 1, SCHEMA_VERSION_FIELD: 2 });
1783        assert_eq!(
1784            apply_upcasters(&chain, vec![current.clone()]),
1785            vec![current],
1786            "an event at the version the step produces is not stepped again"
1787        );
1788
1789        // Not an object, and not a kind the step knows: neither panics, and
1790        // neither is invented into something else.
1791        let out = apply_upcasters(&chain, vec![json!(42), json!({ "kind": "note", "seq": 1 })]);
1792        assert_eq!(out[0], json!(42), "a non-object passes straight through");
1793        assert_eq!(kind_of(&out[1]), "note", "an unknown kind keeps its name");
1794    }
1795}