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
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:
2 files changed
+31
-14
No files matched your search
+27
-10
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user