Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/specify_cli/bundles/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,19 @@ def from_dict(cls, data: Any) -> BundleManifest:
# leaving the bundle wrongly integration-agnostic. Reject it instead.
if integration_raw is not None and not isinstance(integration_raw, dict):
raise BundlerError("'integration' must be a mapping when present.")
if isinstance(integration_raw, dict) and integration_raw.get("id"):
integration = IntegrationRef(id=str(integration_raw["id"]).strip())
if isinstance(integration_raw, dict):
# A mapping without a usable id was silently mishandled: a
# missing or empty id fell through to integration=None (the
# declared integration was dropped and the bundle validated as
# integration-agnostic), while a whitespace-only id passed the
# old truthiness check and became IntegrationRef(id="").
# Reject all three instead.
integration_id = _text(integration_raw.get("id"))
if not integration_id:
raise BundlerError(
"'integration' mapping must include a non-empty 'id'."
)
integration = IntegrationRef(id=integration_id)

provides = data.get("provides")
if provides is None:
Expand Down
11 changes: 11 additions & 0 deletions tests/specify_cli/bundles/test_command_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ def test_validate_reports_invalid_manifest(project: Path):
assert "license" in result.output


@pytest.mark.parametrize("bad_integration", [{}, {"name": "claude"}])
def test_validate_rejects_integration_mapping_without_id(
project: Path, bad_integration
):
data = valid_manifest_dict(integration=bad_integration)
(project / "bundle.yml").write_text(yaml.safe_dump(data), encoding="utf-8")
result = runner.invoke(app, ["bundle", "validate", "--offline"])
assert result.exit_code == 1
assert "integration" in result.output


@pytest.mark.parametrize("version", ["1.2.0", "1.20.30-12alpha.1+build.01", "V10.20.30"])
def test_validate_accepts_valid_manifest(project: Path, version: str):
data = valid_manifest_dict()
Expand Down
25 changes: 25 additions & 0 deletions tests/specify_cli/bundles/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,28 @@ def test_non_list_bundles_rejected(bundles):
def test_non_mapping_bundle_entry_rejected(entry):
with pytest.raises(BundlerError, match=r"provides\.bundles entry must be a mapping"):
_manifest(reference_manifest_dict([entry]))


@pytest.mark.parametrize(
"bad_integration",
[{}, {"name": "claude"}, {"id": ""}, {"id": " "}, {"id": None}],
)
def test_integration_mapping_without_usable_id_rejected(bad_integration):
data = valid_manifest_dict(integration=bad_integration)
with pytest.raises(
BundlerError, match="'integration' mapping must include a non-empty 'id'"
):
_manifest(data)


def test_integration_mapping_with_id_accepted_and_stripped():
manifest = _manifest(valid_manifest_dict(integration={"id": " claude "}))
assert manifest.integration is not None
assert manifest.integration.id == "claude"
assert not manifest.is_agnostic()


def test_absent_integration_stays_agnostic():
manifest = _manifest(valid_manifest_dict())
assert manifest.integration is None
assert manifest.is_agnostic()
Loading