diff --git a/src/specify_cli/bundler/models/manifest.py b/src/specify_cli/bundler/models/manifest.py index 4a903fbd18..e7969683c9 100644 --- a/src/specify_cli/bundler/models/manifest.py +++ b/src/specify_cli/bundler/models/manifest.py @@ -122,6 +122,11 @@ def from_dict(cls, data: Any) -> "BundleManifest": integration = None integration_raw = data.get("integration") + # Mirror the requires/provides guards above: a present-but-non-mapping + # 'integration' (e.g. a bare string "copilot") was silently dropped, + # 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()) diff --git a/tests/contract/test_manifest_schema.py b/tests/contract/test_manifest_schema.py index 4d0d95f608..1c1c279868 100644 --- a/tests/contract/test_manifest_schema.py +++ b/tests/contract/test_manifest_schema.py @@ -124,3 +124,13 @@ def test_string_mcp_rejected_not_split_per_character(): data["requires"]["mcp"] = "github" with pytest.raises(BundlerError, match="'requires.mcp' must be a list of strings"): BundleManifest.from_dict(data) + + +def test_string_integration_rejected_not_silently_dropped(): + # A present-but-non-mapping 'integration' (a bare string) was silently + # dropped, leaving the bundle wrongly integration-agnostic. Reject it like + # the sibling requires/provides mapping fields. + data = valid_manifest_dict() + data["integration"] = "copilot" + with pytest.raises(BundlerError, match="'integration' must be a mapping when present"): + BundleManifest.from_dict(data)