Tests that block the merge
Aepsy is a Swiss digital mental health platform: one TypeScript codebase shipping as the web app, the iOS app, and the Android app. The testing effort there is the team's; my part was making its standard enforceable. I wrote the commit that turned scattered practice into lint rules, machine readable test writing skills, and one fixed test environment, and brought the suite to the point where making it a required check was safe to ask for.
web, iOS, and Android from one codebasetests required to mergeVitest, JUnit, pytest
Problem
Aepsy is a digital mental health platform, and one TypeScript codebase ships as its web app, its iOS app, and its Android app. A change does not stay in one place: the same code reaches all three, and two of the three move at app store cadence rather than at the speed a fix can be written.
The testing effort there is a team effort, which is exactly where a convention goes quiet. Written down, a convention is something people mean to follow. My part of the work was making the standard something the repository holds rather than something a reviewer has to remember under a deadline.
Constraints
Three platforms come out of one codebase. The web app deploys continuously and the native apps move at app store cadence, so anything that has to land everywhere at once has to be planned around the slower channel.
Tests have to be deterministic. The test environment is committed and the timezone is forced to UTC, so a date behaves the same on every machine that runs the suite.
The standard has to survive tests written with AI tooling. A convention that lives only in prose does not survive that, so whatever holds it has to be readable by the tooling itself.
There is no automated end to end tier, so the tiers below it carry the argument that a change is safe. The client strategy is integration first for the same reason: many integration tests, few unit tests.
What I did
The testing effort at Aepsy is collective. Others built the runner foundation and wrote most of the tests; what follows is the part that is mine.
I proposed the standardization round, made the case for why it was needed, and wrote the commit. It turned scattered practice into things a machine reads: lint rules on test files, the first version of the test writing skill, fixtures consolidated onto one shape, test configuration split out on its own, and the test environment fixed. Separately I brought new test tiers into CI, and I write tests in the suite like everyone else.
The suite went from a few dozen files to hundreds in the months after the standard landed, written by the whole team, most of it not by me, against one set of templates, fixtures, and banned patterns. Enforcement arrived in steps, and the last one was not mine to take. A required check that is red or slow blocks everybody else, so I got the suite steady on my own machine first, then ran it on dev to see whether it broke or dragged, and only then asked the person holding the repository rights to turn it on. Where a test turned out flaky I traced the cause and fixed it rather than papering over it with a retry.
The other thread was two factor authentication across all three platforms at once. I built the foundation: the client service, a native plugin with an iOS and an Android implementation behind one TypeScript interface, native because the web auth SDK could not complete the SMS step inside the app's WebView, and the enrollment and challenge UI, all merged dark behind a single shared flag. Weeks of polish by the team followed, and production came on with a one line change. The real coordination problem is the release channels: the web app deploys continuously, the native apps move at app store cadence, so the native releases had to be ready before the flag ever moved. Accounts with and without two factor coexist indefinitely; that is a design property, not a transition.
Decisions
| Decision | Why | What I rejected | What it cost |
|---|---|---|---|
| Enforce in the merge path, not in review | A rule that lives in review discipline disappears under deadline; a required check does not | Relying on the review rubric alone | A gate proves the tests ran and passed. It cannot prove they assert anything; that still takes review |
| Machine readable skills for AI written tests | Tests are increasingly written with AI tooling, and prose conventions do not survive that; templates and banned patterns do | A style guide nobody's tooling reads | A skill shapes what gets written; it cannot block a hand written commit |
| One fixed test environment | A committed env and a fixed timezone make time deterministic, the recurring source of flakes | Per test setup |
Details that mattered
There is no automated end to end layer yet; the strategy documents call it phase two, and scripted manual QA covers the native flows meanwhile.
Three runners have to agree on what a standard means: the client tests run on Vitest, the backend tier on JUnit, and a Python tier on pytest. That is the spread the standard has to hold together, and it is why the rules that matter are the ones a machine reads rather than the ones a document states.