Skip to content

Commit bf4bd5f

Browse files
mnriemCopilot
andcommitted
fix(bundle): reject conflicting catalog entry IDs
Reject declared IDs that disagree with mapping keys before bundle release selection. Cover all four component catalogs, preserve matching or omitted IDs, and order test imports consistently. Assisted-by: GitHub Copilot (model: GPT-6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 182564a commit bf4bd5f

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

‎src/specify_cli/bundles/component_catalog.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ def _matching_entry(entries: dict | list, component: ComponentRef, url: str) ->
5151
f"Invalid {component.kind[:-1]} catalog entry for '{component.id}' "
5252
f"from {url}: expected an object."
5353
)
54+
if "id" in found and found["id"] != component.id:
55+
raise BundlerError(
56+
f"Invalid {component.kind[:-1]} catalog entry for '{component.id}' "
57+
f"from {url}: declared ID {found['id']!r} does not match the catalog key."
58+
)
5459
return {**found, "id": component.id}
5560

5661
seen: set[str] = set()

‎tests/specify_cli/bundles/test_command_list.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from pathlib import Path
66
from unittest.mock import patch # noqa: F401
77

8-
import yaml # noqa: F401
98
import pytest
9+
import yaml # noqa: F401
1010
from typer.testing import CliRunner
1111

1212
from specify_cli import app

‎tests/specify_cli/bundles/test_references.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,49 @@ def fetch(self, source, force_refresh=False):
12191219
assert warnings == []
12201220

12211221

1222+
@pytest.mark.parametrize("kind", ["extensions", "presets", "workflows", "steps"])
1223+
def test_bundle_rejects_conflicting_declared_catalog_id(tmp_path, monkeypatch, kind):
1224+
from specify_cli.extensions import ExtensionCatalog
1225+
from specify_cli.presets import PresetCatalog
1226+
from specify_cli.workflows.catalog import StepCatalog, WorkflowCatalog
1227+
1228+
catalog = {
1229+
"extensions": ExtensionCatalog,
1230+
"presets": PresetCatalog,
1231+
"workflows": WorkflowCatalog,
1232+
"steps": StepCatalog,
1233+
}[kind]
1234+
_mock_catalog(
1235+
monkeypatch, catalog, kind, "requested",
1236+
{**_installable_current(kind), "id": "other"},
1237+
)
1238+
warnings = []
1239+
check = make_reference_checker(tmp_path, allow_network=True, warnings=warnings)
1240+
1241+
problem = check(ComponentRef(
1242+
kind=kind, id="requested", version="1.0.0", source="trusted"
1243+
))
1244+
assert problem is not None and "other" in problem and "requested" in problem
1245+
assert warnings == []
1246+
1247+
1248+
@pytest.mark.parametrize("declared_id", ["requested", None])
1249+
def test_bundle_accepts_matching_or_omitted_catalog_id(tmp_path, monkeypatch, declared_id):
1250+
from specify_cli.extensions import ExtensionCatalog
1251+
1252+
record = _installable_current("extensions")
1253+
if declared_id is not None:
1254+
record["id"] = declared_id
1255+
_mock_catalog(monkeypatch, ExtensionCatalog, "extensions", "requested", record)
1256+
warnings = []
1257+
check = make_reference_checker(tmp_path, allow_network=True, warnings=warnings)
1258+
1259+
assert check(ComponentRef(
1260+
kind="extensions", id="requested", version="1.0.0", source="trusted"
1261+
)) is None
1262+
assert warnings == []
1263+
1264+
12221265
@pytest.mark.parametrize("kind", ["workflows", "steps"])
12231266
@pytest.mark.parametrize("duplicate_id", ["requested", "other"])
12241267
def test_bundle_rejects_duplicate_list_ids_before_lower_source(

0 commit comments

Comments
 (0)