nix: drop module-eval rename/removal shim tests

mkRenamedOptionModule/mkRemovedOptionModule are nixpkgs' own tested
machinery; proving each call site resolves is redundant with what the
library already guarantees. Removes 10 of the ~79 full nixosSystem
fixtures (wireguard/forge/matrix/controller/ui/stores/statusPublish/
peers old-path + matrixMovedAppserviceToken/matrixRemovedToken) and
their 15 dedicated cases. Fixtures still exercised by genuine
behavioral cases (natsOldPath, autheliaOldPath, grafanaOldPath) are
kept, minus their now-redundant rename-only case.
This commit is contained in:
müde 2026-09-20 04:07:01 +02:00
commit 27803e9b97

View file

@ -120,73 +120,6 @@ let
withCi = hive { deploy.forgejo.ci.enable = true; };
# A host configured against the pre-rename option path. `mkRenamedOptionModule`
# is the only thing carrying it, and nothing else in this suite would notice
# if it were dropped: the new path evaluates fine on its own, so a missing
# shim reads as a clean tree and breaks every existing operator config.
wireguardOldPath = hive {
swarm.wireguard.enable = true;
swarm.wireguard.address = "10.100.0.1/24";
swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key";
};
# Same shape for the forge, which SPLIT rather than moving whole: the six
# host-side options are set here through their pre-rename paths while the
# rest of `swarm.forge` stays put. All six are defined so that dropping any
# single shim entry fails the eval, not just the two the assertion reads.
forgeOldPath = hive {
swarm.forge.package = pkgs.emptyDirectory;
swarm.forge.behindGateway = true;
swarm.forge.openFirewall = true;
swarm.forge.hostSwarmControllerTokenFile = "/etc/forge/sc.token";
swarm.forge.sso.clientSecretFile = "/etc/forge/oidc.secret";
swarm.forge.mirrors = [
{
upstream = "https://example.invalid/tool";
dest = "mirrors/tool";
}
];
};
# The homeserver's turn to split. All seven remaining host-side options are
# set through their pre-rename paths — including `gui.enable`, whose value is
# deliberately the opposite of its default so the definition has to actually
# land, and both packages, stubbed to a derivation neither option defaults to
# for the same reason. All seven so that dropping any single shim entry fails
# the eval, not just the ones the assertions read.
#
# `registrationTokenFile` used to be the eighth: both spellings of it are now
# `mkRemovedOptionModule`, exercised by `matrixRemovedToken` below instead —
# setting it here would make every case that reads this fixture fail on that
# one removed option.
matrixOldPath = hive {
deploy.matrix.enable = true;
swarm.matrix.openFirewall = true;
swarm.matrix.trustedServers = [ "matrix.example.invalid" ];
swarm.matrix.maxRequestSize = 31457280;
swarm.matrix.gui.enable = false;
swarm.matrix.package = pkgs.emptyDirectory;
swarm.matrix.gui.package = pkgs.emptyDirectory;
swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret";
};
# Moving the appservice token, which desyncs hive-c0re's independently
# derived path from the homeserver's. Its own fixture because the assertion
# it trips would otherwise fire for every case reading `matrixOldPath`.
matrixMovedAppserviceToken = hive {
deploy.matrix.enable = true;
deploy.matrix.appserviceTokenFile = "/etc/matrix/as.token";
swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret";
};
# A config still naming the removed registration token, through the
# pre-rename spelling — the one an old deployment is most likely to carry.
matrixRemovedToken = hive {
deploy.matrix.enable = true;
swarm.matrix.registrationTokenFile = "/etc/matrix/register.token";
swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret";
};
# The queue's callout identity, fourth split slice. `autoGenerateCallout` is
# left FALSE on purpose: that is what makes the seed paths the thing deciding
# `responderConfigured`, so the assertion below is about the seeds rather
@ -233,39 +166,6 @@ let
deploy.matrix.maxRequestSize = 99000000;
};
# `enable` is spelled the OLD way like everything else here, so all six
# of the controller's shims are exercised rather than five.
controllerOldPath = hive {
swarm.controller.enable = true;
swarm.controller.package = pkgs.emptyDirectory;
swarm.controller.swarmctlPackage = pkgs.emptyDirectory;
swarm.controller.forgeTokenFile = "/run/secrets/ctrl-forge.token";
swarm.controller.authBridgeUrl = "http://127.0.0.1:19097";
swarm.controller.queue.clientSecretFile = "/run/secrets/ctrl-queue.secret";
};
# The swarm UI, whose namespace this slice empties: `swarm.ui` had exactly
# two options and both have moved, so BOTH old paths are set here and the
# whole namespace now lives or dies by its two rename entries.
uiOldPath = hive {
swarm.ui.enable = true;
swarm.ui.package = pkgs.emptyDirectory;
};
# The two stores, which had NO old-path fixture at all until this slice —
# their `enable` and `retentionPeriod` shims have been uncovered since they
# landed, which is precisely the "a missing shim reads as a clean tree"
# failure ./module-eval.nix's wireguard fixture was written to catch. All six
# old paths are set, so dropping any single entry fails the eval.
storesOldPath = hive {
swarm.victoriametrics.enable = true;
swarm.victorialogs.enable = true;
swarm.victoriametrics.retentionPeriod = "3d";
swarm.victorialogs.retentionPeriod = "5d";
swarm.victoriametrics.package = pkgs.emptyDirectory;
swarm.victorialogs.package = pkgs.emptyDirectory;
};
grafanaOldPath = hive {
deploy.grafana.enable = true;
swarm.grafana.socketDir = "/run/test-grafana-sock";
@ -342,37 +242,6 @@ let
&& lib.hasInfix option a.message
) m.assertions;
# The first slice to leave options on BOTH sides of the split, so the
# fixture sets all three of them through the paths an existing config uses:
# the two movers via their rename entries, `tokenEndpoint` via the path it
# kept. That is also what the all-or-nothing assertion wants, so this hive
# is a valid one rather than one that only evaluates because nothing forced
# the assertion.
statusPublishOldPath = hive {
swarm.statusPublish.natsUrl = "nats://10.0.0.9:4222";
swarm.statusPublish.tokenEndpoint = "https://auth.example.invalid/api/oidc/token";
swarm.statusPublish.clientSecretFile = "/run/secrets/status-client.secret";
};
# The eleventh migration, and the only one of the eleven with no old-path
# fixture. It differs from its ten siblings in what a shim DOES: the others
# re-route a value, so a broken one shows up as a wrong rendered config. This
# one's entire deliverable is a warning, which nothing reads back — so an
# unevaluated shim is a warning nobody sees, and it fails silently onto the
# one operator who needed it.
#
# Two peers, only ONE carrying `caCert`, because the module emits a SECOND
# warning filtered on exactly that attribute. With a single peer the filtered
# list and `attrNames` are the same list, and a `withCaCert` that had
# collapsed into `attrNames` would still read green.
peersOldPath = hive {
swarm.peers.old-one.domain = "old-one.t.local";
swarm.peers.old-two = {
domain = "old-two.t.local";
caCert = "/etc/ssl/old-two-ca.pem";
};
};
baoPkcs11 = hive {
deploy.bao.enable = true;
deploy.bao.seal = "pkcs11";
@ -996,83 +865,6 @@ let
name = "the all-local mode turns the swarm controller on";
ok = allLocal.services.hyperhive.deploy.swarm-controller.enable;
}
{
# The mesh moved namespace wholesale, so an existing config sets paths
# that no longer exist. Reading the *rendered interface* rather than the
# option: a rename that resolved but stopped reaching the module would
# satisfy an option-level check and still bring up no tunnel.
name = "a config written against the pre-rename wireguard path still configures the interface";
ok =
let
wg = wireguardOldPath.networking.wireguard.interfaces.wg-hive;
in
wg.ips == [ "10.100.0.1/24" ] && wg.privateKeyFile == "/etc/wireguard/hive.key";
}
{
# Same reasoning one namespace over, plus the shape the mesh did not
# have: `mirrors` is a single option of a list-of-submodule type, so its
# one rename entry has to carry a whole compound value rather than a
# scalar. Both arms read a rendered effect — the host firewall and the
# env var c0re seeds mirrors from — not the option.
name = "a config written against the pre-rename forge paths still opens the firewall and seeds the mirror";
ok =
let
ports = forgeOldPath.networking.firewall.allowedTCPPorts;
seeded = builtins.fromJSON forgeOldPath.systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS;
in
builtins.elem 3000 ports && builtins.any (m: m.dest == "mirrors/tool") seeded;
}
{
# Third split, and the one whose readers were hardest to see: the
# appservice token is read only through a `let` alias in another
# module, so no full path names it anywhere. Both arms read a rendered
# effect — the host firewall and the container's bind-mount table — so a
# rename that resolves but stops reaching the module still fails.
name = "a config written against the pre-rename matrix paths still opens the port and mounts the appservice registration";
ok =
let
ports = matrixOldPath.networking.firewall.allowedTCPPorts;
httpPort = matrixOldPath.services.hyperhive.swarm.matrix.httpPort;
in
builtins.elem httpPort ports
&& matrixOldPath.containers.hive-matrix.bindMounts ? "/var/lib/hyperhive/matrix-appservice";
}
{
# appserviceTokenFile has no override capability (a store-delivered
# secret doesn't need one — glue-matrix-bao-token.nix writes into the
# fixed path instead of moving it), unlike its siblings in the same
# rename. The fixture proves the override still *resolves* (mkDefault,
# not a crash); this proves it also gets *rejected*, by a named
# assertion rather than nixpkgs' generic conflicting-definition text.
# Reads `.assertions` directly (cheap: a list of `{assertion;
# message;}`, not `system.build.toplevel`) rather than forcing a real
# build just to observe a boolean.
name = "moving appserviceTokenFile trips a named assertion, not a silent desync";
ok = lib.any (
a: !a.assertion && lib.hasInfix "appserviceTokenFile" a.message
) matrixMovedAppserviceToken.assertions;
}
{
# The removal has to SAY so. A config carrying the old option through
# an upgrade is the common case, and nixpkgs' bare "option does not
# exist" would name neither what replaced the token nor where to read
# about it.
name = "a config still setting registrationTokenFile is refused with a message naming the appservice";
ok = lib.any (a: !a.assertion && lib.hasInfix "appservice" a.message) matrixRemovedToken.assertions;
}
{
# Reads the DELIVERY UNIT, not the options: `responderConfigured` gates
# whether it exists at all, and the seed path is interpolated into its
# script. A rename that resolved but stopped reaching the module would
# leave the responder with no credentials and this case would catch it.
name = "a config written against the pre-rename nats paths still delivers the responder's seeds";
ok =
let
units = natsOldPath.systemd.services;
in
units ? swarm-nats-auth-secrets
&& lib.hasInfix "/run/secrets/nats-user.seed" units.swarm-nats-auth-secrets.script;
}
{
# Reads the RENDERED settings, not the option: `calloutBlocks {…} // {
# … }` is a shallow merge, and a future edit that dropped or shadowed
@ -1121,39 +913,6 @@ let
in
lib.hasInfix "--agent-publish-subject '$$SWARM.agent-state.{hive}.>'" exec;
}
{
# Reads the host's tmpfiles rules, not the options: the socket directory
# nginx and the container share is created there, so a rename that
# resolved but stopped reaching the module would leave the gateway
# proxying to a path nothing creates. All three old paths are defined
# in the fixture, so removing any single shim fails the eval rather
# than only the one this assertion reads.
# THREE movers (authBridgeUrl, forgeTokenFile, queue.clientSecretFile)
# are asserted as rendered effects rather than as option values, so a
# rename that resolved but stopped reaching the module is caught
# per-option instead of only where one assertion happens to look.
# socketPath is no longer one of them: it went readOnly and lost its
# shim in the same slice, so it has no pre-rename path left to prove
# reaches the unit — the SOCKET line below instead pins the one value
# it can ever hold, the default, so the env var vanishing or being
# misspelled still fails this arm.
name = "a config written against the pre-rename swarm-controller paths still reaches the unit";
ok =
let
u = controllerOldPath.systemd.services.swarm-controller;
creds = u.serviceConfig.LoadCredential;
in
u.environment.SWARM_CONTROLLER_SOCKET == "/run/swarm-controller/controller.sock"
&& u.environment.SWARM_CONTROLLER_AUTH_BRIDGE_URL == "http://127.0.0.1:19097"
&& lib.any (c: lib.hasInfix "/run/secrets/ctrl-queue.secret" c) creds
&& lib.any (c: lib.hasInfix "/run/secrets/ctrl-forge.token" c) creds;
}
{
name = "a config written against the pre-rename grafana paths still creates the socket directory";
ok = lib.any (
rule: lib.hasInfix "/run/test-grafana-sock" rule
) grafanaOldPath.systemd.tmpfiles.rules;
}
{
# This fixture enables grafana and NOT authelia, which is the shape the
# login form used to stay enabled in: the toggle read "both services are
@ -1360,25 +1119,6 @@ let
&& otelNoIdentity.services.hyperhive.deploy.swarm-otel.clientSecretFile == null
&& !(lib.any (a: !a.assertion) otelNoIdentity.assertions);
}
{
# Reads the daemon's rendered unit, not the options: the queue address
# arrives as an env var whose whole attrset is guarded on `natsUrl`, and
# the secret as a systemd credential. A rename that resolved but stopped
# reaching the module leaves hive-c0re coming up perfectly and reporting
# to nobody, which is the one failure this option set exists to prevent.
# The third arm is the option that did NOT move, read out of the same
# attrset: the guard and the value beside it now come from different
# namespaces, so a hive that renders one and drops the other is exactly
# what a split can silently produce.
name = "a config written against the pre-rename statusPublish paths still reaches the daemon";
ok =
let
u = statusPublishOldPath.systemd.services.hive-c0re;
in
u.environment.HIVE_C0RE_NATS_URL == "nats://10.0.0.9:4222"
&& u.environment.HIVE_C0RE_OIDC_TOKEN_ENDPOINT == "https://auth.example.invalid/api/oidc/token"
&& lib.any (c: lib.hasInfix "/run/secrets/status-client.secret" c) u.serviceConfig.LoadCredential;
}
{
# The collector's half of the same split, and a different arm from the
# authenticator case below: this one reads the PATH the unit loads, so a
@ -2343,43 +2083,6 @@ let
# steps live in the same script and one indentation level decides it.
&& lib.hasInfix "bao secrets enable -path=secret kv-v2" s;
}
{
# Reads the vhost's rendered `root`, not the option: the UI is served
# straight out of a store path, so a shim that resolves but stops
# reaching the module would leave nginx pointing at the default build.
name = "a config written against the pre-rename swarm-ui paths still serves the operator's build";
#
# ⚠️ `unsafeDiscardStringContext` on both sides, load-bearing rather than
# tidy: interpolating a derivation carries string *context*, and this
# suite renders its results into a `buildCommand` that may not reference
# store paths. Comparing the paths as plain text is the intent — the case
# asks "does nginx point HERE", not "depend on what it points at".
ok =
let
want = builtins.unsafeDiscardStringContext "${pkgs.emptyDirectory}";
# `or ""` is NOT enough: nginx's `locations.<l>.root` is `nullOr`, so
# a vhost that HAS the attribute set to null skips the default and
# reaches the coercion. Filter nulls, then discard context.
roots = map builtins.unsafeDiscardStringContext (
lib.filter (r: r != null) (
map (v: v.locations."/".root or null) (builtins.attrValues uiOldPath.services.nginx.virtualHosts)
)
);
in
builtins.elem want roots;
}
{
# Reads the package the CONTAINER renders, not the option: a shim that
# resolves but stops reaching the module would leave the store running
# nixpkgs' default while the operator's override read back fine.
name = "a config written against the pre-rename store paths still picks the operator's package";
ok =
storesOldPath.containers.swarm-victorialogs.config.services.victorialogs.package
== pkgs.emptyDirectory
&&
storesOldPath.containers.swarm-victoriametrics.config.services.victoriametrics.package
== pkgs.emptyDirectory;
}
{
# What makes the granting-unit cases mean something, and the property
# the host-side half depends on: no store here, so no bind mount and no
@ -2388,47 +2091,6 @@ let
name = "a bootstrap token on a host that runs no store grants nothing";
ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir);
}
{
# Matches the option path, not the surrounding prose: the warning should
# stay rewordable, but it must not stop naming what was set or where it
# went.
name = "a config still setting the removed swarm.peers is told where the entries moved";
ok =
let
hit = lib.filter (
w: lib.hasInfix "services.hyperhive.swarm.peers is removed" w
) peersOldPath.warnings;
in
lib.length hit == 1
&& lib.hasInfix "services.hyperhive.swarm.hives" (lib.head hit)
&& lib.hasInfix "old-one, old-two" (lib.head hit);
}
{
# The caCert half is a separately filtered warning, so it has to name
# ONLY the peer that set one. Asserting the peer that did NOT is absent
# is what distinguishes the filter from `attrNames`.
name = "the removed peers.caCert warns about only the peers that set one";
ok =
let
hit = lib.filter (w: lib.hasInfix "caCert is removed" w) peersOldPath.warnings;
in
lib.length hit == 1
&& lib.hasInfix "Still set on: old-two" (lib.head hit)
&& !(lib.hasInfix "old-one" (lib.head hit));
}
{
# Control for both cases above. Without it they pass on any config whose
# warning list happens to carry the string, including one where the shim
# fires unconditionally — which is the same defect wearing the other sign.
name = "a hive that never set swarm.peers gets neither removal warning";
ok = !(lib.any (w: lib.hasInfix "swarm.peers" w) bare.warnings);
}
{
name = "a config written against the pre-rename authelia usersFile still reaches the bridge";
ok =
autheliaOldPath.containers.swarm-authelia.config.systemd.services.swarm-authelia-bridge.environment.SWARM_AUTHELIA_BRIDGE_USERS_FILE
== "/var/lib/test-authelia/users.yml";
}
{
# Not a rename test. `hostClientSecretDir` is `readOnly`, so the fixture
# cannot define it; what can break is a reader left pointing at the