Skip to content

Add a behavioural conformance suite for MemoryProvider drivers (#18 §E1) - #20

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/18-e1-conformance-suite
Aug 17, 2026
Merged

Add a behavioural conformance suite for MemoryProvider drivers (#18 §E1)#20
senamakel merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/18-e1-conformance-suite

Conversation

@YellowSnnowmann

Copy link
Copy Markdown
Contributor

Stacked on #19. This branch is built on the housekeeping branch, so the diff here also contains #19's three commits. Review #19 first; once it merges, this diff reduces to the conformance work alone. (GitHub cannot take a cross-fork base branch, so the base is main rather than the housekeeping branch.)

The conformance work is the single commit Add a behavioural conformance suite for MemoryProvider drivers.

Summary

Step 2 of the sequencing in #18 — §E1, the driver conformance suite. Additive only: no existing crate changes behaviour.

audit_provider checks that a driver's advertised capabilities match its reachable accessors. That is a structural check — it proves the shape is honest, not that the behaviour is. Nothing checked that two drivers answer the same question the same way, which is the claim "a second engine binds in its place without the host learning anything new" actually rests on.

tinymemory-conformance is that check. assert_provider takes any bound Arc<dyn MemoryProvider> and drives the contract:

  • the mandatory three families — store / get / forget / list / namespaces
  • upsert semantics on (namespace, key) — a re-store replaces, never duplicates
  • namespace isolation, in both list and get
  • provenance preservationExternalSync survives store → get and export → import
  • recall honours its limit and does not cross a namespace
  • export pagination, and a cursor that terminates on None rather than on an empty page
  • an unrecognised cursor is refused rather than silently restarting the export
  • unicode, empty, 64 KiB, and control-character content round-trip
  • audit_provider reports nothing advertised-but-unreachable

Each sub-assertion is also public, so a driver mid-implementation can run the parts it claims and get a useful failure rather than an unrelated one.

Dependencies

tinymemory-api, async-trait, serde_json, anyhow. Deliberately nothing else: a suite that pulled in an engine could not prove interchangeability, having already chosen one — and reaching tinymemory-core would drag in a bundled SQLite and the embedded engine besides (§D).

The reference driver

InMemoryProvider, for two reasons.

A suite that only ever ran against real engines cannot distinguish "the engine is wrong" from "the assertion is wrong". Running it against a driver whose behaviour is obvious by inspection separates those.

It also documents the contract by example — the (namespace, key) upsert, taint persisted verbatim, the cursor that terminates on None. It advertises exactly the mandatory three and leaves every optional accessor at None, which is what makes its own audit pass.

One design note worth review

Writing this surfaced a distinction the contract permits but does not name: a driver may accept writes and discard them. NullMemoryProvider is exactly that, and the storage assertions are vacuous for it.

The tempting fix is to weaken each assertion to tolerate an empty read. That is wrong — it would let a driver that intends to retain and silently does not pass the whole suite, which is the failure mode most worth catching. Instead the suite probes retention once (retains_writes) and skips only the storage half, never the contract-shape half. The probe is deliberately not a flag the caller passes, for the same reason.

If you would rather null were simply excluded from the suite, say so and I will invert it — but I think a contract suite that cannot run against the contract's own reference driver is asserting storage rather than the contract.

Validation

All four contract commands from AGENTS.md, from the repository root:

Command Result
cargo fmt --all -- --check pass
cargo clippy --all-targets --all-features -- -D warnings pass
cargo build --all-targets --all-features pass
cargo test --all-features pass — 1036 passed, 0 failed, 3 ignored

The suite itself: 3 integration tests + 1 doctest, green against both InMemoryProvider and NullMemoryProvider.

Not in this PR

  • Running it against the TinyCortex adapter and the three remote adapters. Those are the run targets §E1 names, and they need the integration/remote-engines/ Docker harness wired into CI (§E2). Worth its own PR so a live-engine failure is not confused with a suite bug.
  • Everything in §A–§D. §E1 exists to be the safety net for those, which is why it lands first.

Related

Part of #18. This is also the item blocking OPENCOMPANY_MEMORY=remote in tinyhumansai/opencompany#914 — routing a tenant's memory at an adapter with one happy-path test each is what this suite exists to stop.

`rust_out` and `tmp` are 3.6 MB Linux x86-64 ELF executables with debug info,
committed into a repository developed on macOS. Nothing references either name
in source, config, CI, or docs; both are the output of an ad-hoc build that was
staged by accident, and every clone has carried 7.3 MB of them since.

Ignored with anchored paths so a legitimate `tmp` directory inside a crate is
unaffected.
The workspace declared three different answers: 1.96 at the root and for the
module crate, 1.85 for `core` and both adapters, and nothing at all for `api`.
The low ones were not true — the root facade requires 1.96 and depends on all of
them, so nothing in this workspace builds on 1.85, and no CI job checked the
claim.

Aligning on 1.96 immediately surfaced three lints that the understated MSRV had
been suppressing: clippy only suggests `is_multiple_of` when the declared
minimum supports it. Those are fixed here rather than allowed, since they are a
consequence of this change and not separable from it.

`api/` gaining an explicit MSRV is worth noting for downstream: it is the
dependency-light crate an embedding host binds directly, so it now states a
floor rather than leaving consumers to infer one.
The layout section omitted `tinymemory-core` entirely — the largest crate in the
repository, and the one a real host actually depends on. It also omitted
`crates/tinymemory-module`.

Adds both, and states plainly that `core` is not dependency-light the way `api`
is, since that asymmetry is the thing a reader most needs to know before
choosing which to depend on.

Also documents `git submodule update --init --recursive`. Nothing builds without
it, and because `core` names its engines by path through `vendor/`, an
uninitialized checkout fails at manifest resolution with an error that does not
mention submodules.
`audit_provider` checks that a driver's advertised capabilities match its
reachable accessors — a structural check, proving the shape is honest, not the
behaviour. Nothing checked that two drivers answer the same question the same
way, which is the claim "swap the engine without the host learning anything
new" actually rests on.

`tinymemory-conformance` is that check. `assert_provider` takes any bound
driver and drives the contract: the mandatory three families, upsert on
`(namespace, key)`, namespace isolation, provenance preservation, recall limits
and scoping, export pagination, import round-tripping, and unicode / empty /
64 KiB / control-character content.

It depends on `tinymemory-api` and nothing else of substance. A suite that
pulled in an engine could not prove interchangeability, having already chosen
one; reaching `tinymemory-core` would drag in a bundled SQLite besides.

Ships an in-memory reference driver, for two reasons. A suite that only ever
ran against real engines cannot distinguish "the engine is wrong" from "the
assertion is wrong", and a driver whose behaviour is obvious by inspection
separates those. It also documents the contract by example — the upsert, the
verbatim taint, the cursor that terminates on `None` rather than on an empty
page.

Writing it immediately found a distinction worth naming. The contract permits a
driver that accepts writes and discards them; `NullMemoryProvider` is exactly
that, and the storage assertions are vacuous for it. Rather than weakening each
assertion to tolerate an empty read — which would let a driver that *intends* to
retain and silently does not slip through — the suite probes retention once and
reports which half it ran. The contract-shape assertions are never skipped.

Provenance is the sharp assertion. A driver reading back `Internal` for content
stored as `ExternalSync` has laundered external content into internal-trust
content, and every downstream gate keyed on taint is then silently wrong.

Refs tinyhumansai#18 (§E1)
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b7afec03-9a4f-49ac-b3dd-9a817c374c11


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 17, 2026

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out · 718 embedded · openrouter/openai/text-embedding-3-small

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed this PR's own commit (59178d2). The crate is well-placed — workspace and default member, no required-features, no cfg gates, so a bare cargo test runs it. The manifest's refusal to depend on tinymemory-core is the right call and the comment explaining it is worth keeping: a suite that linked an engine could not prove interchangeability.

Four findings, the first significant.

1. The retention probe swallows the exact failure it claims to catch

if !retains_writes(p).await {
    return;   // skips all seven storage assertions
}

The doc comment argues:

a driver that intends to retain and silently does not is the failure mode worth catching, and a caller-supplied flag would let it through

I think that is inverted. With the probe as written, a driver that intends to retain and silently does not returns false, assert_provider returns early, and the suite reports success — seven assertions skipped, nothing printed. A caller-supplied expectation is precisely what would catch it: the caller declares "TinyCortex retains", the probe disagrees, the test fails.

This matters most for the drivers §E1 actually targets. Run the TinyCortex adapter or Mem0 through this suite with a broken write path and it passes green.

Suggestion that keeps the property you want (don't trust the caller) while closing the hole — have the caller declare, and fail on disagreement rather than branching on the probe alone:

pub enum Retention { Required, Discards }

pub async fn assert_provider(p: Arc<dyn MemoryProvider>, expect: Retention) {
    // ... contract-shape assertions, unchanged ...
    let retains = retains_writes(&*p).await;
    match (expect, retains) {
        (Retention::Required, false) => panic!("{}: declared retaining, discarded a write", p.driver_id()),
        (Retention::Discards, true)  => panic!("{}: declared /dev/null, retained a write", p.driver_id()),
        (Retention::Discards, false) => return,
        (Retention::Required, true)  => {}
    }
    // ... storage assertions ...
}

The probe still runs, so a lying caller is still caught — but silence is no longer a passing outcome.

2. Nothing pins that the reference driver takes the retaining branch

retains_writes is called in exactly one place and asserted nowhere. If InMemoryProvider regressed, the_in_memory_reference_driver_conforms would pass vacuously too — so the calibration subject, whose whole job is to tell "assertion is wrong" from "driver is wrong", cannot currently detect its own breakage. One line fixes it:

assert!(retains_writes(&InMemoryProvider::new()).await, "the reference driver must retain");

3. §E1's Unsupported requirement is not implemented

§E1 asks that "every unadvertised family returns MemoryError::Unsupported, never Ok." There are zero occurrences of Unsupported in conformance/. What is here is assert_capability_audit (advertised == reachable) plus the accessor is_none() checks in the test target — that is §E1's audit bullet, which is a different requirement. Worth either implementing or calling out as deferred, so the §E1 checkbox is not read as complete.

4. A soft if let makes the awkward-content assertion skippable

if let Some(got) = provider.get(&ns, key).await... {
    assert_eq!(&got.content, content, "{who}: `{key}` content was mangled");
}

A driver that retains generally but drops specifically the unicode / empty / 64 KiB / NUL-byte cases returns None, the assert_eq! never runs, and the case passes. The 64 KiB large case is exactly where a real backend silently truncates or rejects. Since this assertion only runs after the driver has already been shown to retain, None here is a defect by definition and should be else { panic!(...) }.

Scope note (not a defect in this PR)

§E1 asks the suite be run against the TinyCortex adapter and the three remote adapters too; only Null and InMemory are wired. I can see why — the adapter needs the host's process-global seams, as #22's engine/test.rs explains — but the §E1 checkbox is not fully met by the stack as it stands, and it would be good to say where that leg joins.

The structure here is good and the two-reference-driver rationale (calibration subject + opposite end) is a genuinely nice design. Finding 1 is the one I would want addressed before this merges, since everything later in the stack is measured against this net.

@senamakel
senamakel merged commit 4549cda into tinyhumansai:main Aug 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants