From 7c4f2b720a28166ae45c55232cea2aef08e705c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:21:17 +0000 Subject: [PATCH] refactor(negentropy): audit fixes for interop tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete `strfryDrivesGeodeAsServer` — boots strfry but uses kmp-negentropy ↔ Geode, no actual strfry-vs-geode interop. Duplicates `Nip77NegentropyTest.negentropyComputesSymmetricDifference`. - Strip speculative `negentropy { enabled = ... }` and `nofiles` blocks from the strfry config; defaults are what we want to test. - `InteropSyncDriver.reconcile` → `negotiate`. The function computes the symmetric difference; it doesn't move events. Convert to `suspend fun` to drop a nested `runBlocking` that could deadlock under dispatcher pressure. - Inline `pullSync` helper in GeodeVsGeodeNegentropySyncTest — it was a one-line wrapper with a misleading name. - Batch `relayB.preload(needFromA)` instead of looping. - Tighten `idsOnRelay(NormalizedRelayUrl)` signature (was re-parsing the string on every call). - Drop redundant `assertTrue(size > cap)` after `assertEquals(11)`. --- .../interop/GeodeVsGeodeNegentropySyncTest.kt | 77 ++----- .../GeodeVsStrfryNegentropySyncTest.kt | 212 +++++------------- .../geode/interop/InteropSyncDriver.kt | 38 ++-- .../sqlite/SnapshotIdsForNegentropyTest.kt | 5 +- 4 files changed, 105 insertions(+), 227 deletions(-) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsGeodeNegentropySyncTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsGeodeNegentropySyncTest.kt index 3c7a49a82..dd79a9475 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsGeodeNegentropySyncTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsGeodeNegentropySyncTest.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirm import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync @@ -112,25 +113,7 @@ class GeodeVsGeodeNegentropySyncTest { } } - /** - * One-way reconciliation from `srcRelay` ⇒ `dstRelay`'s perspective - * (the destination is the side initiating the sync, mirroring - * `strfry sync ` semantics where the calling instance pulls - * from the remote). - * - * Returns the driver result so callers can assert round counts / - * error states. - */ - private fun pullSync( - sourceWsUrl: String, - destLocalEvents: List, - filter: Filter, - ): InteropSyncDriver.Result = - InteropSyncDriver(httpClient).reconcile( - wsUrl = sourceWsUrl, - filter = filter, - localEvents = destLocalEvents, - ) + private val driver by lazy { InteropSyncDriver(httpClient) } @Test fun bidirectionalSyncConvergesTwoRelays() = @@ -145,71 +128,59 @@ class GeodeVsGeodeNegentropySyncTest { relayB.preload(bEvents) val filter = Filter(kinds = listOf(1)) + val urlA = serverA.url.normalizeRelayUrl() + val urlB = serverB.url.normalizeRelayUrl() - // --- B pulls from A --- - val pullAtoB = pullSync(serverA.url, bEvents, filter) - assertNull(pullAtoB.error, "A→B reconciliation must not error") + // B negotiates the symmetric difference with A. + val diff = driver.negotiate(serverA.url, filter, bEvents) + assertNull(diff.error, "negotiation must not error: ${diff.error}") // From B's perspective: needs A-only, has B-only. assertEquals( aEvents.subList(0, 5).map { it.id }.toSet(), - pullAtoB.needIds, + diff.needIds, "B should NEED [0..4] from A", ) assertEquals( bEvents.subList(10, 15).map { it.id }.toSet(), - pullAtoB.haveIds, + diff.haveIds, "B should announce HAVE for [15..19]", ) // Close the loop: fetch needs from A, push haves to A. val needFromA = - client.fetchAll( - relay = serverA.url.normalizeRelayUrl(), - filter = Filter(ids = pullAtoB.needIds.toList()), - ) - for (e in needFromA) relayB.preload(e) + client.fetchAll(relay = urlA, filter = Filter(ids = diff.needIds.toList())) + relayB.preload(needFromA) - for (id in pullAtoB.haveIds) { - val ev = bEvents.first { it.id == id } - client.publishAndConfirm(ev, setOf(serverA.url.normalizeRelayUrl())) + for (id in diff.haveIds) { + client.publishAndConfirm(bEvents.first { it.id == id }, setOf(urlA)) } // --- Verify convergence --- - val expectedAll = all.map { it.id }.toSet() - val onA = relaySnapshotIds(serverA.url, filter) - val onB = relaySnapshotIds(serverB.url, filter) - assertEquals(expectedAll, onA, "Relay A should hold every event after sync") - assertEquals(expectedAll, onB, "Relay B should hold every event after sync") + val expected = all.map { it.id }.toSet() + assertEquals(expected, idsOnRelay(urlA, filter), "Relay A should hold every event") + assertEquals(expected, idsOnRelay(urlB, filter), "Relay B should hold every event") } @Test fun negentropyConvergesInBoundedRoundsOnSmallCorpus() = runBlocking { val all = makeEvents(200) - val aEvents = all.subList(0, 150) + relayA.preload(all.subList(0, 150)) val bEvents = all.subList(50, 200) - relayA.preload(aEvents) relayB.preload(bEvents) - val filter = Filter(kinds = listOf(1)) - val res = pullSync(serverA.url, bEvents, filter) + val res = driver.negotiate(serverA.url, Filter(kinds = listOf(1)), bEvents) assertNull(res.error) assertEquals(50, res.needIds.size, "B should need [0..49]") assertEquals(50, res.haveIds.size, "B should have [150..199]") - // strfry typically converges these in ≤5 rounds; we leave - // generous headroom but guard against catastrophic regression. + // strfry typically converges these in ≤5 rounds; ≤16 is + // generous headroom that still catches regressions. assertTrue(res.rounds <= 16, "expected ≤16 NEG-MSG rounds, got ${res.rounds}") } - /** Pulls every id matching [filter] from a relay via REQ. */ - private suspend fun relaySnapshotIds( - wsUrl: String, + /** Every event id matching [filter] visible on the relay at [url] via REQ. */ + private suspend fun idsOnRelay( + url: NormalizedRelayUrl, filter: Filter, - ): Set = - client - .fetchAll( - relay = wsUrl.normalizeRelayUrl(), - filter = filter, - ).map { it.id } - .toSet() + ): Set = client.fetchAll(relay = url, filter = filter).map { it.id }.toSet() } diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsStrfryNegentropySyncTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsStrfryNegentropySyncTest.kt index cb9575e7d..ef6271ce6 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsStrfryNegentropySyncTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/GeodeVsStrfryNegentropySyncTest.kt @@ -20,129 +20,100 @@ */ package com.vitorpamplona.geode.interop -import com.vitorpamplona.geode.LocalRelayServer -import com.vitorpamplona.geode.Relay import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener import java.io.File import java.net.ServerSocket +import java.net.Socket import kotlin.io.path.createTempDirectory import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue /** - * Reciprocal interop test: a Geode client driving NIP-77 against a - * real `strfry` instance. + * Reciprocal interop test: Geode's NIP-77 client driving a real + * `strfry` instance. * - * **Opt-in.** Skipped unless the `STRFRY_BIN` environment variable - * (or `-Dstrfry.bin=…` system property) points at a `strfry` binary. - * When unset the test prints a `[skip]` line and returns. Mirrors the - * way `LoadBenchmark` handles its own opt-in: + * **Opt-in.** Skipped unless `STRFRY_BIN` env var (or + * `-Dstrfry.bin=...`) points at a `strfry` binary. When unset, the + * test prints a `[skip]` line and returns. Mirrors the gate + * `LoadBenchmark` uses for `-DrunLoadBenchmark`: * * STRFRY_BIN=/usr/local/bin/strfry ./gradlew :geode:test \ * --tests "*GeodeVsStrfryNegentropySyncTest*" * * The strfry process is booted on a free loopback port with a fresh - * temp LMDB dir. We feed events into it via the Nostr `EVENT` wire - * (no need for `strfry import`), then run [InteropSyncDriver] against - * it. Strfry's `RelayNegentropy.cpp` answers the same NIP-77 wire we - * test against Geode — if both pass we have byte-shape interop, not - * just "passes our own tests". + * temp LMDB dir. We feed events into it via the NIP-01 EVENT wire + * (no `strfry import`), then run [InteropSyncDriver] against it. + * Strfry's `RelayNegentropy.cpp` answers the same NIP-77 wire we test + * against Geode — passing both is byte-shape interop, not just + * "passes our own tests". */ class GeodeVsStrfryNegentropySyncTest { private val strfryBin: String? = System.getenv("STRFRY_BIN") ?: System.getProperty("strfry.bin") private val enabled = strfryBin != null - private lateinit var geodeRelay: Relay - private lateinit var geodeServer: LocalRelayServer - private lateinit var scope: CoroutineScope - private lateinit var client: NostrClient private lateinit var strfryDir: File private var strfryProcess: Process? = null - private var strfryUrl: String? = null - private val httpClient = OkHttpClient.Builder().build() - - @BeforeTest - fun setup() { - if (!enabled) return - geodeRelay = Relay(url = "ws://127.0.0.1:7771/".normalizeRelayUrl()) - geodeServer = LocalRelayServer(geodeRelay, host = "127.0.0.1", port = 0).start() - scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - client = NostrClient(BasicOkHttpWebSocket.Builder { _ -> httpClient }, scope) - } + private val httpClient by lazy { OkHttpClient.Builder().build() } @AfterTest fun teardown() { - if (!enabled) return strfryProcess?.destroy() strfryProcess?.waitFor() if (::strfryDir.isInitialized) strfryDir.deleteRecursively() - client.disconnect() - scope.cancel() - geodeServer.stop() - geodeRelay.close() } /** - * Boots a strfry instance in a temp directory on a free port. - * Writes a minimal config, starts the relay subprocess, and spins - * until the WebSocket port is accepting connections. + * Boots a strfry instance in a temp LMDB dir on a free port. + * Writes the smallest config strfry accepts, starts the + * subprocess, and polls until the WebSocket port is reachable. + * + * The config is intentionally minimal — strfry's defaults + * (negentropy on, sane limits) are what we want to test against. + * Adding speculative config keys risks failing on schema drift. */ private fun startStrfry(): String { val port = ServerSocket(0).use { it.localPort } strfryDir = createTempDirectory(prefix = "strfry-interop-").toFile() val configFile = File(strfryDir, "strfry.conf") - // Minimal strfry config — bind, db dir, NIP-77 enabled. Strfry - // uses libconfig's hcl-ish syntax; this snippet is the smallest - // that boots a relay accepting NEG-OPEN/REQ/EVENT. configFile.writeText( """ db = "${strfryDir.absolutePath}/strfry-db" relay { bind = "127.0.0.1" port = $port - nofiles = 1024000 - negentropy { - enabled = true - maxSyncEvents = 1000000 - } } """.trimIndent(), ) File(strfryDir, "strfry-db").mkdirs() - val pb = + strfryProcess = ProcessBuilder(strfryBin, "--config", configFile.absolutePath, "relay") .redirectErrorStream(true) .redirectOutput(File(strfryDir, "strfry.log")) - strfryProcess = pb.start() + .start() - // Wait for strfry to start accepting connections — give up - // after a few seconds. Strfry typically binds in <500 ms. val deadline = System.currentTimeMillis() + 5_000 while (System.currentTimeMillis() < deadline) { runCatching { - java.net.Socket("127.0.0.1", port).close() - strfryUrl = "ws://127.0.0.1:$port/" - return strfryUrl!! + Socket("127.0.0.1", port).close() + return "ws://127.0.0.1:$port/" } Thread.sleep(100) } @@ -161,37 +132,33 @@ class GeodeVsStrfryNegentropySyncTest { } /** - * Push an event directly into a relay over a one-shot WebSocket. - * Used for both Geode (via `geodeRelay.preload`) and strfry - * (via this method) so the corpus is byte-identical on both sides. + * Push every event in [events] to the relay at [wsUrl] over a + * one-shot WebSocket, waiting for an `OK` response per event. + * + * Used to seed strfry from the same `Event` objects Geode's + * `Relay.preload` accepts — that way both sides start from a + * byte-identical corpus. */ - private fun publishToStrfry( + private suspend fun publishToStrfry( wsUrl: String, events: List, ) { - val ok = - java.util.concurrent.atomic - .AtomicInteger() - val target = events.size - val incoming = kotlinx.coroutines.channels.Channel(kotlinx.coroutines.channels.Channel.UNLIMITED) + val incoming = Channel(UNLIMITED) val ws = httpClient.newWebSocket( - okhttp3.Request - .Builder() - .url(wsUrl.replace("ws://", "http://")) - .build(), - object : okhttp3.WebSocketListener() { + Request.Builder().url(wsUrl.replace("ws://", "http://")).build(), + object : WebSocketListener() { override fun onMessage( - webSocket: okhttp3.WebSocket, + webSocket: WebSocket, text: String, ) { incoming.trySend(text) } override fun onFailure( - webSocket: okhttp3.WebSocket, + webSocket: WebSocket, t: Throwable, - response: okhttp3.Response?, + response: Response?, ) { incoming.close(t) } @@ -199,16 +166,14 @@ class GeodeVsStrfryNegentropySyncTest { ) try { for (e in events) { - val cmd = """["EVENT",${OptimizedJsonMapper.toJson(e)}]""" - check(ws.send(cmd)) { "publish to strfry failed" } + check(ws.send("""["EVENT",${OptimizedJsonMapper.toJson(e)}]""")) { + "publish to strfry failed" + } } - // Drain OK responses. - runBlocking { - kotlinx.coroutines.withTimeout(30_000) { - while (ok.get() < target) { - val raw = incoming.receive() - if (raw.startsWith("[\"OK\"")) ok.incrementAndGet() - } + var oks = 0 + withTimeout(30_000) { + while (oks < events.size) { + if (incoming.receive().startsWith("[\"OK\"")) oks++ } } } finally { @@ -225,86 +190,31 @@ class GeodeVsStrfryNegentropySyncTest { } val strfryWs = startStrfry() - // Same overlap shape as the Geode-vs-Geode test so the two - // results are directly comparable: A=[0..14], B=[5..19]. + // Same overlap shape as GeodeVsGeodeNegentropySyncTest so + // results are directly comparable: A=[0..14], local=[5..19]. val all = makeEvents(20) val strfryEvents = all.subList(0, 15) - val geodeEvents = all.subList(5, 20) + val localEvents = all.subList(5, 20) publishToStrfry(strfryWs, strfryEvents) - geodeRelay.preload(geodeEvents) val filter = Filter(kinds = listOf(1)) - // Drive the negentropy reconciliation from Geode's - // perspective against strfry. This is the wire we care - // about: our client-side `NegentropySession` (kmp-negentropy) - // talking to strfry's server-side `Negentropy ne(storage, - // 500'000)`. Symmetric difference must match the Geode-vs- - // Geode case. - val res = InteropSyncDriver(httpClient).reconcile(strfryWs, filter, geodeEvents) + // The wire we care about: kmp-negentropy (client) talking + // to strfry's `Negentropy ne(storage, 500'000)` (server). + // Symmetric difference must match the Geode-vs-Geode case. + val res = InteropSyncDriver(httpClient).negotiate(strfryWs, filter, localEvents) assertNull(res.error, "Geode↔strfry NEG must not error: ${res.error}") assertEquals( strfryEvents.subList(0, 5).map { it.id }.toSet(), res.needIds, - "Geode should NEED [0..4] from strfry", + "client should NEED [0..4] from strfry", ) assertEquals( - geodeEvents.subList(10, 15).map { it.id }.toSet(), + localEvents.subList(10, 15).map { it.id }.toSet(), res.haveIds, - "Geode should announce HAVE for [15..19]", + "client should announce HAVE for [15..19]", ) - - // Spot-check the wire-level health: round count is bounded. assertTrue(res.rounds <= 16, "expected ≤16 NEG-MSG rounds, got ${res.rounds}") } - - @Test - fun strfryDrivesGeodeAsServer() = - runBlocking { - if (!enabled) { - println("[skip] strfryDrivesGeodeAsServer — set STRFRY_BIN=/path/to/strfry to enable") - return@runBlocking - } - // Reverse direction: the server under test is *Geode*. - // We use kmp-negentropy as the client driver — same role - // strfry's own client takes when running `strfry sync - // ws://geode`. We don't actually shell out to `strfry sync` - // here (its CLI doesn't expose the corpus split we want - // to test); the wire-level equivalence is what matters, - // and InteropSyncDriver uses the same NIP-77 protocol that - // strfry's client speaks. - startStrfry() // unused — we just need to confirm the binary boots - val all = makeEvents(20) - val geodeEvents = all.subList(0, 15) - val driverEvents = all.subList(5, 20) - geodeRelay.preload(geodeEvents) - - val res = - InteropSyncDriver(httpClient).reconcile( - wsUrl = geodeServer.url, - filter = Filter(kinds = listOf(1)), - localEvents = driverEvents, - ) - assertNull(res.error) - assertEquals( - geodeEvents.subList(0, 5).map { it.id }.toSet(), - res.needIds, - ) - assertEquals( - driverEvents.subList(10, 15).map { it.id }.toSet(), - res.haveIds, - ) - - // Cross-check with REQ that Geode actually has what we - // think it has. - val onGeode = - client - .fetchAll( - relay = geodeServer.url.normalizeRelayUrl(), - filter = Filter(kinds = listOf(1)), - ).map { it.id } - .toSet() - assertEquals(geodeEvents.map { it.id }.toSet(), onGeode) - } } diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/InteropSyncDriver.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/InteropSyncDriver.kt index 1ee39d8b7..1768a0f96 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/interop/InteropSyncDriver.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/interop/InteropSyncDriver.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip77Negentropy.NegMsgMessage import com.vitorpamplona.quartz.nip77Negentropy.NegentropySession import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import okhttp3.OkHttpClient import okhttp3.Request @@ -41,27 +40,27 @@ import kotlin.test.fail /** * Equivalent of strfry's `test/syncTest.pl` driver for our interop - * tests. Drives one round of NIP-77 reconciliation against a real - * `ws://` endpoint (a `LocalRelayServer` running Geode, or any other - * relay that speaks NIP-77 such as `strfry`). + * tests. Drives one round of NIP-77 *negotiation* (NEG-OPEN / + * NEG-MSG / NEG-CLOSE) against a real `ws://` endpoint — a Geode + * `LocalRelayServer`, a strfry process, or any other NIP-77 relay. * * The driver opens a raw WebSocket — no `NostrClient` overhead — - * because the goal is to keep the wire format under our direct - * control, the same way `strfry sync` does. That way we exercise the - * server's NEG-OPEN / NEG-MSG / NEG-CLOSE handling with no client- - * side framing or filter-management indirection. + * to keep the wire format under direct control, the same way + * `strfry sync` does. That way we exercise the server's NEG-OPEN / + * NEG-MSG / NEG-CLOSE handling with no client-side framing or + * filter-management indirection. * - * Given a server endpoint and a `localEvents` snapshot, drives the - * reconciliation until completion (or [maxRounds] is reached), and - * returns the symmetric difference plus stats. + * Note: this driver only computes the symmetric difference. The + * actual *sync* (REQ for `needIds`, EVENT for `haveIds`) is the + * caller's job; that's a NIP-01 follow-up, not part of NIP-77. */ class InteropSyncDriver( private val httpClient: OkHttpClient = OkHttpClient.Builder().build(), ) { /** - * Reconciles `localEvents` against the relay at [wsUrl] under - * [filter]. Returns the symmetric-difference id sets so the - * caller can verify or close the loop with REQ/EVENT. + * Negotiates the symmetric difference between `localEvents` and + * the relay at [wsUrl] under [filter]. Returns the id sets so + * the caller can close the loop with REQ / EVENT. * * @param wsUrl the source relay's `ws://…` URL. * @param filter NEG-OPEN filter — usually the broadest filter the @@ -72,12 +71,12 @@ class InteropSyncDriver( * here so the relay's configured cap (500_000 by default) is * what governs framing — same shape as `strfry sync`. * @param timeoutMs hard timeout on a single NEG-MSG round trip. - * @param maxRounds upper bound on round trips. Strfry's typical - * converge in ≤5 rounds for 100 k corpora; 64 is a generous + * @param maxRounds upper bound on round trips. Strfry typically + * converges in ≤5 rounds for 100 k corpora; 64 is a generous * safety net that catches pathological splits without hanging * tests forever. */ - fun reconcile( + suspend fun negotiate( wsUrl: String, filter: Filter, localEvents: List, @@ -128,10 +127,7 @@ class InteropSyncDriver( val needIds = mutableSetOf() var rounds = 0 while (rounds < maxRounds) { - val raw = - runBlocking { - withTimeout(timeoutMs) { incoming.receive() } - } + val raw = withTimeout(timeoutMs) { incoming.receive() } val msg = OptimizedJsonMapper.fromJsonToMessage(raw) rounds++ when (msg) { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SnapshotIdsForNegentropyTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SnapshotIdsForNegentropyTest.kt index d2cb5e134..fe1355160 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SnapshotIdsForNegentropyTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SnapshotIdsForNegentropyTest.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue /** * Verifies the NIP-77 negentropy id-and-time projection against the @@ -92,9 +91,11 @@ class SnapshotIdsForNegentropyTest : BaseDBTest() { // cap = 10; we have 30 rows, so the result must be 11 // (cap + 1 sentinel) — matches strfry's `maxSyncEvents` // overflow-detection idiom. + // cap=10 with 30 rows → result must be the +1 sentinel + // (11 rows). Caller compares `size > cap` to detect + // overflow — matches strfry's `maxSyncEvents` idiom. val capped = db.snapshotIdsForNegentropy(listOf(filter), maxEntries = 10) assertEquals(11, capped.size) - assertTrue(capped.size > 10, "caller relies on size > cap as overflow signal") // cap >= total: returns the whole set unchanged. val whole = db.snapshotIdsForNegentropy(listOf(filter), maxEntries = 100)