Files
mantra-kmp/composeApp
Kgothatso Ngako ba0c60dd2e test: cover the NostrDao event funnel and the publish durability split
NostrDao is what every event passes through, inbound and outbound, so its two
decisions carry everything downstream: which of two copies of an event wins, and
what survives when the enrichment after a write fails. Both were described in
comments and neither was asserted.

Deduplication, at all four positions. A first sighting is stored. A strictly
newer copy replaces the stored one. An older copy is ignored. And -- the case
that actually distinguishes the implementations -- a redelivery carrying the
*same* timestamp is a no-op, because the comparison is a strict `>`. That last
one is not hypothetical: relays redeliver and negentropy re-syncs, so the
common case is the same event arriving again unchanged, and a `>=` there would
rewrite the row on every delivery.

The publish durability split, which is where a bug shipped.
`commitPublishedNostrEvent` is the durable half -- mark the unsigned row signed,
store the event, queue a broadcast per relay -- and indexing is best-effort
enrichment that runs in its own transaction. They used to share one, so any
throw in indexing rolled back `signedAt` too. Because the notary drains one
unsigned row at a time, that row was then re-selected forever and every event
queued behind it went unsigned, including the MLS key package that is enqueued
last. The test provokes the failure the way the code itself would fail:
publishing with no target relays reaches `relayURLs.first()` inside the try and
throws. It then asserts `signedAt` and the stored event both survived. The happy
path is covered alongside it, asserting a broadcast request per target relay, so
the durability test cannot pass by publishing nothing at all.

Also covered: an event from an author with no profile leaves a "LOADING..."
placeholder stamped GENESIS_AT rather than nothing, since that row is the only
record that the pubkey was seen and needs fetching; and
rescheduleBroadcastNostrEventRequests re-queueing a broadcast and re-linking it
to the chat line when the event is a group message that has one, without
inventing a relation when it does not.

One test began as a wrong assumption and the schema corrected it. The "no chat
line" case was first written against an event id that had never been stored, and
failed with SQLite 787: BroadcastNostrEventRequest.nostrEventId is a foreign key
onto NostrEvent. So the real invariant is that a broadcast cannot be scheduled
for an event the caller has not saved; the test now stores the event and leaves
only the chat line missing, and says so in a comment rather than quietly
seeding around it.

Verified by mutation: relaxing the dedup comparison to `>=` fails the
same-timestamp test; removing the try/catch around indexing so the throw
propagates fails the durability test. Both mutations were reverted; no
production source is touched by this commit.

Uses `runBlocking<Unit>` on the durability test because its last expression is an
assertNotNull, and a test method that returns a value is rejected by the JUnit4
runner outright.

9 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 03:05:36 +02:00
..