Live tests: read what a person sees, not what the wire carries

Six live conversation tests failed after key-log checking became
automatic, and the product was right and the tests were wrong.

`poll` returns control traffic as well as messages. Automatic
enforcement is what sets the transparency-log head pin — and §19.4
gossip has nothing to send without one, so until now the first send to a
contact never carried this device's view of the log. It does now. That
envelope is consumed by the client and never shown, exactly as the app
has always filtered it, but these tests asserted on the raw list and
counted it as a message.

Worth stating plainly, because it is the more interesting half: gossip
was written, called, and inert. Each client verified the relay was
consistent with itself and never offered a head for anybody to compare
against — which is the whole mechanism for catching a relay that serves
two different logs. Turning enforcement on turned that on too.

Verified against a live relay rather than a mock: 203 client tests
including sign-up, device registration, prekey publication, two-party
and group conversations, images, sealed sender and the deployed key log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-30 17:04:38 -04:00
co-authored by Claude Opus 4.8
parent db5aa33280
commit 366f5cbeab
2 changed files with 35 additions and 11 deletions
@@ -69,6 +69,23 @@ class LiveConversationTest {
)
}
/**
* What a person would see, which is what every assertion below means.
*
* 🔴 **`poll` returns control traffic too**, and since key-log checking
* became automatic there is reliably some: the first send to a contact now
* gossips this device's view of the transparency log (§19.4), because
* enforcement sets the head pin that gossip had nothing to send without.
* That envelope is consumed by the client and never shown, and asserting on
* the raw list counted it as a message — so these tests failed on a change
* that made the product more correct, not less.
*
* The app has always filtered this way; the tests were reading a layer
* lower than the thing they were describing.
*/
private fun Conversations.readable(): List<Conversations.Received> =
poll().filter { it.content.isRenderable }
@Test
fun `two people exchange messages through a real relay`() {
val base = fleet ?: return
@@ -81,7 +98,7 @@ class LiveConversationTest {
assertTrue(sent.reachedSomeone, "nothing was delivered: ${sent.failed}")
assertEquals(listOf(bob.identity.deviceId), sent.delivered)
val received = bob.conversations.poll()
val received = bob.conversations.readable()
assertEquals(1, received.size, "Bob's inbox had ${received.size} messages")
assertEquals("hello from the Android engine 👋", received[0].text)
assertEquals(alice.userId, received[0].fromUser)
@@ -90,16 +107,16 @@ class LiveConversationTest {
// Bob replies on the session he just established, which is the step
// that turns a handshake into a conversation.
assertTrue(bob.conversations.send(alice.userId, "and hello back").reachedSomeone)
val back = alice.conversations.poll()
val back = alice.conversations.readable()
assertEquals(1, back.size)
assertEquals("and hello back", back[0].text)
// Several turns, so the ratchet is exercised past the handshake.
for (turn in 1..3) {
alice.conversations.send(bob.userId, "alice $turn")
assertEquals("alice $turn", bob.conversations.poll().single().text)
assertEquals("alice $turn", bob.conversations.readable().single().text)
bob.conversations.send(alice.userId, "bob $turn")
assertEquals("bob $turn", alice.conversations.poll().single().text)
assertEquals("bob $turn", alice.conversations.readable().single().text)
}
}
@@ -116,8 +133,8 @@ class LiveConversationTest {
val bob = signUp(base, "d")
alice.conversations.send(bob.userId, "only once please")
assertEquals(1, bob.conversations.poll().size)
assertEquals(0, bob.conversations.poll().size, "the relay re-delivered a read message")
assertEquals(1, bob.conversations.readable().size)
assertEquals(0, bob.conversations.readable().size, "the relay re-delivered a read message")
}
/**
@@ -146,8 +163,8 @@ class LiveConversationTest {
val sent = alice.conversations.send(bob.userId, "to both of your devices")
assertEquals(2, sent.delivered.size, "only reached ${sent.delivered}; failed: ${sent.failed}")
assertEquals("to both of your devices", bob.conversations.poll().single().text)
assertEquals("to both of your devices", secondConversations.poll().single().text)
assertEquals("to both of your devices", bob.conversations.readable().single().text)
assertEquals("to both of your devices", secondConversations.readable().single().text)
}
/**
@@ -198,7 +215,7 @@ class LiveConversationTest {
bob.userId, picture, "image/png", width = 64, height = 48)
assertTrue(sent.reachedSomeone, "nothing delivered: ${sent.failed}")
val received = bob.conversations.poll().single()
val received = bob.conversations.readable().single()
val ref = received.content.attachment
assertTrue(ref != null, "no attachment reference arrived; text was '${received.text}'")
assertEquals("image/png", ref!!.mime)
@@ -249,7 +266,7 @@ class LiveConversationTest {
assertTrue(sent.reachedSomeone, "nothing delivered: ${sent.failed}")
for (member in listOf(bob, carol)) {
val got = member.conversations.poll().single()
val got = member.conversations.readable().single()
assertEquals("hello everyone", got.text)
assertEquals(group.id, got.content.groupId,
"the message did not carry group routing, so it would file under the sender")
@@ -284,7 +284,14 @@ class LiveShapesTest {
val sent = alice.second.send(bob.first, text)
assertTrue(sent.reachedSomeone, "$base delivered nothing: ${sent.failed}")
val got = bob.second.poll()
// 🔴 What a PERSON would see. `poll` also returns control traffic, and
// since key-log checking became automatic there is reliably some: the
// first send to a contact gossips this device's view of the
// transparency log (§19.4), which enforcement is what sets the pin for.
// The client consumes that envelope and never shows it — counting it as
// a message made this test fail on a change that made the product more
// correct.
val got = bob.second.poll().filter { it.content.isRenderable }
assertEquals(1, got.size, "$base: expected one message, got ${got.size}")
assertEquals(text, got.single().text)
}