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
@@ -213,7 +213,7 @@ and you can fill everything in from **Settings**.
|
|||||||
|
|
||||||
* `backend/` — FastAPI app. `app/services/` holds the thinking (timer, tasks,
|
* `backend/` — FastAPI app. `app/services/` holds the thinking (timer, tasks,
|
||||||
stats, report, blocker policy, scheduler, bot); `app/sources/` holds one module
|
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
|
* `frontend/` — React + TypeScript + MUI, built by Vite into the backend image and
|
||||||
served from the same origin (so there is no CORS to configure).
|
served from the same origin (so there is no CORS to configure).
|
||||||
* **The look** — warm paper, ink type, hairline rules instead of boxes and
|
* **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
|
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).
|
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
|
* `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).
|
[`android/README.md`](android/README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -270,10 +270,10 @@ Gitea Actions, in `.gitea/workflows/`:
|
|||||||
|
|
||||||
| Workflow | When | What |
|
| 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` |
|
| `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 `:<short-sha>` |
|
| `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 `:<short-sha>` |
|
||||||
| `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
|
**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
|
of that, and its header is the setup checklist: a registration token from
|
||||||
|
|||||||
+27
-10
@@ -10,7 +10,7 @@ the rear Glyph Matrix.
|
|||||||
| Module | What it is | Needs the Android SDK? |
|
| 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** |
|
| **`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
|
Everything with interesting behaviour lives in `core`, deliberately. The result is that
|
||||||
the hard parts — offline reconciliation, glob and mask matching, the blocking policy,
|
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:
|
no emulator, no SDK and no phone in the loop:
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
./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
|
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
|
`app/src/test/resources/fixtures/` and a `MockWebServer`; there is no step that curls a
|
||||||
live server.
|
live server.
|
||||||
|
|
||||||
`:core` (190 tests):
|
`:core` (193 tests):
|
||||||
|
|
||||||
| Suite | What it pins down |
|
| 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 |
|
| `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 |
|
| `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 |
|
| 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 |
|
| `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 |
|
| `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` |
|
| `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
|
## Toolchain
|
||||||
|
|
||||||
@@ -131,9 +137,17 @@ user's always-on VPN:
|
|||||||
the accessibility service presses **home**. Android gives no app permission to
|
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
|
force-stop another, so "closed" means "ejected to the home screen, every time", and
|
||||||
the attempt is still reported.
|
the attempt is still reported.
|
||||||
* **Offline, the server's last answer keeps working.** The `blocker` half of
|
* **Offline, the server's last answer keeps working** — and it survives the process,
|
||||||
`/api/settings` is kept and re-evaluated with `:core`'s `evaluatePolicy` — the same
|
which is what makes that claim true rather than aspirational. The `blocker` half of
|
||||||
function and the same reason strings as `backend/app/services/blocker_service.py`.
|
`/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:
|
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
|
always-on VPN. Website blocking reads the browser URL bar through an accessibility
|
||||||
service instead; app blocking polls `UsageStatsManager`. Neither touches the VPN slot.
|
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,
|
* **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
|
and re-anchors on every reply. Offline writes go to `:core`'s outbox — coalesced at
|
||||||
always wins the final word.
|
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
|
## What the app actually is
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user