From c853853616d7b6a6ea8d8b9a8e2004be21c640be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:33:53 +0000 Subject: [PATCH 1/4] Initial plan From 38f49e9161ec19667ac849ac292c2e279cff60f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:36:33 +0000 Subject: [PATCH 2/4] fix: group sovran hosts append redirection --- app/tests/test_local_domain_loopback_nix.py | 50 ++++++++++++++++++++- modules/core/local-domain-loopback.nix | 12 ++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/app/tests/test_local_domain_loopback_nix.py b/app/tests/test_local_domain_loopback_nix.py index 365930a..fe79872 100644 --- a/app/tests/test_local_domain_loopback_nix.py +++ b/app/tests/test_local_domain_loopback_nix.py @@ -16,11 +16,14 @@ Verifies that the sovran-hosts-update helper: - does NOT rely on environment.systemPackages for the helper's dependencies. """ +import shutil +import subprocess import unittest from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[2] NIX_FILE = ( - Path(__file__).resolve().parents[2] + REPO_ROOT / "modules" / "core" / "local-domain-loopback.nix" @@ -31,6 +34,14 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): def setUp(self): self.source = NIX_FILE.read_text() + def _helper_script(self) -> str: + start = self.source.index("text = ''") + len("text = ''") + end = self.source.index(" '';", start) + return "\n".join( + line[6:] if line.startswith(" ") else line + for line in self.source[start:end].splitlines() + ).lstrip("\n") + # ── writeShellApplication and explicit runtimeInputs ──────────────────── def test_uses_write_shell_application(self): @@ -120,6 +131,43 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): self.assertIn("skip=1", self.source) self.assertIn("skip=0", self.source) + def test_managed_block_uses_grouped_append_redirect(self): + self.assertIn('} >> "$TMP"', self.source) + self.assertEqual(self.source.count('>> "$TMP"'), 1) + + def test_helper_script_passes_shellcheck(self): + shellcheck = shutil.which("shellcheck") + if shellcheck is None: + self.skipTest("shellcheck is not installed") + proc = subprocess.run( + [shellcheck, "-s", "bash", "-"], + input=self._helper_script(), + text=True, + capture_output=True, + check=False, + ) + output = proc.stdout + proc.stderr + self.assertEqual(proc.returncode, 0, output) + self.assertNotIn("SC2129", output) + + def test_helper_derivation_builds_when_nix_available(self): + nix = shutil.which("nix") + if nix is None: + self.skipTest("nix is not installed") + proc = subprocess.run( + [ + nix, + "build", + '.#nixosConfigurations.nixos.config.environment.etc."sovran-hosts-update.sh".source', + "--no-link", + ], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + # ── Activation script warns on failure rather than silently swallowing ─── def test_activation_script_emits_warning_on_failure(self): diff --git a/modules/core/local-domain-loopback.nix b/modules/core/local-domain-loopback.nix index a49f684..89adfed 100644 --- a/modules/core/local-domain-loopback.nix +++ b/modules/core/local-domain-loopback.nix @@ -107,11 +107,13 @@ let # ── Step 4: append the Sovran block if there are any entries ────────── if [ -n "$ENTRIES" ]; then - printf '\n%s\n' "$BEGIN_MARKER" >> "$TMP" - printf '%s\n' "# These entries route configured service domains to local Caddy." >> "$TMP" - printf '%s\n' "# They are managed automatically — do not edit this block." >> "$TMP" - printf '%s\n' "$ENTRIES" >> "$TMP" - printf '%s\n' "$END_MARKER" >> "$TMP" + { + printf '\n%s\n' "$BEGIN_MARKER" + printf '%s\n' "# These entries route configured service domains to local Caddy." + printf '%s\n' "# They are managed automatically — do not edit this block." + printf '%s\n' "$ENTRIES" + printf '%s\n' "$END_MARKER" + } >> "$TMP" fi # ── Step 5: atomically replace /etc/hosts ───────────────────────────── From ec4c1c851be8cf8b1d7461530515ea1cae256889 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:37:48 +0000 Subject: [PATCH 3/4] test: derive nix helper build attr from flake --- app/tests/test_local_domain_loopback_nix.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/tests/test_local_domain_loopback_nix.py b/app/tests/test_local_domain_loopback_nix.py index fe79872..3b13958 100644 --- a/app/tests/test_local_domain_loopback_nix.py +++ b/app/tests/test_local_domain_loopback_nix.py @@ -16,12 +16,23 @@ Verifies that the sovran-hosts-update helper: - does NOT rely on environment.systemPackages for the helper's dependencies. """ +import re import shutil import subprocess import unittest from pathlib import Path +NIX_STRING_INDENT = 6 REPO_ROOT = Path(__file__).resolve().parents[2] +FLAKE_SOURCE = (REPO_ROOT / "flake.nix").read_text() +PRIMARY_NIXOS_CONFIGURATION = re.search( + r"nixosConfigurations\.([A-Za-z0-9_-]+)\s*=", + FLAKE_SOURCE, +).group(1) +HELPER_BUILD_ATTR = ( + f'.#nixosConfigurations.{PRIMARY_NIXOS_CONFIGURATION}.config.environment.etc.' + '"sovran-hosts-update.sh".source' +) NIX_FILE = ( REPO_ROOT / "modules" @@ -38,7 +49,9 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): start = self.source.index("text = ''") + len("text = ''") end = self.source.index(" '';", start) return "\n".join( - line[6:] if line.startswith(" ") else line + line[NIX_STRING_INDENT:] + if line.startswith(" " * NIX_STRING_INDENT) + else line for line in self.source[start:end].splitlines() ).lstrip("\n") @@ -158,7 +171,7 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): [ nix, "build", - '.#nixosConfigurations.nixos.config.environment.etc."sovran-hosts-update.sh".source', + HELPER_BUILD_ATTR, "--no-link", ], cwd=REPO_ROOT, From 92cf41776001ef540fd6e18cf3dc66458690ad1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:38:48 +0000 Subject: [PATCH 4/4] test: harden flake configuration detection --- app/tests/test_local_domain_loopback_nix.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/tests/test_local_domain_loopback_nix.py b/app/tests/test_local_domain_loopback_nix.py index 3b13958..72d2a12 100644 --- a/app/tests/test_local_domain_loopback_nix.py +++ b/app/tests/test_local_domain_loopback_nix.py @@ -25,10 +25,13 @@ from pathlib import Path NIX_STRING_INDENT = 6 REPO_ROOT = Path(__file__).resolve().parents[2] FLAKE_SOURCE = (REPO_ROOT / "flake.nix").read_text() -PRIMARY_NIXOS_CONFIGURATION = re.search( +PRIMARY_NIXOS_CONFIGURATION_MATCH = re.search( r"nixosConfigurations\.([A-Za-z0-9_-]+)\s*=", FLAKE_SOURCE, -).group(1) +) +if PRIMARY_NIXOS_CONFIGURATION_MATCH is None: + raise RuntimeError("Could not determine the primary nixosConfigurations entry from flake.nix") +PRIMARY_NIXOS_CONFIGURATION = PRIMARY_NIXOS_CONFIGURATION_MATCH.group(1) HELPER_BUILD_ATTR = ( f'.#nixosConfigurations.{PRIMARY_NIXOS_CONFIGURATION}.config.environment.etc.' '"sovran-hosts-update.sh".source' @@ -161,7 +164,6 @@ class LocalDomainLoopbackNixStructureTests(unittest.TestCase): ) output = proc.stdout + proc.stderr self.assertEqual(proc.returncode, 0, output) - self.assertNotIn("SC2129", output) def test_helper_derivation_builds_when_nix_available(self): nix = shutil.which("nix")