From a6f86cf57550877202611c0c0f2fbdaeb6818c49 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 16 Jul 2026 14:04:16 +0100 Subject: [PATCH] =?UTF-8?q?test(state):=20#133=20=E2=80=94=20pin=20the=20#?= =?UTF-8?q?107=20legacy-name=20shim=20(and=20its=20fail-closed=20gate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mkFlake prefers state.json and still accepts the legacy theme-state.json, because a machine only migrates on its next menu write. Nothing evaluated a checkout carrying ONLY the legacy name, so the shim could rot with every check green — breaking rebuilds for precisely the users who have not opened the menu since #107, i.e. the ones least likely to see it coming. checks.state-legacy-name evaluates two fixtures: legacy-only must resolve, and no-state-at-all must still throw. The gate is half the shim's contract (it turns "no state file" into one readable error instead of a readFile stack from inside the module system) and was equally unpinned. Cheap by construction: mkFlake wraps its return in `builtins.seq _themeState`, so forcing the attrset forces the state read and nothing else — no module system, no home.nix. Hence a one-file fixture and no measurable cost (flake check still ~35s). That file is a SYMLINK to the shipped template's state.json: a copy would drift and the check would quietly start testing a fossil. The fixture also asserts it has no state.json, or it would pass while testing nothing. Both assertions proven by breaking lib.nix — deleting the fallback, then defeating the gate — and watching each fail by name. Delete this check together with the shim and the nomarchy-theme-sync alias. Co-Authored-By: Claude Opus 4.8 --- agent/BACKLOG.md | 12 ----- checks/fixtures/legacy-state/theme-state.json | 1 + checks/fixtures/no-state/README.md | 5 +++ docs/ROADMAP.md | 16 +++++++ flake.nix | 45 +++++++++++++++++++ 5 files changed, 67 insertions(+), 12 deletions(-) create mode 120000 checks/fixtures/legacy-state/theme-state.json create mode 100644 checks/fixtures/no-state/README.md diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 7db4111..d40ccea 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -153,18 +153,6 @@ whether Nomarchy holds itself higher — darker label, or a darker accent mix for light palettes — and note `tools/check-theme-contrast.py` covers palette pairings only, never GTK widget surfaces. `[human]` for the aesthetic call. -### 133. No check pins the #107 legacy-name compat shim - -`lib.mkFlake`, doctor and lifecycle all still accept `theme-state.json`, and -`nomarchy-state-sync` migrates it on write — but nothing in `checks.*` -evaluates a checkout that has *only* the legacy name, so the shim can rot -silently while every check stays green. It is load-bearing until every -existing machine has taken one menu write. Add a cheap eval check: a fixture -dir with `theme-state.json` only → `mkFlake` resolves it; plus the write-side -migration (state.json created + git-staged, legacy `git rm`'d). Proven by -hand 2026-07-15 (drvPaths identical across the rename) — this just keeps it -proven. Delete together with the shim + the `nomarchy-theme-sync` alias. - ### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry Bernardo 2026-07-16, and his repro is exact: open the app menu, type `ca`, move diff --git a/checks/fixtures/legacy-state/theme-state.json b/checks/fixtures/legacy-state/theme-state.json new file mode 120000 index 0000000..3f8b7a9 --- /dev/null +++ b/checks/fixtures/legacy-state/theme-state.json @@ -0,0 +1 @@ +../../../templates/downstream/state.json \ No newline at end of file diff --git a/checks/fixtures/no-state/README.md b/checks/fixtures/no-state/README.md new file mode 100644 index 0000000..6fcd9ac --- /dev/null +++ b/checks/fixtures/no-state/README.md @@ -0,0 +1,5 @@ +Deliberately empty: a checkout with **neither** `state.json` nor the legacy +`theme-state.json`. `checks.state-legacy-name` asserts `mkFlake` fails closed +here — the fail-closed gate in `lib.nix` is what turns "you have no state file" +into one readable error instead of a raw `readFile` stack from deep inside the +module system. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3ff373f..43602f5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -555,6 +555,22 @@ Design/decision records and a running log of shipped work (items marked `%26`, `%3F`, `%23`, `caf%C3%A9`. **Unverified on purpose:** whether each vendor's page fills the box or auto-submits — testing that means posting into Bernardo's own accounts. +- ✓ **The #107 compat shim is pinned (#133, 2026-07-16):** `mkFlake` prefers + `state.json` and still accepts the legacy `theme-state.json`, because a + machine only migrates on its next menu write — but nothing evaluated a + checkout carrying **only** the legacy name, so the shim could rot with every + check green, breaking rebuilds for exactly the users who have not touched the + menu since #107 (i.e. the least likely to notice). `checks.state-legacy-name` + now evaluates two fixtures: legacy-only must resolve, and **no state at all + must still throw** — the fail-closed gate is half the shim's contract and was + equally unpinned. Cheap by construction: `mkFlake` wraps its return in + `builtins.seq _themeState`, so forcing the attrset forces the state read and + nothing else — no module system, no `home.nix` — which is why the fixture is + one file and the check costs no measurable time. That file is a **symlink** to + the shipped template's `state.json`: a copy would drift and the check would + quietly start testing a fossil. Both assertions were proven by breaking + `lib.nix` — deleting the fallback, then defeating the gate — and watching each + fail by name. Delete it with the shim and the `nomarchy-theme-sync` alias. - ✓ **The VM's verdict on menu geometry is worthless — and so is measuring (#132, 2026-07-16):** the guest draws the picker with icons at ~¼ of `ui.iconSize` and rows short enough to clip a 6-row root, so screenshots are diff --git a/flake.nix b/flake.nix index 5fb15a8..a34b8d4 100644 --- a/flake.nix +++ b/flake.nix @@ -1511,6 +1511,51 @@ in assert ok; pkgs.runCommand "nomarchy-rofi-text-width" { } "touch $out"; + # The #107 rename shipped a compat shim: mkFlake prefers state.json + # and still accepts the legacy theme-state.json (lib.nix), because a + # machine only migrates on its next menu write. Nothing evaluated a + # checkout carrying ONLY the legacy name, so the shim could rot with + # every check green — and it is load-bearing for exactly the users who + # have not touched the menu since #107, i.e. the ones least likely to + # notice before their rebuild breaks. + # + # Cheap by construction: mkFlake wraps its return in + # `builtins.seq _themeState`, so forcing the attrset forces the state + # read and nothing else — no module system, no home.nix. Which is also + # why the fixture needs only that one file. + # + # Delete this check together with the shim and the + # `nomarchy-theme-sync` alias. + state-legacy-name = + let + inherit (nixpkgs) lib; + expect = cond: msg: lib.assertMsg cond "state-legacy-name: ${msg}"; + legacy = ./checks/fixtures/legacy-state; + none = ./checks/fixtures/no-state; + evalsOk = src: + (builtins.tryEval + (builtins.isAttrs (self.lib.mkFlake { inherit src; username = "fixture"; }))) + .success; + ok = + # Fixture drift: if someone adds a state.json here the check + # would still pass, while testing nothing at all. + expect (builtins.pathExists (legacy + "/theme-state.json")) + "fixture drift: checks/fixtures/legacy-state has no theme-state.json" + && expect (! builtins.pathExists (legacy + "/state.json")) + ("fixture drift: checks/fixtures/legacy-state also has a state.json, " + + "so it no longer exercises the legacy-only path") + && expect (evalsOk legacy) + ("mkFlake cannot evaluate a checkout carrying only theme-state.json — " + + "the #107 compat shim is broken and every pre-#107 machine " + + "fails to rebuild") + # The other half of the shim's contract: no state at all must + # still fail, and fail here rather than deep in a module. + && expect (! evalsOk none) + ("mkFlake accepted a checkout with no state file at all — the " + + "fail-closed gate in lib.nix is gone"); + in + assert ok; pkgs.runCommand "nomarchy-state-legacy-name" { } "touch $out"; + # Docking artifact guard: default/no-profile Home Manager output # must still launch both hotplug watchers, put Rofi on the focused # monitor, expose the manual keyboard picker, and never ship the