diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-10 10:49:27 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-10 10:49:27 +0200 |
| commit | ae1b825b458d14cc1fa251a56331767d5fcf4ec6 (patch) | |
| tree | 9d6b53a64ee3a987f5f8545c5429491fc4535dec | |
| parent | 37d723f2ffd55dcca141831ee2368b5b49618de8 (diff) | |
| download | abusectl-ae1b825b458d14cc1fa251a56331767d5fcf4ec6.tar.gz abusectl-ae1b825b458d14cc1fa251a56331767d5fcf4ec6.zip | |
fix: stop init duplicating its own preamble on every rewrite
The carry-across preserved every line before the first [table], but that
text is build()'s own header, not the user's. A second init emitted it
twice and a third three times, with [general] missing from the tail half,
so the file grew a copy per run. It still parsed, which is why the suite
stayed green and only a hand test found it.
Preserving from the first table onward fixes the trailing "Later parts"
block too: it trails whichever table came last, so it is already inside a
skipped section. A separate guard for it passed its own mutation check
and was removed as redundant.
Required prompts now say so. The relay question and the hop picker cannot
be skipped, because with no trust boundary parse refuses and init is the
route out, but they read like the reporter questions that do offer a skip.
The cases prompt says "Enter for <default>" rather than showing the
default in brackets, which was the same ambiguity in the other direction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LByBnw83xr9YP85nskzkyE
| -rw-r--r-- | abusectl/cli.py | 6 | ||||
| -rw-r--r-- | abusectl/init.py | 6 | ||||
| -rw-r--r-- | tests/test_init.py | 27 |
3 files changed, 35 insertions, 4 deletions
diff --git a/abusectl/cli.py b/abusectl/cli.py index 36a12ec..3065c34 100644 --- a/abusectl/cli.py +++ b/abusectl/cli.py @@ -128,7 +128,7 @@ def _pick_hops(hops: list[str]) -> list[str]: while True: picks = _ask( - "\nWhich of these are YOUR OWN servers? (comma-separated numbers): " + "\nWhich of these are YOUR OWN servers? (comma-separated numbers, required): " ) chosen = [] for token in picks.split(","): @@ -164,7 +164,7 @@ def _ask_relays() -> list[str]: while True: answer = _ask( - f"Your relays as CIDRs, or a provider name ({known}): " + f"Your relays as CIDRs, or a provider name, required ({known}): " ) if not answer: print(" Needed: a CIDR such as 192.0.2.0/24, or a provider name.") @@ -256,7 +256,7 @@ def _prompt_answers(sample: Path | None) -> dict: else: trusted_relays = _ask_relays() - cases = _ask(f"\nCases directory [{config.DEFAULT_CASES}]: ") + cases = _ask(f"\nCases directory (Enter for {config.DEFAULT_CASES}): ") return {"trusted_relays": trusted_relays, "cases": cases, **_ask_reporter()} diff --git a/abusectl/init.py b/abusectl/init.py index 626dd41..27a4385 100644 --- a/abusectl/init.py +++ b/abusectl/init.py @@ -306,7 +306,11 @@ def _preserved_sections(path: pathlib.Path, built: set) -> str: lines = text.splitlines(keepends=True) kept = [] - skipping = False + # Skip until the first table: text before it is build()'s own preamble, + # which this run has already rendered. Carrying it duplicated the header + # on every re-init, and a comment block trailing the last table rode + # along with it. + skipping = True for line in lines: stripped = line.strip() if stripped.startswith("[") and stripped.endswith("]"): diff --git a/tests/test_init.py b/tests/test_init.py index 46b1a26..4f2042c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -321,3 +321,30 @@ class TestWriteGuard(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class TestRepeatedInit(unittest.TestCase): + """build()'s own preamble and footer must not accrete across rewrites. + + Found by hand test, not by the suite: a single rewrite looks fine, and + the file still parses, so only the third run makes it obvious. The + carried text is build()'s, not the user's, so preserving it duplicated + the header once per run. + """ + + def test_the_preamble_and_footer_survive_four_rewrites_exactly_once(self): + answers = {"trusted_relays": ["192.0.2.0/24"], "cases": ""} + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "config.toml" + init.write(path, answers) + path.write_text( + path.read_text() + '\n[misp]\napi_key = "SECRET"\n' + ) + for _ in range(3): + init.write(path, answers, force=True) + + text = path.read_text() + self.assertEqual(text.count("# abusectl configuration."), 1) + self.assertEqual(text.count("# Later parts of abusectl"), 1) + # The unknown section still rides across untouched. + self.assertEqual(tomllib.loads(text)["misp"]["api_key"], "SECRET") |
