docs: the counts, and two claims that are now true
android / apk (push) Failing after 59s
lint / ruff (push) Failing after 3s
tests / backend (push) Failing after 2s
tests / desktop (push) Failing after 2s
tests / web (push) Successful in 27s
tests / android-core (push) Failing after 3s
docker / image (push) Canceled after 8s

274 Android tests, not 234; 73 backend, not 71. The new suites named alongside
the others, since that table is how anyone finds out what is already covered.

Two sentences in the Android README described the intent rather than the code,
and both now describe the code: "offline, the server's last answer keeps
working" (it is on disk, so it survives the process — and the process Android
starts for the accessibility service is the one that needed it most), and
"offline writes go to an outbox" (they do now; it says which guarantees that
buys, and what happens to an interval that cannot be filed honestly). The `:app`
module never contained Room, either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
timka123andClaude Opus 5 committed 2026-10-22 18:02:04 +03:00
1 parent 8e659cd315
commit 38f346883c
2 files changed
+31 -14

No files matched your search

+27 -10
View File
@@ -10,7 +10,7 @@ the rear Glyph Matrix.
| Module | What it is | Needs the Android SDK? |
| --- | --- | --- |
| **`core`** | Plain Kotlin on the JVM: the matchers, the policy clock, the timer maths, the outbox reconciliation, the Glyph frame renderer, the design tokens | **No** |
| **`app`** | The Android client: Compose UI, services, Room, the HTTP client, the Glyph toy | Yes |
| **`app`** | The Android client: Compose UI, services, the HTTP client, the local store, the Glyph toy | Yes |
Everything with interesting behaviour lives in `core`, deliberately. The result is that
the hard parts — offline reconciliation, glob and mask matching, the blocking policy,
@@ -18,7 +18,7 @@ even the 25×25 rear display — are **testable on a laptop with nothing but a J
no emulator, no SDK and no phone in the loop:
```bash
make android-test # both modules: 234 tests, no device, no backend
make android-test # both modules: 274 tests, no device, no backend
make android-test-core # just the pure-logic suite: needs only a JDK
```
@@ -31,12 +31,12 @@ command works on a machine that has never seen the Android tooling.
./gradlew :core:test :app:testDebugUnitTest
```
**234 tests, none of which need a device or a running backend.** The backend lives only
**274 tests, none of which need a device or a running backend.** The backend lives only
on the dev machine, so the suite feeds on committed JSON fixtures under
`app/src/test/resources/fixtures/` and a `MockWebServer`; there is no step that curls a
live server.
`:core` (190 tests):
`:core` (193 tests):
| Suite | What it pins down |
| --- | --- |
@@ -51,7 +51,7 @@ live server.
| `TokenParityTest` | reads `frontend/src/tokens.ts` and `lib/viz.ts` and fails if a single colour channel drifts |
| `AccentTest` | the system-accent policy and its contrast guard |
`:app` (44 tests, on the JVM via Robolectric):
`:app` (81 tests, on the JVM via Robolectric):
| Suite | What it pins down |
| --- | --- |
@@ -60,6 +60,12 @@ live server.
| `KadenceApiTest` | `X-API-Token`, the session cookie, 401 routing (and *not* routing on the login path), both FastAPI `detail` shapes, a 422 rendered as `field: message`, an unreachable server as a typed error |
| `ThemeTest` | the palette actually reaching a composable, and that the serif clock is genuinely fixed-advance |
| `NormalizeBaseUrlTest` | what a phone keyboard produces becoming a URL okhttp accepts — and the scheme guess, since no certificate covers `192.168.1.40` |
| `BlockerEngineTest` | the bug that let the second launch through: enforcement runs on every visit, the report is throttled, and a missing accessibility grant does not consume a visit. Also that a mask the backend could not expand is enforced *online* |
| `UrlBarTest` | which text in a browser's window is the address bar's — and which is the page's, because a false positive ejects you from a page you are allowed to be on |
| `TimerEngineTest` | a tap's effect appearing at once, and the server's reply being kept rather than discarded as a conflict |
| `OfflineWriteTest` | the outbox for real: a queued pause, coalescing, pause-then-resume annihilating, the unfilable interval being reported, and an offline interval surviving a restart |
| `ColdStartOfflineTest` | a process that has never reached the server: the dial shows the kept interval, the guard enforces the kept rules |
| `LocalStoreTest` | the cache round-tripping, and an unreadable file degrading to "no cache" rather than crash-looping a background service |
## Toolchain
@@ -131,9 +137,17 @@ user's always-on VPN:
the accessibility service presses **home**. Android gives no app permission to
force-stop another, so "closed" means "ejected to the home screen, every time", and
the attempt is still reported.
* **Offline, the server's last answer keeps working.** The `blocker` half of
`/api/settings` is kept and re-evaluated with `:core`'s `evaluatePolicy` — the same
function and the same reason strings as `backend/app/services/blocker_service.py`.
* **Offline, the server's last answer keeps working** — and it survives the process,
which is what makes that claim true rather than aspirational. The `blocker` half of
`/api/settings` is written to `filesDir` and re-evaluated with `:core`'s
`evaluatePolicy` — the same function and the same reason strings as
`backend/app/services/blocker_service.py`. Without that, a process Android started to
bind the accessibility service had no rules at all and enforced nothing while
reporting "disabled".
* **Enforcement is not the report's cooldown.** A blocked site or app is turned away
*every* time, debounced only against one visit's duplicate signals; the twenty-second
cooldown throttles what reaches the daily report. Conflating the two is what let the
second launch through.
Two honest limitations follow:
@@ -162,8 +176,11 @@ Two honest limitations follow:
always-on VPN. Website blocking reads the browser URL bar through an accessibility
service instead; app blocking polls `UsageStatsManager`. Neither touches the VPN slot.
* **The server owns the clock.** The phone polls, ticks locally from a monotonic anchor,
and re-anchors on every reply. Offline writes go to an outbox and the server's answer
always wins the final word.
and re-anchors on every reply. Offline writes go to `:core`'s outbox — coalesced at
enqueue, replayed in order, at most once, with a post-condition probe instead of a
blind retry, because none of these endpoints is idempotent. An interval that both
began *and* ended offline is quarantined and said out loud rather than filed at the
wrong hour. The server's answer always wins the final word.
## What the app actually is