fix(nixos): two toggles that reported success and did nothing — wire the state bridges
All checks were successful
Check / eval (push) Successful in 3m16s
All checks were successful
Check / eval (push) Successful in 3m16s
BACKLOG #116. `nomarchy.settings` is declared in exactly one place —
modules/home/options.nix:412, the Home Manager side. On NixOS the attribute
does not exist, and `or <fallback>` swallows the missing-attribute error, so
four options that "defaulted from the state" had silently been their fallback
on every machine ever built.
The item said three options, and called them benign. Both halves were wrong,
and re-grepping rather than trusting the account is what found it:
* There were four. The original enumeration read options.nix instead of
modules/nixos/ and missed services.nix's printing.enable — the same
mistake in miniature as the bug it was filing.
* Two were live user-facing bugs. Control Center is shipped
(default.nix:337) and reachable from the menu; its Bluetooth and Printing
toggles wrote settings.{bluetooth,printing}.enable and printed "requires
rebuild", and the rebuild changed nothing. They had never worked.
The fix is one shape, now uniform: the option declares a STATIC default, and
the implementing module reads the state via theme-state-read.nix (fails closed
on bad JSON, unlike greeter.nix's raw fromJSON — also moved onto the reader
here) and mkDefaults it behind `mkIf (state != null)`. An absent key leaves the
option default as the single source of the fallback; a hand-set system.nix
value still pins it. batteryChargeLimit gets no bridge and loses its dead read:
power.nix's oneshot already reads that key with jq at RUNTIME and prefers it
over the baked value, which is why that menu worked all along.
V2. The bug is proved real before/after on the same flipped state: BEFORE,
bluetooth stays true and printing stays false; AFTER, both flip, and a hand-set
value still outranks the state. Nothing in a build fails when a bridge dies, so
the guards are the point — checks.state-bridges asserts 11 eval cases, and
checks.printing-from-state boots a VM whose only input is the state file and
waits for a running cups.service. The guard was itself proved to fail:
re-breaking the bluetooth bridge makes it throw, naming both assertions. A
check that passes whether or not the property holds is worse than no check
(625b7e3). flake check, option-docs, template-sot, downstream-template-*,
installer-safety, hardware-toggles and battery-charge-limit all pass.
No V3: the mechanism is fully proved headlessly. Design record in ROADMAP §
NixOS-side state bridges (#116); new #117 (PROPOSED) for the control-center
toggles still leaving the rebuild to the user.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
106
flake.nix
106
flake.nix
@@ -148,6 +148,22 @@
|
||||
username = "me";
|
||||
hardwareProfile = "framework-13-7040-amd"; # exercises the mapping
|
||||
};
|
||||
|
||||
# theme-state.json fixtures for the state-bridge checks below,
|
||||
# built off the real shipped template so they stay honest about
|
||||
# what a machine actually carries.
|
||||
baseState = builtins.fromJSON
|
||||
(builtins.readFile ./templates/downstream/theme-state.json);
|
||||
stateFileOf = name: overrides: pkgs.writeText name
|
||||
(builtins.toJSON (nixpkgs.lib.recursiveUpdate baseState overrides));
|
||||
# The shipped template carries none of these keys — the
|
||||
# absent-key path every existing machine is on.
|
||||
absentState = stateFileOf "state-absent.json" { };
|
||||
flippedState = stateFileOf "state-flipped.json" {
|
||||
settings.bluetooth.enable = false; # default true → flipped
|
||||
settings.printing.enable = true; # default false → flipped
|
||||
settings.greeter.autoLogin = "me";
|
||||
};
|
||||
in
|
||||
{
|
||||
downstream-template-system =
|
||||
@@ -155,6 +171,96 @@
|
||||
downstream-template-home =
|
||||
downstream.homeConfigurations.me.activationPackage;
|
||||
|
||||
# The NixOS-side state bridges (ROADMAP § state bridges, #116). `nomarchy.settings`
|
||||
# exists ONLY on the Home Manager side, so a NixOS option that
|
||||
# defaulted from `config.nomarchy.settings.… or X` silently got X
|
||||
# forever — and `or` makes a dead bridge look exactly like a live
|
||||
# one, which is why two Control Center toggles wrote JSON nothing
|
||||
# read for as long as they existed. Nothing in a build fails when a
|
||||
# bridge dies, so assert the flip: no VM, pure eval.
|
||||
state-bridges =
|
||||
let
|
||||
configWith = stateFile: extra:
|
||||
(self.nixosConfigurations.nomarchy.extendModules {
|
||||
modules = [
|
||||
({ lib, ... }: lib.mkMerge [
|
||||
{ nomarchy.system.stateFile = lib.mkForce stateFile; }
|
||||
extra
|
||||
])
|
||||
];
|
||||
}).config;
|
||||
|
||||
absent = configWith absentState { };
|
||||
flipped = configWith flippedState { };
|
||||
# A hand-set value in system.nix must still outrank the state,
|
||||
# or the documented "setting it by hand pins it" is a lie.
|
||||
pinned = configWith flippedState {
|
||||
nomarchy.system.bluetooth.enable = true;
|
||||
nomarchy.services.printing.enable = false;
|
||||
nomarchy.system.greeter.autoLogin = null;
|
||||
};
|
||||
|
||||
cases = [
|
||||
{ n = "absent key leaves bluetooth at its default (true)";
|
||||
ok = absent.hardware.bluetooth.enable == true; }
|
||||
{ n = "absent key leaves printing at its default (false)";
|
||||
ok = absent.services.printing.enable == false; }
|
||||
{ n = "absent key leaves autoLogin unset";
|
||||
ok = !(absent.services.greetd.settings ? initial_session); }
|
||||
{ n = "state bluetooth=false reaches hardware.bluetooth";
|
||||
ok = flipped.hardware.bluetooth.enable == false; }
|
||||
{ n = "state bluetooth=false reaches services.blueman";
|
||||
ok = flipped.services.blueman.enable == false; }
|
||||
{ n = "state printing=true reaches services.printing";
|
||||
ok = flipped.services.printing.enable == true; }
|
||||
{ n = "state printing=true reaches services.avahi (mDNS)";
|
||||
ok = flipped.services.avahi.enable == true; }
|
||||
{ n = "state autoLogin reaches greetd initial_session";
|
||||
ok = (flipped.services.greetd.settings.initial_session.user or null) == "me"; }
|
||||
{ n = "hand-set bluetooth outranks the state";
|
||||
ok = pinned.hardware.bluetooth.enable == true; }
|
||||
{ n = "hand-set printing outranks the state";
|
||||
ok = pinned.services.printing.enable == false; }
|
||||
{ n = "hand-set autoLogin outranks the state";
|
||||
ok = !(pinned.services.greetd.settings ? initial_session); }
|
||||
];
|
||||
failed = builtins.filter (c: !c.ok) cases;
|
||||
in
|
||||
if failed != [ ] then
|
||||
throw ''
|
||||
State bridge broken — a settings.* key no longer reaches the config:
|
||||
${nixpkgs.lib.concatMapStringsSep "\n" (c: " ✗ ${c.n}") failed}
|
||||
Read the state via modules/theme-state-read.nix and mkDefault it
|
||||
in the module — `config.nomarchy.settings` does NOT exist on the
|
||||
NixOS side, and `or <fallback>` swallows that silently.
|
||||
See ROADMAP § "NixOS-side state bridges (#116)".''
|
||||
else
|
||||
pkgs.runCommand "nomarchy-state-bridges" { }
|
||||
"touch $out"; # all ${toString (builtins.length cases)} assertions held at eval
|
||||
|
||||
# The bridge, end to end on a booted machine: checks.state-bridges
|
||||
# proves the option flips at eval, this proves a state key alone
|
||||
# reaches a RUNNING service. The node sets no
|
||||
# nomarchy.services.printing.enable anywhere — the only thing
|
||||
# asking for CUPS is settings.printing.enable in the state file,
|
||||
# which is exactly what Control Center's Printing toggle writes and
|
||||
# what nothing read until #116 (ROADMAP § state bridges).
|
||||
printing-from-state = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-printing-from-state";
|
||||
nodes.machine = { ... }: {
|
||||
imports = [ ./modules/nixos/options.nix ./modules/nixos/services.nix ];
|
||||
nomarchy.system.stateFile = flippedState;
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
# CUPS is up because a JSON key said so, and for no other reason.
|
||||
machine.wait_for_unit("cups.service")
|
||||
# mDNS rides along, so a network printer is discoverable —
|
||||
# the half of the toggle's promise that isn't the daemon.
|
||||
machine.wait_for_unit("avahi-daemon.service")
|
||||
'';
|
||||
};
|
||||
|
||||
# Every hex-on-hex text pairing the generated swaync CSS uses
|
||||
# must contrast in EVERY palette — summer-day's body text was
|
||||
# invisible on hardware (item 25: subtext==base there). Cheap
|
||||
|
||||
Reference in New Issue
Block a user