From 9d6923f2258b01a0646364f617f1ea37bd2d2f29 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 17 Aug 2026 20:42:04 -0400 Subject: [PATCH] fix(docs): merge, not overwrite, COMPONENT lines on identity collision collect_components() used dict.update(), which let conf.d silently overwrite functions/ (or vice versa) when the same bare identity appears in both, e.g. functions/auto-pull.fish and conf.d/auto-pull.fish. The runtime guard can only ever look up the bare status current-function/basename string, so both call sites genuinely share one identity and their raw COMPONENT lines must be concatenated, not replaced. --- docs/generate_component_registry.py | 21 +++++++++++++---- docs/verify-manual.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/generate_component_registry.py b/docs/generate_component_registry.py index 712290b..2c0b572 100644 --- a/docs/generate_component_registry.py +++ b/docs/generate_component_registry.py @@ -23,10 +23,23 @@ OUTPUT = REPO / "conf.d" / "__fish_config_op_registry.fish" def collect_components() -> dict[str, list[str]]: - """Gather every `# COMPONENT` header across the whole repo.""" - out = mt.parse_components(REPO / "functions") - out.update(mt.parse_components(REPO / "conf.d")) - out.update(mt.parse_component_file(REPO / "config.fish")) + """Gather every `# COMPONENT` header across the whole repo. + + Concatenates raw component lines when the same identity appears in + more than one source (e.g. functions/auto-pull.fish and + conf.d/auto-pull.fish both self-identify as "auto-pull" at runtime, + since the guard can only ever look up the bare status + current-function/basename string) rather than letting one silently + overwrite the other. + """ + out: dict[str, list[str]] = {} + for source in ( + mt.parse_components(REPO / "functions"), + mt.parse_components(REPO / "conf.d"), + mt.parse_component_file(REPO / "config.fish"), + ): + for identity, lines in source.items(): + out.setdefault(identity, []).extend(lines) return out diff --git a/docs/verify-manual.py b/docs/verify-manual.py index c4067f3..5d6d449 100644 --- a/docs/verify-manual.py +++ b/docs/verify-manual.py @@ -938,6 +938,42 @@ def test_build_registry_keeps_sites_independent(): assert not warnings +def test_collect_components_merges_identity_collisions_across_sources(): + """functions/auto-pull.fish and conf.d/auto-pull.fish both self-identify + as "auto-pull" at runtime -- the guard only ever has the bare + status current-function/basename string to look up with -- so + collect_components must concatenate their raw COMPONENT lines + rather than letting conf.d's entry silently overwrite functions'.""" + import generate_component_registry as gcr + + with tempfile.TemporaryDirectory() as d: + root = Path(d) + (root / "functions").mkdir() + (root / "conf.d").mkdir() + (root / "functions" / "auto-pull.fish").write_text( + "# COMPONENT\n" + "# autoexec/sync\n" + "function auto-pull\n" + "end\n" + ) + (root / "conf.d" / "auto-pull.fish").write_text( + "# COMPONENT\n" + "# autoexec/sync\n" + ) + (root / "config.fish").write_text("") + + orig_repo = gcr.REPO + try: + gcr.REPO = root + got = gcr.collect_components() + finally: + gcr.REPO = orig_repo + + assert got["auto-pull"] == ["autoexec/sync", "autoexec/sync"], ( + f"both sources' tags should survive the merge, not overwrite: {got}" + ) + + def test_render_registry_is_valid_fish_and_round_trips(): """Sourcing render()'s output must leave the two arrays in the exact shape __fish_config_op_registry_lookup expects -- checked via the real