Commit graph CultLib/packages/cultnet-rs/tests
Author SHA1 Message Date
Meta Vens
6bb41f39ea Give every cultcache-rs entry type one home store
Cut 5 (Rust). The four mirror loops in put, put_envelope, put_raw_envelope and
delete are deleted, and so is put_prepared_batch's route.len() != 1 branch.
resolve_route_index returns the single home store (the store that claims the
type, else the generic store) and replaces resolve_route_indices.

add_backing_store and add_generic_backing_store return Result and refuse a
second generic store or a type already claimed by another store, attaching
nothing when they refuse. Callers take the Result: the crate's tests, the
cultcache_interop example, cultnet-rs's tests and interop peer example, and
CultMeshNode::create_node in cultmesh-rs.

Tests: second_generic_store_is_rejected, type_claimed_twice_is_rejected;
type_specific_store_routes_before_generic_store stays, now attaching the routed
store before the generic one. No bytes change.

The store-composition contract's runtime table now records Rust, TypeScript
and Python as conforming to one home store per type, and the README routing
section drops its non-conformance note.

Version 0.1.0 -> 0.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:38:41 +02:00
Meta Vens
47b9fc4ee3 A reader returns "nothing" only when nothing is waiting
Both RUDP socket readers returned None after consuming a packet that carried
no frame or event for the caller: an acknowledgement, a ping, a fragment
that completed nothing yet. A caller that polls once per send therefore read
one incoming datagram per outgoing one, and fell behind its peer's
acknowledgements as soon as a reliable channel shared the session; every
unacknowledged packet was then resent, and the resends starved the sends.
Measured on the media path 2026-09-10: 7 video frames in 9 s with reliable
audio on, 266 without. Readers now consume no-event packets and keep
reading; None means the socket is empty.

The client no longer acknowledges unreliable frames. They carry no sequence,
so the ACK told the sender nothing and cost it one datagram to read for
every one it sent.

The fragment-set bound test now asserts eviction, which is what the bound
has done since the fuse was cut earlier today; the old assertion was hiding
behind a truncated test run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 01:21:37 +02:00
Meta Vens
c84cb2e862 The consumer dials the producer
A media stream advertisement now carries `media_endpoint`, where the
producer serves it from; the request no longer carries a consumer endpoint.
The producer listens and the consumer connects, the same way as every other
client/server pair on the mesh, so a viewer's host admits nothing inbound
and needs no firewall rule or port forward. Unshipped contract, keys
renumbered in place.

Audio gets its own channel ("audio": reliable, unordered, media expiry) so
one session can carry lossy video under parity and reliable audio together,
which is what lets the producer serve both to one dialled-in consumer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 22:50:20 +02:00
Meta Vens
d3bc6e6604 Keep receiving when fragment sets go stale
A fragment set that lost one fragment was never reclaimed: the buffer map
only shrank on successful reassembly or a payload-size violation. Once 64
sets were stranded, every new fragmented payload returned an error and the
receiver's loop terminated. Any nonzero loss on any fragmenting channel was
therefore a countdown, not a threshold: 3 fragments at 3% loss killed the
receiver at ~700 payloads, 1% at ~2,100. Found by the media load test.

When the bound is hit, evict the set untouched longest and count it via
fragment_sets_evicted(). A stranded set's siblings were delivered long ago;
dropping it is the right call and it was already lost.

Also stop saturating the fragment id. saturating_add parked every set after
the 65,535th on one shared id, and the reassembler stitched them together.
That is the July 2026 eight-minute failure; the id now wraps to 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 11:59:48 +02:00
Meta Vens
05e0925143 Merge the media stream contract into CultLib 2026-09-10 02:32:21 +02:00
Meta Vens
a58968e7ac Bring the media delivery choice alongside the contract
Both branches are the same body of work seen from two angles: what a media
stream is, and what its channel does. Muninn needs both to build, so they merge
here rather than making a consumer track two refs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 02:07:13 +02:00
Meta Vens
a3e6877945 Move the media wire envelope where both ends can reach it
A media record travels wrapped in cultnet.document_put_raw.v0 with a schema id,
a record key and provenance. Both ends have to agree on that exactly, and until
now only Muninn had it: the producer was Rust in one repository and the consumer
was 6,515 lines of C++ in another, each with its own reading of the same bytes.
Ratatoskr could not decode a real stream without it.

Two things change in the move rather than after it.

The producer namespace is now the caller's. The Muninn implementation hardcoded
"muninn-media:" into every message id and "muninn.media" into every tag, so a
second producer would have had to label its traffic as Muninn's to be understood.
`MediaWireProvenance` carries it instead, and a test asserts the envelope names
no producer the caller did not supply.

The four hand-written wire payload structs are gone. Each restated a record's
entire field list as a positional tuple, in order, kept in step by discipline
alone — and existed for one reason: `#[serde(with = "serde_bytes")]` on the
payload, which cannot be applied to a DatabaseEntry struct because the derive
does not read serde attributes. Now that the records carry
`#[cultcache(key = N, bytes)]` the derive emits bin itself, so the records
serialize directly.

That last part is a wire claim, so it is tested rather than asserted:
`the_derive_reproduces_the_hand_written_wire_payload_byte_for_byte` reconstructs
the legacy struct and compares output byte for byte. It matches, which makes the
deletion a simplification instead of a silent break.

Record key shapes move here too. A consumer parses them to tell a chunk from its
siblings and a retransmission from a new frame, so they are contract and belong
beside the records rather than in whichever producer wrote them first.

Muninn's copies are not removed yet; that is the other half of this move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 02:00:19 +02:00
Meta Vens
36f31920ce Let a caller choose what the media channel does
`media` was hardcoded Reliable in two places — the profile builder and the
per-channel send options — so no caller could ask for anything else.

Measured on Raven through `cultnet-impair`, 848-byte packets at 12 Mbps, which
is Muninn's real media shape:

    profile    mode          delivered    ack bytes
    clean      unreliable      100.00%           43
    clean      reliable         69.10%      131,322
    loss 1%    unreliable       98.95%           43
    loss 1%    reliable         72.00%      135,837
    loss 3%    unreliable       96.73%           43
    loss 3%    reliable         73.30%      138,030

A reliable media channel loses 31% of a clean 12 Mbps offer, and its delivery
barely moves as loss rises, so loss is not the binding constraint — it cannot
carry the rate. An unreliable one holds everything on a clean link and then
degrades by about the loss injected. The reverse channel differs by three orders
of magnitude.

Muninn wanted the unreliable lane and had no way to say so. It signalled the
intent the only way the API allowed, by passing no reliable expiry, and its own
test has asserted Unreliable and failed since it was written. With this change
it passes.

Delivery now comes from the advertised profile, resolved by `channel_delivery`
alongside the `channel_reliable_expire_after_ms` that already worked this way.
That removes the send path's private opinion about reliability, so the profile a
peer was shown and the behaviour it gets cannot disagree. The channels whose
semantics are fixed by design — schema reliable and ordered, realtime unreliable
— are untouched and pinned by a test.

`media_delivery: None` means Reliable, so no existing consumer changes
behaviour. It is still source-breaking for anyone constructing the three options
types with a struct literal rather than `::client(...)`: two sites in
cultmesh-rs, four in Muninn, seven across the test suites here, all mechanical.
An earlier draft claimed Muninn was unaffected; the compiler disagreed.

The `examples/media_delivery_probe.rs` added here is what produced the table, so
the claim can be re-run rather than believed. Note its sender-side wire-cost
figure is unsound for the reliable case: the sender stops draining after five
seconds while retransmission is still in flight, so those byte counts
under-report. The delivery percentages are receiver-measured and stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 01:08:31 +02:00
Meta Vens
9e6027e8d8 Measure what adding a bytes slot does to records already written
Four of cultnet-rs's ten DatabaseEntry byte fields lack
#[cultcache(key = N, bytes)] and so serialize as MessagePack
array-of-integers rather than bin: IdunnDeploymentBrakeRecord.signature,
IdunnDaemonHealthTrustBindingRecord.signer_public_key,
GameCultServiceTrustAnchorRecord.signer_public_key, and
IdunnAuthenticatedProviderHealthProjectionRecord.signature. Converting
them touches live stores, so the question is what breaks.

Measured rather than reasoned, and the answer is the opposite of what I
expected. Reading is compatible in both directions: serde_bytes::ByteBuf
accepts an array marker, and a plain Vec<u8> accepts bin, so the flip is
neither a hard read break nor a one-way door. No dual-read path is
needed and reverting the binary is survivable.

What does change is reproduction. A record written before the flip can
be read but can never re-encode to the bytes it was stored as, and its
canonical digest moves with it. That is the cost, and it lands on
byte-exact store checks and canonical_sha256, not on readability -- so
the migration is a re-encode question, not a compatibility one.

Also pins the size difference the media work is chasing: 406 bytes as an
array against 278 as bin for a 256-byte payload, 1.46x, independently
matching the 1.45x measured on an 848-byte payload.

  cargo test --test byte_slot_migration -> test result: ok. 4 passed; 0 failed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 00:42:26 +02:00
Meta Vens
2ba404cc0b Carry media payloads as bin
The three media payload fields take the derive's `bytes` attribute, so they
serialize as MessagePack `bin` instead of an array of integers. An 848-byte
payload goes from 1234 bytes on the wire to 851 — 1.45x off every media byte,
on the one link measured to be starved.

This is a correction to the previous commit's reasoning, not a new capability.
That commit claimed the derive could not emit bin and left a test ignored
against a hint that needed designing. The hint has existed all along:
cultcache-rs-derive parses `bytes` at lib.rs:92, serializes through
serde_bytes::Bytes at :135, and decodes through ByteBuf at :154 and :170. What
is inert on a DatabaseEntry struct is `#[serde(with = "serde_bytes")]`, because
the derive reads only its own attribute namespace — which is a different and
much smaller claim than the one that was made.

The ignored test is now un-ignored and passing, and the tolerant assertion it
was propping up is exact again.

Four fields elsewhere are missing `bytes` and are deliberately untouched here:
IdunnDeploymentBrakeRecord.signature, IdunnDaemonHealthTrustBindingRecord
.signer_public_key, GameCultServiceTrustAnchorRecord.signer_public_key, and
IdunnAuthenticatedProviderHealthProjectionRecord.signature. Those carry stored
state and signing history — six trust anchors live in the deployment projection
on yggdrasil, and the brake store is byte-exact checked — so they need
sequencing with a re-sign rather than a flag flip. Owned by the Swarm Migration
work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 00:31:11 +02:00
Meta Vens
a250ec69eb Give the media stream records a home that is not one producer's
Four record types described a media stream and nothing about who produced it,
while living in odin-core under a `muninn.` prefix. Every field is
producer-agnostic; the name was the only coupling, and a second producer would
have had to impersonate Muninn or fork the shape. They move to cultnet-rs as
`gamecult.media_*`, matching the prefix already used for cross-service contracts
like gamecult.runtime_presence_health and gamecult.service_trust_anchor, while
cultmesh.* stays reserved for session primitives.

Field order and key indices are carried over untouched, including the `default`
on missing_video_chunk_keys. The tests pin what a consumer in another language
is entitled to rely on: positional layout, the slot each field occupies, and
that a record written before that field was appended still decodes.

One test is ignored rather than passing. Byte payloads currently serialize as
MessagePack arrays of integers because the derive writes its own Serialize and
ignores serde attributes; C#, TypeScript and Python all emit bin, which makes
Rust the divergent runtime and costs 1.45x on every media byte. Fixing it needs
an opt-in hint in cultcache-rs-derive rather than a global change, because
signatures are taken over the whole positional serialization and canonical
hashes over the same bytes — flipping byte encoding estate-wide would invalidate
the live Idunn authority chain. That work is sequenced by the Swarm Migration
session; the ignored test un-ignores when the hint lands.

Parked on its own branch. The Muninn and Odin ends of this move are not done:
odin-core still declares these types, and muninn-daemon still imports them from
there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 00:17:19 +02:00
Meta Vens
cbde940df2 Let the advertised profile own each channel's reliable expiry
The first pass put the media deadline on the transport options and carried it as
a runtime field on the connection and hub. That is a second answer to what a
channel does, able to disagree with the profile the peer was actually shown.

CultNetTransportChannel now carries reliable_expire_after_ms and the send path
resolves the deadline from the profile, so there is one answer. The runtime
fields are deleted rather than left as a shadow copy. The transport option
remains as the knob that populates the profile, not as a parallel authority.

Odin's fork had the field in exactly this place, which is how the gap surfaced:
its muninn test reads it off the channel descriptor.

This is wire-visible, unlike the rest of the expiry work. The field is optional
and skipped when absent, so a profile that does not set it is byte-identical to
one from a runtime that has never heard of it -- but the RUDP profile does set
it on the media channel, so Rust peers now advertise it. Verified against the
interop lane: TypeScript, C# and Kotlin all accept the profile unchanged, in
five passing lanes each. The Python lanes cannot run here because this machine's
Python runtime has no msgpack, and they fail identically before and after this
change.

The other four runtimes should grow the field so a profile round-trips through
them without losing the channel's declared behaviour.

cultnet-rs: 107 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:57:44 +02:00
Meta Vens
bac5be88b9 Restore reliable expiry and snapshot compare-exchange to the Rust core
Two capabilities existed only in Odin's vendored forks, which is why the forks
could not be deleted. Both are ported here with tests so Odin can consume
CultLib instead.

cultnet-rs gains reliable send expiry. A reliable packet may now carry a
deadline; past it the packet is dropped instead of retransmitted, and the count
is reported through session and transport stats. Live media tolerates loss but
not lateness -- a frame retransmitted after its moment has passed is worse than
a missing one -- so the media channel takes a 75ms default while state channels
never expire. Handshake packets never expire either: a dropped connect is not a
late frame.

The deadline is fixed when a send is admitted, so time spent waiting in the
backlog spends it exactly as time spent awaiting an acknowledgement does. This
lineage has a queued_reliable backlog that Odin's does not, and a queued packet
promoted after its moment had passed would be as useless as a retransmitted
one. Expiry runs before the window is measured, so an expiring stream is not
refused for a backlog it no longer owns.

cultcache-rs gains compare_exchange_snapshot and a nonblocking
try_compare_exchange_snapshot. CultLib had neither, despite carrying the larger
body -- the two lineages had diverged in both directions. Contention is returned
as data rather than waited on, so the caller keeps retry and scheduling
authority. This is the primitive an admitted-generation CAS needs.

No wire format changes. Expiry is a sender-side policy and compare-exchange is
local storage, so the other four runtimes stay in agreement without matching
these refusals. They are still worth carrying eventually, and the parity ledger
already tracks that this kind of gap does not announce itself.

cultnet-rs: 106 tests. cultcache-rs: 51 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 03:21:01 +02:00
Meta Vens
85f7024a35 Refuse a framed length beyond the advertised maximum
Found while checking what would be lost by deleting Ghostlight's vendored fork.
Its copy of transport.rs carried a guard CultLib never had: tcp_framed receive
compared the declared payload length against the max_payload_bytes the profile
advertises for the schema channel, and refused before allocating.

Without it the length in a four-byte header is taken on trust and used directly
to size a buffer, so one header reserves whatever the peer names before a single
payload byte arrives. The guard sits ahead of the allocation, which is the only
place it does any good.

It is inert unless a profile advertises a maximum, so the test advertises one.

This is the last thing unique to that fork. Everything else in it is now here or
was already here, which is what makes the fork safe to delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 01:09:11 +02:00
Meta Vens
5dbc0c29f5 Bound the RUDP receive path and refuse hostile sequences
Clears the cultnet half of the retained debt. The two unported commits from the
retiring GameCult/cultnet-rs lineage were not portable as patches: this lineage
had reworked the same receive path with sequence domains and ACK horizons, and
applying them raised nine conflicts that would have reverted that work. Most of
what they added already existed here by another route -- received-sequence
history, fragment-set count and reassembly bytes are all bounded already. So
the gaps were taken rather than the patches.

Three were real. The per-channel maps grew without limit, and a peer picks its
own channel ids, so ordered and sequenced channel tracking is now capped at 64
each. The out-of-order hold buffer grew without limit, and a peer picks its own
send order, so it is capped at 1024 frames and 4 MB; sizes are computed from the
buffer rather than tracked in counters, which cannot drift.

The fourth was the sharpest. remember_received ran before the duplicate
early-return, so a single forged packet far past the current highest advanced
the receive state beyond every sequence still in flight and starved the
session. Such a packet is now refused without being remembered.

Reliable sequence exhaustion aborted the process through expect(). The capacity
gate that already guards the send window now also refuses when the sequence
space cannot cover the request, and allocation saturates rather than panicking
so no future path can reintroduce the abort.

Five tests cover the bounds and both refusals. cultnet-rs passes 103,
cultcache-rs 48, cultmesh-rs 7, and the TypeScript-Rust RUDP interop lanes pass
5 of 5 with the hardened peer, so legitimate traffic is unchanged.

The parity ledger now records what this actually leaves owed: these are
refusals, not wire-format changes, so the interop lanes stay green whether or
not the other four runtimes match. The gap will not announce itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:57:07 +02:00
Meta Vens
85bc4c71b5 Align the replay-window test with the reliable sequence domain
Separating the sequence domains gated duplicate suppression on packet.reliable
in Rust, TypeScript and C# alike, so lossy datagrams are remembered nowhere and
a replayed one is delivered again. That is the intended contract -- lossy
traffic is fire-and-forget and its history is bounded by not existing -- but
replay_history_is_bounded_and_idle_hub_sessions_expire still asserted the old
behaviour with a lossy packet and had been left failing on the branch.

The three tests the change added in each runtime cover ordered-requires-
reliable, per-channel sequenced scope, and lossy gaps. None covers the bounded
replay window, which was this test's real subject, so removing the assertion
would have dropped the only coverage of a security property.

The lossy assertion now states the new contract, and the reliable domain gets
its own replay check: 4,100 acknowledged reliable sends, then the oldest
replayed and rejected by the window.

Rust 151, C# 148, TypeScript 48, Python 138 all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:36:36 +02:00
Meta Vens
0736ae4824 Separate RUDP reliable and lossy sequence domains 2026-09-04 00:33:16 +02:00
Meta Vens
4c0dc07ba5 Make the Rust suite green and stop silently ignoring packages
exact_reliable_acks_clear_more_than_one_ack_window_of_fragments has been
failing on main. It was not a defect: send_many returned 32 packets where the
test expected 40, and 32 is exactly CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.
The reliable send window landed and this test kept asserting the pre-window
behaviour, so a test whose name is about crossing an ack window had stopped
crossing one. It now asserts that a window's worth leaves immediately, that
the remaining eight fragments queue, and that acknowledgements promote them
until every fragment is delivered -- the same loop packages/cultnet-rs/tests
/cultnet.rs already uses for this contract.

All three Rust crates are green: 46, 95 and 7.

The NuGet packages rule hides every language package until one is named in
the re-include list, and cultmath, cultmesh-browser and cultmesh-kotlin were
never added. Nothing is currently lost -- every source file in them is
tracked -- but any new file added to those three would have been dropped in
silence. cultmesh-rs hit this on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:01:17 +02:00
Meta Vens
75c180782a Acknowledge retransmits across RUDP horizons 2026-08-23 19:15:22 +02:00
Meta Vens
ed273e6865 Keep large RUDP test receiver alive 2026-08-23 19:05:54 +02:00
Meta Vens
fda51fad50 Acknowledge reliable RUDP windows cumulatively 2026-08-23 19:00:33 +02:00
Meta Vens
a2e3d3fa35 Bind reliable windows to the RUDP ACK horizon 2026-08-23 18:54:18 +02:00
Meta Vens
f4dabfefb0 Bound RUDP reliable transmission windows 2026-08-23 18:35:49 +02:00
Meta Vens
ba2ef582d2 Add Rust provider session transport 2026-07-19 00:49:53 +01:00
Meta Vens
5a57e5e8a9 Keep RUDP acknowledgements sequence-neutral 2026-07-10 11:44:04 +01:00
Meta Vens
60f782faa7 Add Rust reactive document sync 2026-06-28 00:31:35 +01:00
Meta Vens
bbde981082 Add Rust one-call snapshot document sync 2026-06-28 00:17:24 +01:00
Meta Vens
8533da0037 Add Kotlin and Rust alias sync coverage 2026-06-28 00:12:05 +01:00
Meta Vens
d85f3fd32a Document CultLib substrate and schema server port 2026-06-21 16:31:50 +01:00
Meta Vens
b72bc7803b Add Rust CultMesh connected RUDP clients 2026-06-17 06:04:00 +01:00
Meta Vens
d131d0f42a Add Rust RUDP schema message helpers 2026-06-17 04:29:22 +01:00
Meta Vens
2998f96e73 Add Rust authorized RUDP peer sugar 2026-06-16 17:01:37 +01:00
Meta Vens
0308167c3e Add Rust CultMesh catalog factories 2026-06-16 16:26:04 +01:00
Meta Vens
3a8844be11 Add Rust shard catalog helpers 2026-06-16 14:58:35 +01:00
Meta Vens
4335ab10bd Fix RUDP ordered control sequence parity 2026-06-16 13:18:39 +01:00
Meta Vens
3fde29cc04 Add Rust RUDP reconnect loop 2026-06-16 06:43:22 +01:00
Meta Vens
4a54966b10 Add portable reconnect controllers 2026-06-16 05:29:45 +01:00
Meta Vens
290620a317 Add Rust CultMesh peer RUDP facade 2026-06-16 04:58:06 +01:00
Meta Vens
9e97d94796 Advertise RUDP reconnect policy in profiles 2026-06-16 04:01:41 +01:00
Meta Vens
b3be836115 Add portable reconnect policy helpers 2026-06-16 03:07:31 +01:00
Meta Vens
48135703e8 Preserve Libby persona state and database preverify flow 2026-06-15 19:45:34 +01:00
Meta Vens
c85b5fb561 Add RUDP reliable queue bounds across runtimes 2026-06-15 15:24:11 +01:00
Meta Vens
8287926921 Add RUDP ping timeout parity 2026-06-15 03:26:40 +01:00
Meta Vens
d6f5ee28b0 Add RUDP fragmentation across runtimes 2026-06-15 02:26:34 +01:00
Meta Vens
41bc58a3f6 Add Rust RUDP socket transport 2026-06-14 23:11:19 +01:00
Meta Vens
c71a1bc5da Port RUDP reliability core to Rust 2026-06-14 22:42:36 +01:00
Meta Vens
c72abaf904 Port RUDP packet fixture across runtimes 2026-06-14 22:20:49 +01:00
Meta Vens
a11312a467 Add Rust CultNet transport port 2026-06-14 21:48:07 +01:00
Meta Vens
a2bc9aeab4 Advertise CultNet transport profiles 2026-06-14 21:20:52 +01:00
Meta Vens
94e9b4b2b4 Add CultNet transport profile contract 2026-06-14 20:32:25 +01:00