Engine parity for the five features iOS regained: text stories, mentions,
story reactions, story replies. This build could already receive them —
legibly, because each degrades to its caption — but understood none of
them as what they are.
🔴 Every envelope in the test is the shape **iOS** assembles, not this
engine's own output. Two implementations that each only test against
themselves agree right up to the day somebody sends a real message, and
the symptom is a mention that never rings or a story that arrives as
"[unsupported attachment]", with nothing in either log to say why.
Two things the tests caught that reasoning would not have:
⚠️ Kotlin prints a Double in scientific notation, so this engine writes
`"exp":1.8E9` where iOS writes `1800000000.0`. Both are valid JSON and
both sides read either — but a story's expiry decides whether it shows at
all, and "arrived already expired" is indistinguishable from "never
arrived". Now pinned on both sides. Not a new risk: picture stories have
carried `exp` this way all along, which is the reason it works.
⚠️ An earlier version of one test built a JSONObject from an assembled
envelope and re-serialised it — which reordered the keys and moved `fnc`
off the front. It failed, correctly, and is a neat demonstration of why
both engines hand-assemble these strings instead of encoding a map.
UI comes next; this is the layer that decides whether the two apps agree
about what a message means.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>