SeamConformanceSuite

abstract class SeamConformanceSuite

Reusable contract test suite for Loom implementations.

Subclass and implement newLoomPair to bind any fabric under test. Every Test in this class encodes a required invariant of the seam contract — a conforming implementation must pass all of them.

Lives in commonMain of :kuilt-conformance (not a module's commonTest) so every fabric adapter can subclass it from its own test source set — realising the "one conformance suite, every fabric passes it" invariant.

Provide a fresh host/joiner pair per test via newLoomPair:

  • .first hosts via Loom.host (i.e. weave(Rendezvous.New(pattern))).

  • .second joins via Loom.join with joinTag (i.e. weave(Rendezvous.Existing(joinTag()))).

In-process radio fabrics return the same instance twice: (loom, loom). Role-split fabrics (websocket, mdns, webrtc, multipeer) return distinct host/joiner Looms wired to reach each other.

Capabilities & gaps

Not every fabric can honor every corner of the contract — a browser WebRTC data channel cannot throw synchronously on a torn send, a relay-only fabric never delivers peer-to-peer. Rather than let each fabric carry bespoke @Ignore overrides, every subclass declares one SeamCapabilities value via capabilities and, for every false flag, an issue-tracking URL via capabilityGaps.

There is no skip API in common kotlin-test — an obligation that early-returns reports PASS, which is worse than a JVM-visible @Ignore. So a gap is not made loud by skipping. Two things make it loud instead:

The core obligations (host yields a usable seam, broadcast delivery, order, peers≥2, close-idempotency, availability, both Woven-state invariants, close→Torn, absent-peer throw, close-does-not-mint-a-cancellation) are ungated — no capability flag can suppress them. That structural guarantee is pinned by SeamConformanceUngatedCoreTest, which drives the core obligations through a harness whose capabilities would betray any read.

wovenSeamCapabilityIsHonest is a flag-selected obligation — a third kind. It reads SeamCapabilities.reportsLiveCapability (so it is not core) but never early-returns: the flag picks which assertion applies, so no capability value can make it vacuous.

Only the capability-specific obligations gate in-body on their own flag:

Every flag on SeamCapabilities now appears in that list or is a selector (SeamCapabilities.reportsLiveCapability, above) — with one deliberate exception, SeamCapabilities.securesTransport, which is a standing declaration no property can read because the suite has no wire tap. Its own KDoc argues that and names what holds a fabric to it instead. Keeping the set closed is the point: publishing a capability value subscribes a fabric to every case selected on it, so a flag with zero cases charges the everyFalseCapabilityDeclaresAGap toll, delivers no coverage, and invites a reader auditing the matrix to infer coverage that does not exist. That is what #2304 found on three flags, one of which (SeamCapabilities no longer declares it) was actively contradicted by ungated core.

There is a third gating mechanism alongside core (ungated) and capability-gated: harness-hook-gated. incomingCompletesOnInjectedMidSessionDeath runs only when a harness overrides injectMidSessionDeath to actually drop the transport — a capability of the harness, not the fabric. Its silent-skip is made accountable exactly as a capability gap is: an un-overridden harness must declare a tracking URL via midSessionDeathGap, enforced by midSessionDeathObligationIsTrackedWhenUnproven. The same shape governs injectMembershipDrain (a peer leaving without a tear) and injectSelfDial (a peer dialling its own advertisement, the #1466 class) — each opt-in, each tracked-by-default via its own *Gap() and *IsTrackedWhenUnproven meta-test rather than a required abstract every fabric would have to implement.

A fourth selects on a value the fabric already publishes rather than on any declaration: payloadOfExactlyTheBudgetIsCarried and overBudgetAddressedSendIsRefusedNotLeaked run exactly when Seam.maxPayloadBytes is non-null, because a fabric reporting null has made no promise to keep. That is the one gating input a fabric cannot get wrong by declaring wrong — but it can still stay silent while enforcing a ceiling internally, so the same tracked-by-default umbrella applies (payloadBudgetGap / payloadBudgetObligationIsTrackedWhenUnpublished, #2069). Unusually, that pairing binds in both directions: publishing a budget requires the gap to be cleared, so the declaration cannot be left behind as an opt-out.

Continuous contract monitor

connectedPair launches a background collector that asserts selfId ∈ peers on a live (not SeamState.Torn) seam for the whole test — every obligation test is thereby also a monitor. peersReportsSelfIdAndAtLeastTwoAfterJoin only samples the invariant once at the end of a join; the monitor watches it across the whole test. Honest limit: peers is a kotlinx.coroutines.flow.StateFlow, so the collector observes the latest value at each resumption, not every intermediate write — a persistent selfId ∉ peers on a live seam (the #1466 failure — a survivor's roster collapsing to {theOtherPeer} while it stays Woven) is reliably caught, but a purely transient sub-scheduling drop that is overwritten before the collector resumes may be missed. There is no stronger primitive against a StateFlow; the monitor raises the floor from "sampled once" to "sampled continuously".

This suite is deliberately fixed at two Looms (ADR-001) and has no positive N-peer/mesh obligation; roster convergence, sender-attributed broadcast, directed routing, peer-leave, and dial dedup across three or more peers are covered by the sibling MeshConformanceSuite, which every SeamCapabilities.meshDelivery fabric supporting ≥3 peers must also subclass.

Weaving timing invariant

The invariant "a frame sent while SeamState.Weaving is not silently dropped" is not asserted in this suite because all current harnesses produce instant-SeamState.Woven seams: relay fabrics (WebSocket, InMemory) weave at construction, and the Multipeer fake fires its peer-connected callback synchronously during weave(), so no harness actually starts SeamState.Weaving by the time newLoomPair returns. Asserting a Weaving precondition here would produce a vacuously-passing test on every fabric.

The enforcement point for this invariant is DelayedWovenLoomTest, which uses DelayedWovenLoom — a test-only harness that holds the seam in SeamState.Weaving until DelayedWovenSeam.markWoven is called explicitly — to reproduce the radio-fabric timing window deterministically. Radio fabric conformance harnesses that fire their connected event asynchronously should run their own equivalent of DelayedWovenLoomTest to confirm frames are not dropped in the window.

Constructors

Link copied to clipboard
constructor()

Functions

Link copied to clipboard

This fabric's declared behaviour against the seam contract.

Link copied to clipboard
abstract fun capabilityGaps(): Map<String, String>

Issue-tracking URL for every false flag in capabilities, keyed by the capability's canonical name (see SeamCapabilities.falseFlags).

Link copied to clipboard
fun closeDrivesStateTornNormal(): TestResult
Link copied to clipboard
fun closeIsIdempotent(): TestResult
Link copied to clipboard
fun hostStateIsWovenEvenAlone(): TestResult
Link copied to clipboard
Link copied to clipboard
open suspend fun injectMembershipDrain(host: Seam, joiner: Seam): Boolean

Inject a mid-session membership drain: drop joiner from host's peer set mid-session without tearing host's seam — host.peers shrinks while host.state stays SeamState.Woven. Return true if the harness performed the injection; false (the default) means "this harness cannot inject a drain", and peersDrainWithoutTearOnInjectedMembershipDrain early-returns without asserting.

Link copied to clipboard
open suspend fun injectMidSessionDeath(host: Seam, joiner: Seam): Boolean

Inject a mid-session transport death under both host and joiner — the way a real fabric dies when the underlying connection drops rather than being closed by the application. Return true if the harness performed the injection; false (the default) means "this harness cannot inject death", and incomingCompletesOnInjectedMidSessionDeath early-returns without asserting.

Link copied to clipboard
open suspend fun injectSelfDial(host: Seam): Boolean

Inject a self-dial: make host resolve a connection whose remote identity is its OWN Seam.selfId — the #1466 class. A symmetric advertise+browse fabric is delivered its own advertisement (real Bonjour/mDNS/NWBrowser returns a device's own service to its own browser), dials it, and the resulting connection resolves to selfId. The seam's self-connection guard MUST drop it. Return true if the harness performed the injection; false (the default) means "this harness cannot inject a self-dial", and selfDialIsRejected early-returns without asserting.

Link copied to clipboard
open fun joinTag(): Tag

The advertisement the joiner uses. Defaults to the in-memory tag.

Link copied to clipboard

Tracking URL for why this harness does not prove the membership-drain obligation — the accountability analog of midSessionDeathGap for the injectMembershipDrain hook.

Link copied to clipboard

Tracking URL for why this harness does not prove the mid-session-death obligation — the accountability analog of capabilityGaps for the injectMidSessionDeath hook.

Link copied to clipboard
abstract fun newLoomPair(): Pair<Loom, Loom>

Provide a fresh host/joiner Loom pair per test.

open fun newLoomPair(testScope: TestScope): Pair<Loom, Loom>

Scope-aware variant used by every runTest-based test below. Stateless fabrics ignore testScope — the default delegates to the no-arg newLoomPair.

A payload one byte over the budget is refused by the seam with us.tractat.kuilt.core.PayloadTooLarge, not leaked as the fabric's own frame error.

Link copied to clipboard

Tracking URL for why this fabric names no frame ceiling — the accountability analog of midSessionDeathGap for us.tractat.kuilt.core.Seam.maxPayloadBytes (#2069).

Link copied to clipboard

A payload of exactly Seam.maxPayloadBytes crosses. Sent with broadcast so the obligation does not also depend on SeamCapabilities.supportsSendTo.

Link copied to clipboard
Link copied to clipboard
open fun selfDialGap(): String?

Tracking URL for why this harness does not prove the self-dial obligation — the accountability analog of midSessionDeathGap for the injectSelfDial hook.

Link copied to clipboard
fun selfDialIsRejected(): TestResult
Link copied to clipboard
fun sendOnTornSeamThrows(): TestResult
Link copied to clipboard
fun sendToDeliversToNamedPeer(): TestResult
Link copied to clipboard
Link copied to clipboard
fun stateIsWovenAfterConnect(): TestResult
Link copied to clipboard
fun stateStaysTornAfterClose(): TestResult
Link copied to clipboard
fun wovenSeamCapabilityIsHonest(): TestResult