It ran by hand until now, and a by-hand gate is one that rots: the e2e
copy sat red for nine days after a formatter moved a marker, and nothing
said so. The job is the same script the release runbook already names,
called from the repository root, and it takes seconds.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The module build directories are rewritten every job on a machine that
is also the mac-local runner for both app repos, so indexing them lands
in the merge path for everything. `mds_stores` and `mdworker_shared` sit
at the top of CPU while no build process is near it, and the load
average goes into the hundreds.
Measured on the iPhone repo, same from-scratch build twice: it peaks at
load 12.5 with its output excluded and over a hundred without, and the
indexing work stops accumulating rather than trailing the build by
minutes.
The markers are written by the job rather than committed, because the
directories they belong in are created by the build and removed between
runs. The stable caches outside the repo were excluded on the host
itself: DerivedData, the Android SDK, the runner's builds directory and
the Gradle home, 11 GB between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The content layer had no vectors. Everything from section 6.6 onward was
guarded only by same-code round trips: this engine encodes and parses
through its own codec and would pass unchanged if it and the iPhone
disagreed about every key. That is how SPEC 6.18 specified `ment` for as
long as it has existed while both clients have always sent `men`.
`content-v1.json` holds fifteen envelopes generated by RUNNING the
iPhone's encoders, and this suite parses each one and asserts field by
field. "It did not crash" is what the old tests already proved.
One of them pins something easy to get wrong twice: the iPhone writes
`fwd` as the integer 1, not as `true`, and `optBoolean` does not coerce a
number. A parser reading only the boolean answers false and the message
quietly loses the fact that it was passed on.
The suite also fails if the vector file grows a case nobody asserts on.
Separately, and the reason any of this is trustworthy: the test task did
not track the vector files at all. They arrive by environment variable,
Gradle does not track one as an input, so a vector could change under a
build directory that already had results and the task reported
UP-TO-DATE without running. That is not hypothetical here, it is how
twenty vector tests once passed in CI for weeks without executing, on
this same runner, whose build directory survives between runs. All six
vector files are now declared inputs.
Proven rather than asserted: with the inputs declared, flipping `men` to
`ment` in one vector turns the suite red without `--rerun-tasks`. Before
the fix the same change left it UP-TO-DATE and green.
Full suite: 1294 tests, no failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The §16 backup port shipped with a vector suite that finds its input by
walking relative paths out of the checkout. That works on a developer's
machine, where flamenet-e2e sits beside this repo, and cannot work on
the runner, where it does not.
The suite fails rather than skips, deliberately, so it said so: nine
BackupVectorTest failures on every pipeline since the port merged, on
main included. Two things were missing, and both had to be fixed or the
gate would have stayed red.
The pin predates the vectors, so `unit-tests` cloned a flamenet-e2e that
has no backup-v1.json in it. Bumped; nothing else under test/vectors
moved between the two commits, so no other suite changes what it checks.
And neither job exported FLAMENET_BACKUP_VECTORS, so even the job that
clones HEAD, which did have the file, never pointed the suite at it. It
is exported in both now, and named in the pre-flight check, which is
there precisely so a missing file reads as a missing file rather than a
protocol bug.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Remove dates and "this used to say X" framing from prose and comments.
What the tree does now is the only thing that matters; the history is in
git.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Emoji markers, markdown bold inside code comments, and em-dashes are
gone from source comments, string literals, scripts, Gradle files and
the markdown docs. Counted before and after:
emoji markers 2680 -> 1 (the emoji picker's own list, which is the feature)
em-dashes 2490 -> 9 (table placeholders and fenced code)
Two warning glyphs in user-facing strings in ChatScreens.kt are removed
as well; the sentences say the same thing without them. Two backtick
test names carried an em-dash and now carry a comma.
Nothing else changed. 1208 tests pass, counted from the result XML.
The three obfs4 interop tests are opted out with OBFS4_INTEROP_OPTOUT=1
for this run, as their own message instructs when no bridge is set up.
CONTRIBUTING gets a four-line rule so it does not grow back.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I pinned the vector clone earlier today on flamenet-relay#56's evidence,
and stopped a step short. flamenet-relay did not: it pins AND keeps
`conformance-head` / `doc-pages-head` running against HEAD with
`allow_failure: true`, and its `.e2e-ref.md` says why in a sentence I had
read and not acted on — "a relay tested against a pinned old harness is
tested against a pinned old spec, and that argument still holds."
A pin alone is worse than HEAD for exactly that reason. HEAD couples this
board to another repository's merges; a bare pin means the vectors can
move and this suite goes on checking last month's protocol, reporting
green, with nothing anywhere saying so. The first failure is loud and
wrong; the second is quiet and wrong, and quiet is worse in a conformance
suite whose whole job is to notice a divergence.
So: `.e2e-ref` holds the commit, `unit-tests` checks against it and
blocks, and `vectors-head` runs the same vector suites against HEAD and
is allowed to fail. Being behind is loud and non-blocking. Being broken
is still a hard stop.
Verified it would actually notice, rather than assuming — pin and HEAD
are the same commit today, so a green advisory run proves nothing. With
one expected value in `account-kdf-v1.json` mutated, `AccountKdfTest`
reddens. `.e2e-ref.md` records the procedure and the deadlock story
behind it.
⚠️ `vectors-head` runs `:engine:test` only. The rest of the suite has
nothing to say about whether flamenet-e2e moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I cloned flamenet-e2e at tip an hour ago and wrote down the reasoning:
if the published protocol changes, this suite SHOULD go red without a
commit here, because that is a real divergence and not a stale pin.
flamenet-relay#56 is the write-up of what that costs in practice. The
same pattern there — a job cloning flamenet-e2e at HEAD — turned relay's
main red in a pipeline nobody was watching, blocked six MRs sitting on
merge-when-pipeline-succeeds, and was misdiagnosed as runner starvation
before anyone found the cause. It fired twice in one evening. The person
whose merge did it was working in a different project and had no reason
to look.
The argument for tip is real but it is the smaller one. A protocol
divergence that surfaces as a surprise failure in an unrelated repo does
not get read as a protocol divergence — it gets read as flaky CI, which
is how relay#56 went for three hours. A pin makes the same event a
deliberate bump here, reviewed by somebody who is actually thinking about
the protocol at the time.
Pinned at 9775fbfe (2026-09-05). Move it when the protocol moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turning off the build cache exposed twenty failures that had been there
all along: AccountKdfTest, PQVectorTest and PublishedVectorsTest look for
a sibling `flamenet-e2e` checkout. That exists on a developer's machine
and has never existed in a build directory, so every one of them has been
failing in CI since it was written — and CI reported green, because the
cache was replaying passes computed here.
These are the cross-implementation conformance tests. They are what says
this engine derives the same keys as the one iPhone runs; twenty of them
silently not running is the worst possible thing to find in the suite
that guarantees the two platforms can talk.
They fail rather than skip when the file is missing — deliberately,
exactly so this could not hide. It still hid, one layer up, in a cache.
`flamenet-e2e` is public, so the job shallow-clones it and points the
three environment variables at the result. The paths are checked before
the suite runs: a variable pointing at nothing would put the tests back
where they were, and the failure would read as a protocol bug rather than
a missing file.
Cloned at its tip rather than pinned. The vectors are normative and
shared with iOS, so if the published protocol changes this suite SHOULD
go red without a commit here — that is a real divergence, not a stale
pin.
Verified the variables are actually honoured, rather than assuming: with
one pointed at an empty JSON file the test reddens even though the
sibling checkout is present. 539 engine tests pass against the cloned
vectors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two holes, found by reading the trace of the pipeline that had just
merged android!78 rather than trusting its green.
`:obfs4:test` was not in the task list. The module shipped in !77 with
~100 tests — the Elligator2 port, the framing, the ntor handshake, the
socket factory — and not one of them has ever run in CI. A break anywhere
in the transport would have merged green. The bridge-line parser added in
!78 was in the same position.
The rest ran, but not in this pipeline. The runner is tagged `mac-local`,
so it is the developer's own machine and shares Gradle's build cache with
local runs. Every test task came back FROM-CACHE — the whole job finished
in 5 seconds — which means CI replayed results it never computed. The
outcomes were real, they were just mine from ten minutes earlier. That is
not independent verification, and nothing in the log distinguishes it: a
cached pass and a real pass both print "BUILD SUCCESSFUL". `--rerun-tasks`
now forces execution.
And the job asserted nothing. It ended on gradle's exit status, which is
the same shape of mistake the iOS gate was carrying when it ended in
`grep ... || exit 1` and went green on a red suite. `:app` makes it worse
by printing no per-test names at all, so a filter matching nothing looks
exactly like 145 passing tests.
So the gate reads the results XML: per-module floors at roughly 90% of
today's counts, zero failures, and a total. Old results are deleted first
— the runner reuses its build directory, and XML from a previous pipeline
would otherwise be counted by the very check meant to notice a suite had
stopped running. Hiding one module's results reddens it; that is how the
floors were checked.
1162 tests across the four modules, 0 failures, 44 tasks executed and
none from cache.
⚠️ The interop tests are opted out here and only here — they need a real
lyrebird from the flamenet-elligator2 checkout, which is not in the build
directory. They fail rather than skip when unconfigured, deliberately, so
the opt-out is written down where it can be seen. Run
`scripts/obfs4-interop.sh` before merging obfs4 changes: every other test
in that module compares against recorded vectors or against itself, and
two identically-wrong ends agree perfectly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unit suites run on the project's own runner before main moves. And
scripts/verify-build.py proves what two clean builds already showed:
every byte of the APK's contents reproduces from this source — only the
signing block differs, because signatures are randomized. Rebuild,
compare contents, check the certificate: that is the whole trust chain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The engine job failed on its first real run: PublishedVectorsTest and
AccountKdfTest search sibling checkouts for the canonical vector files, and the
standalone repository has no ../../engine and no flamenet-e2e clone beside it
on a runner. Both tests already honour an env override, so CI fetches both
files from flamenet-e2e at HEAD and points the tests at them.
Deliberately NOT a copied vector file in this repo: a copy drifts, and an
engine tested against its own copy has quietly stopped being tested against
the published ones. The job now fails if flamenet-e2e is unreachable, which is
the correct failure - an engine that cannot see the vectors must not report
green.
Verified by reproducing CI locally: standalone checkout, env vars only,
:engine:test passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every job failed with ci_quota_exceeded on first run. GitLab.com shared minutes
are spent and a public project does not get them free on this plan. The ovh-
runner already serving the iOS repository was not locked to it, so it now serves
all four projects.
⚠️ It is registered run_untagged=false, so a job WITHOUT the tag is never picked
up - it sits pending forever rather than failing, which is the more confusing of
the two outcomes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔴 The find: the secret sweep was failing closed on false positives, so the
mirror job refused to publish. flamenet-e2e has been stale since 2026-08-21 and
flamenet-relay since 08-25, and this is why. A gate that blocks every release on
nothing is a gate somebody switches off, and then it is not a gate.
Two exclusions, neither weakening the shape probes. Interpolated values -
`let token = "\(selector).\(secret)"` builds a string from variables and has no
literal to leak. And `sweep:allow`, an explicit annotation for genuine
exceptions, deliberately visible so an exception is written on the line it
applies to rather than hidden in the sweep script.
⚠️ It must be on the SAME line as the match. I put one on the line above first
and it silently did nothing - documented, because the next person will do the
same.
The test fixture that tripped it sixteen times is now one named constant, which
is better code regardless: one place to change instead of sixteen.
Pipelines for the three mirrored repos, each of which is a mirror and therefore
needs its own - a green pipeline upstream proves the code passes THERE, next to
siblings it does not ship with, and says nothing about whether what landed is
complete. Each has a `complete` job asserting the shape of the tree, because
what a mirror gets wrong is missing files, not failing logic: a test that cannot
import a module is a test that does not run, and a partial copy passes
everything.
And the mirror job whose absence let flamenet-messenger-android rot. It was a
hand-made extract carrying only :engine, from before :client, :app and :interop
existed; its settings.gradle.kts still declared one module while this tree
declared four. Its `complete` job now fails on exactly that drift.
Upstream also runs :client:test, which was never in CI - the module where the
silent message loss lived, whose regression test is worth nothing unless
something runs it. And the server is no longer copied into flamenet-e2e as
well: two public copies of one server means one of them is stale with no way to
tell which.
All four validate against GitLab's linter. Engine offline tests: 87, 32 and 124
passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>