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:
.firsthosts via Loom.host (i.e.weave(Rendezvous.New(pattern)))..secondjoins 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.
That difference is load-bearing, not incidental, so it is declared — via joinerRosterOrigin (#2591). When both ends read one backend's roster, the joiner is holding the host's id because the fixture put it there, and every joiner-side obligation is satisfied by construction; the arm a harness names is what tells a reader whether a green meant anything. See JoinerRosterOrigin.
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:
everyFalseCapabilityDeclaresAGap fails the suite if any
falseflag has no URL.Task 1.8's rendered capability matrix surfaces the declared gaps.
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, self-send refusal, 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:
incomingCompletesWhenSeamCloses ↔ SeamCapabilities.terminatesIncomingOnClose
stateStaysTornAfterClose ↔ SeamCapabilities.staysTornAfterClose
peersCollapseToSelfIdWhenTorn ↔ SeamCapabilities.collapsesPeersOnTear
survivorStopsAdvertisingADepartedPeer ↔ SeamCapabilities.reportsPeerLoss
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: every harness declares an ObligationDeclaration via midSessionDeathDeclaration, enforced by midSessionDeathDeclarationIsHonest. 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 *Declaration() and *DeclarationIsHonest meta-test rather than a required abstract every fabric would have to implement.
Those declarations were a String? until #2568 — a tracking URL or null — which had two states for three situations and no way at all to say this obligation does not apply to this fabric, by design. Nine of the sixteen harnesses tracked under the mid-session-death umbrella were exactly that, and the framing invited a contributor burning the list down to "fix" kuilt-nw by re-introducing tear-on-peer-loss (undoing #1513, breaking redial). ObligationDeclaration adds the two by-design arms — and, because an "I cannot reach this state" opt-out otherwise just moves the vacuity one level up, checks them against the injection hook rather than believing them. Its KDoc carries the arm-by-arm contract and what each arm cannot detect.
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.
Functions
This fabric's declared behaviour against the seam contract.
Issue-tracking URL for every false flag in capabilities, keyed by the capability's canonical name (see SeamCapabilities.falseFlags).
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.
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.
Inject a self-dial at BOTH ends: make host and joiner each 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. Each seam's self-connection guard MUST drop its own. Return true only if the harness injected at both ends; false (the default) means "this harness cannot inject a self-dial", and selfDialIsRejected early-returns without asserting.
Where this harness's joiner gets the remote peer in its Seam.peers — i.e. whether the joiner half of peersReportsSelfIdAndAtLeastTwoAfterJoin can fail here at all.
How many frames this harness's own join path has already delivered into the HOST's incoming by the time a test body starts. Zero for every harness whose join() moves no application frames; the joiner→host delivery obligations (#2601) drop exactly this many before reading what the test itself sent.
What this harness says about the membership-drain obligation — the accountability analog of midSessionDeathDeclaration for the injectMembershipDrain hook.
What this harness says about the mid-session-death obligation — the accountability analog of capabilityGaps for the injectMidSessionDeath hook, and since #2568 a four-armed ObligationDeclaration rather than a two-state String?.
Provide a fresh host/joiner Loom pair per test.
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.
Tracking URL for why this fabric names no frame ceiling — the accountability analog of midSessionDeathDeclaration for us.tractat.kuilt.core.Seam.maxPayloadBytes (#2069).
A payload of exactly Seam.maxPayloadBytes crosses. Sent with broadcast so the obligation does not also depend on SeamCapabilities.supportsSendTo.
What this harness says about the self-dial obligation — the accountability analog of midSessionDeathDeclaration for the injectSelfDial hook.