Check the backup payload against the published vector

Both apps were taught one backup shape and nothing checked that they
agreed. Each side's tests asserted against what the OTHER was believed
to write, transcribed by hand from reading its source, which is a test
of one person's reading rather than of interoperability.

`backup-payload-v1.json` in flamenet-e2e is now the authority, and the
bar it sets is exact: decode the canonical payload, encode it again, and
the result must match byte for byte once both are put in a canonical
order. The same vector is checked on the iPhone, so the two are
compared against one file rather than against each other's source.

One real fix came out of writing it. `messageFromShared` wrote
`author` unconditionally, so a one-to-one message that carried no author
came back claiming user zero sent it, and a round trip through this app
changed the file. Storage keeps an author on every message and reads a
missing one as 0, so it is now written only when the file carried one.

The env var is exported in BOTH ci jobs, the pinned one and the one that
runs at vectors HEAD, and added to the gate that checks every vector
file exists before the suite runs. A path that silently does not exist
puts the tests back where they were and the failure reads as a protocol
bug rather than a missing file. `.e2e-ref` moves to the commit that adds
the vector.

Full suite: 1279 tests, no failures. `ci/lint` accepts the pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-10 19:43:33 -04:00
co-authored by Claude Opus 5
parent ca679c22f4
commit fb68878752
4 changed files with 121 additions and 7 deletions
@@ -143,10 +143,11 @@ object BackupPayload {
.put("at", m.optLong("at"))
.put("text", m.optString("text"))
m.optString("mid").takeIf { it.isNotEmpty() }?.let { out.put("mid", it) }
// The author rides along on one-to-one threads too. The iPhone reads it
// only for groups and ignores it elsewhere, which costs nothing and
// keeps an Android-to-Android restore whole.
if (m.has("author")) out.put("author", m.optLong("author"))
// The author rides along on group threads, where it says who spoke.
// Only when there is one: this app writes 0 on a one-to-one message,
// where the author is whoever is not you, and emitting that would put
// a claim in the file that the message came from user zero.
m.optLong("author", 0L).takeIf { it != 0L }?.let { out.put("author", it) }
m.optJSONObject("att")?.let {
out.put("att", attachmentToShared(it, m.optString("kind", Attachment.IMAGE)))
}
@@ -307,7 +308,9 @@ object BackupPayload {
.put("mine", m.optBoolean("mine"))
.put("at", m.optLong("at"))
.put("text", m.optString("text"))
.put("author", m.optLong("author"))
// Storage keeps an author on every message and reads a missing one as
// 0, so this is written only when the file carried one.
m.optLong("author", 0L).takeIf { it != 0L }?.let { out.put("author", it) }
m.optString("mid").takeIf { it.isNotEmpty() }?.let { out.put("mid", it) }
m.optJSONObject("att")?.let { att ->
out.put("kind", att.optString("kind", Attachment.IMAGE))