Write the control flag as the number the spec says
`Content.disappearing` emitted `"ctl":true` where §6.10 specifies `"ctl":1`. That was not a cosmetic disagreement between engines. iOS decodes the field as a number through a synthesised `Codable`, so a boolean did not merely fail to set the flag — it threw out of the initialiser and failed the WHOLE envelope's decode, dropping the message onto the plain-text path. An Android user turning on disappearing messages sent an iPhone a bubble containing raw JSON, and the timer was never applied at that end. Nothing caught it because each engine parsed what it had written itself. The piece-level tests passed on both sides, the vectors do not cover this envelope, and the two halves only meet on two real phones. Both spellings are accepted on the way in. `org.json`'s `optBoolean` does not coerce a number, so reading only that missed every `"ctl":1` the other two engines send — and reading only the number would miss the `true` this engine wrote until today, which is on phones now and cannot be recalled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
72314491fa
commit
db5aa33280
@@ -0,0 +1,73 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import org.json.JSONObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* The `ctl` flag, as all three engines actually write it.
|
||||
*
|
||||
* 🔴 **This engine wrote `"ctl":true` and the specification says `"ctl":1`.**
|
||||
* That was not a cosmetic disagreement. iOS decodes the field as a number
|
||||
* through a synthesised `Codable`, so a boolean did not fail to set the flag —
|
||||
* it failed the whole envelope's decode and dropped the message onto the
|
||||
* plain-text path. An Android user turning on disappearing messages therefore
|
||||
* sent an iPhone a bubble containing raw JSON, and the timer was never applied
|
||||
* at that end. Nothing caught it because each engine parsed what it had written
|
||||
* itself, and the piece-level tests on both sides passed.
|
||||
*/
|
||||
class ControlFlagInteropTest {
|
||||
|
||||
@Test
|
||||
fun `a disappearing envelope carries the number the spec asks for`() {
|
||||
val envelope = JSONObject(Content.disappearing(3600))
|
||||
assertEquals(1, envelope.get("ctl"),
|
||||
"the spec says {\"fnc\":1,\"kind\":\"…\",\"ctl\":1}")
|
||||
assertEquals(3600, envelope.getInt("secs"))
|
||||
assertEquals(1, envelope.getInt("fnc"))
|
||||
}
|
||||
|
||||
/** And it still parses here, which is the half that already worked. */
|
||||
@Test
|
||||
fun `our own disappearing envelope round trips`() {
|
||||
val content = Content.parse(Content.disappearing(900))
|
||||
assertEquals(900, Content.disappearSeconds(content))
|
||||
assertTrue(content.isControl)
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ Both shapes are accepted on the way IN. `org.json`'s `optBoolean` does
|
||||
* not coerce a number, so reading only that missed every `"ctl":1` the
|
||||
* other two engines send — and reading only the number would miss the
|
||||
* `true` this engine itself wrote, which is on phones now and cannot be
|
||||
* recalled.
|
||||
*/
|
||||
@Test
|
||||
fun `both spellings of the flag are understood`() {
|
||||
for (spelling in listOf("1", "true")) {
|
||||
val raw = """{"fnc":1,"kind":"some_future_thing","ctl":$spelling}"""
|
||||
assertTrue(Content.parse(raw).isControl,
|
||||
"a control envelope written as ctl:$spelling was shown to the user")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 The point of the flag: a control kind from a NEWER peer must be
|
||||
* swallowed, not rendered. Without it, every control kind added after a
|
||||
* given release shows in that release as a bubble the person cannot act on.
|
||||
*/
|
||||
@Test
|
||||
fun `an unknown control kind is swallowed rather than shown`() {
|
||||
val content = Content.parse("""{"fnc":1,"kind":"invented_next_year","ctl":1}""")
|
||||
assertTrue(content.isControl)
|
||||
assertTrue(!content.isRenderable, "control traffic must not reach the thread")
|
||||
}
|
||||
|
||||
/** An ordinary message is untouched by any of it. */
|
||||
@Test
|
||||
fun `a plain message is not control traffic`() {
|
||||
assertTrue(!Content.parse("just a message").isControl)
|
||||
assertTrue(!Content.parse(Content.text("hello")).isControl)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user