Cover the dense-second step-past the new guard sits in front of

The step-past path had no test, and it is the one thing the ignored-cursor
guard could plausibly break: both cases reach the same `delivered == 0`
branch. They are told apart by WHERE the events landed — a dense boundary
second returns them AT the boundary, so `aboveBoundary` stays 0 while
`received` is 1 and the guard holds its fire; only a relay answering ABOVE
the boundary is not paging at all.

Scripted end to end: a second the relay's page cap can only ever return the
head of, the step strictly past it, and the empty EOSEd page below. Also
proves the documented cost is still paid rather than silently changed — the
unreachable tail of that second is lost, and `downloaded` says so.

`event()` grows a nonce so two events can share one `created_at`; the id was
derived from the timestamp alone, which collapsed them into one event and made
a dense second impossible to script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPSzniNdvJxkhRsCe1QcyT
This commit is contained in:
Claude
2026-08-09 23:48:56 +00:00
parent 3b7ffcd06c
commit 7ca4fbab33
@@ -75,16 +75,23 @@ class NostrClientFetchAllPagesDrainTest {
private val relay = RelayUrlNormalizer.normalize("wss://drain.example.com")
private fun event(createdAt: Long) =
Event(
id = createdAt.toString(16).padStart(64, '0'),
pubKey = "f".repeat(64),
createdAt = createdAt,
kind = 1,
tags = emptyArray(),
content = "e$createdAt",
sig = "0".repeat(128),
)
/**
* [nonce] distinguishes two events sharing one `created_at`, which the id would
* otherwise collapse into the same event — and a boundary second holding more
* than one is the whole subject of the dense-second test below.
*/
private fun event(
createdAt: Long,
nonce: String = "",
) = Event(
id = (createdAt.toString(16) + nonce).padStart(64, '0'),
pubKey = "f".repeat(64),
createdAt = createdAt,
kind = 1,
tags = emptyArray(),
content = "e$createdAt$nonce",
sig = "0".repeat(128),
)
@Test
fun anEmptyPageConfirmedByEoseDrains() =
@@ -227,6 +234,50 @@ class NostrClientFetchAllPagesDrainTest {
// ---- termination: the walk must END, whatever the relay does -------------
@Test
fun aBoundarySecondDenserThanAPageIsStillSteppedPast() =
runBlocking {
// The step-past path itself, which had no test and which the
// ignored-cursor guard now sits in front of. The two look identical
// from `delivered == 0` and must NOT be treated alike: a dense second
// returns events AT the boundary, so `aboveBoundary` stays 0 while
// `received` is 1, the guard holds its fire, and the walk steps past
// exactly as before. Only a relay answering ABOVE the boundary — which
// is not paging at all — trips it.
val client = ScriptedClient()
val feeder =
launch {
client.awaitPage(1)
client.listener!!.onEvent(event(2000), false, relay, null)
client.listener!!.onEvent(event(1000, "a"), false, relay, null)
client.listener!!.onEose(relay, null)
// Page two re-asks second 1000 inclusively. The relay's page cap
// hands back the same head of that second — event "b" living
// there too can never be reached. Nothing new: stuck.
client.awaitPage(2)
client.listener!!.onEvent(event(1000, "a"), false, relay, null)
client.listener!!.onEose(relay, null)
// So the walk steps strictly past to 999 and finds the corpus
// ends there.
client.awaitPage(3)
client.listener!!.onEose(relay, null)
}
val result =
client.fetchAllPages(
relay = relay,
filters = listOf(Filter(kinds = listOf(1))),
idleTimeoutMs = 2_000,
) { }
feeder.join()
assertEquals(2, result.downloaded, "the duplicate is dropped, the dense second's tail is the documented loss")
assertEquals(3, client.subscribeCount, "it stepped past the stuck second instead of stopping on it")
assertEquals(PagedFetchResult.End.DRAINED, result.end, "and reached a genuinely empty, EOSEd page below it")
}
@Test
fun aRelayThatIgnoresTheCursorEndsTheWalkInsteadOfSteppingForever() =
runBlocking {