From 65778592a6a0fc4dc5fa12fa3807dfdbfe3b7348 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:25:59 +0200 Subject: [PATCH 1/2] fix: reject duplicate bundle components Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/bundler/models/manifest.py | 8 ++++++++ tests/contract/test_manifest_schema.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/specify_cli/bundler/models/manifest.py b/src/specify_cli/bundler/models/manifest.py index 39684b2327..4d52757c43 100644 --- a/src/specify_cli/bundler/models/manifest.py +++ b/src/specify_cli/bundler/models/manifest.py @@ -192,9 +192,17 @@ def structural_errors(self) -> list[str]: "(lowercase letters, digits, '.', '_', '-'; no path separators)." ) + seen_components: set[tuple[str, str]] = set() for ref in self.components: if not ref.id: errors.append(f"A {ref.kind[:-1]} entry is missing its 'id'.") + key = (ref.kind, ref.id) + if ref.id and key in seen_components: + errors.append( + f"Duplicate {ref.kind[:-1]} '{ref.id}' in " + f"'provides.{ref.kind}'." + ) + seen_components.add(key) if ref.kind != "steps" and not ref.version: errors.append( f"{ref.kind[:-1]} '{ref.id or ''}' must be pinned to a 'version'." diff --git a/tests/contract/test_manifest_schema.py b/tests/contract/test_manifest_schema.py index 4784bdf462..cfad9dae77 100644 --- a/tests/contract/test_manifest_schema.py +++ b/tests/contract/test_manifest_schema.py @@ -127,6 +127,20 @@ def test_components_property_orders_by_kind(): assert kinds == ["extensions", "presets", "steps", "workflows"] +def test_duplicate_component_in_same_kind_is_rejected(): + data = valid_manifest_dict() + data["provides"]["extensions"].append( + {"id": "ext-a", "version": "9.9.9"} + ) + + errors = BundleManifest.from_dict(data).structural_errors() + + assert any( + "duplicate extension 'ext-a'" in error.lower() + for error in errors + ) + + def test_string_tags_rejected_not_split_per_character(): # A bare string would otherwise be iterated character-by-character; the # schema requires a list of strings. From 8376e332de57593ce9bedbb4ff5991bb5da37039 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:42:25 +0200 Subject: [PATCH 2/2] test: allow duplicate IDs across component kinds Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/contract/test_manifest_schema.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/contract/test_manifest_schema.py b/tests/contract/test_manifest_schema.py index cfad9dae77..6006540495 100644 --- a/tests/contract/test_manifest_schema.py +++ b/tests/contract/test_manifest_schema.py @@ -141,6 +141,15 @@ def test_duplicate_component_in_same_kind_is_rejected(): ) +def test_same_component_id_in_different_kinds_is_allowed(): + data = valid_manifest_dict() + data["provides"]["steps"].append({"id": "ext-a"}) + + errors = BundleManifest.from_dict(data).structural_errors() + + assert not any("duplicate" in error.lower() for error in errors) + + def test_string_tags_rejected_not_split_per_character(): # A bare string would otherwise be iterated character-by-character; the # schema requires a list of strings.