From 38f346883cc21519510d9c6f5f4e738dc143ccec Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Thu, 22 Oct 2026 18:02:04 +0300 Subject: [PATCH] docs: the counts, and two claims that are now true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 8 ++++---- android/README.md | 37 +++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7d8bb62..f9e76e6 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ and you can fill everything in from **Settings**. * `backend/` — FastAPI app. `app/services/` holds the thinking (timer, tasks, stats, report, blocker policy, scheduler, bot); `app/sources/` holds one module - per integration; `app/api/` is a thin HTTP layer. 71 tests under `backend/tests`. + per integration; `app/api/` is a thin HTTP layer. 73 tests under `backend/tests`. * `frontend/` — React + TypeScript + MUI, built by Vite into the backend image and served from the same origin (so there is no CORS to configure). * **The look** — warm paper, ink type, hairline rules instead of boxes and @@ -228,7 +228,7 @@ and you can fill everything in from **Settings**. enforcement. There is deliberately no window with pages in it — lists, rules and reports are the web app's job. See [`desktop/README.md`](desktop/README.md). * `android/` — the phone client. Everything with interesting behaviour lives in a - plain-JVM `:core` module, so 234 tests run on a laptop with nothing but a JDK. See + plain-JVM `:core` module, so 274 tests run on a laptop with nothing but a JDK. See [`android/README.md`](android/README.md). --- @@ -270,10 +270,10 @@ Gitea Actions, in `.gitea/workflows/`: | Workflow | When | What | | --- | --- | --- | -| `test.yml` | push to master, every PR | backend (73), desktop (47), `:core` (196), and the web app's typecheck + build — four parallel jobs, none of which may need a running backend | +| `test.yml` | push to master, every PR | backend (73), desktop (47), `:core` (193), and the web app's typecheck + build — four parallel jobs, none of which may need a running backend | | `lint.yml` | push to master, every PR | `ruff check` over `backend/` and `desktop/`, with the rule set named in `backend/pyproject.toml` | | `docker.yml` | push to master, `v*` tags | builds the image, starts it against SQLite and waits for `/api/health`, then pushes `git.timka.su/timka123/kadence:latest` and `:` | -| `android-release.yml` | push to master, `v*` tags | `:app`'s Robolectric suite (44), then the release APK — signed from secrets, with `apksigner verify` printing the certificate it actually used | +| `android-release.yml` | push to master, `v*` tags | `:app`'s suite (81, on the JVM), then the release APK — signed from secrets, with `apksigner verify` printing the certificate it actually used | **Nothing runs until a runner is registered.** `docker-compose.ci.yml` is the whole of that, and its header is the setup checklist: a registration token from diff --git a/android/README.md b/android/README.md index 1da74b7..bfc43b4 100644 --- a/android/README.md +++ b/android/README.md @@ -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