From 5970d21538f4a3ef4cb48f854aff9376d236c2c2 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 22 Jul 2026 10:27:12 +0500 Subject: [PATCH] fix(bundler): reject non-mapping 'integration' in a bundle manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BundleManifest.from_dict guarded 'requires' and 'provides' with "must be a mapping when present", but a present-but-non-mapping 'integration' (e.g. a bare string "copilot") silently failed the isinstance(dict) check and was dropped — leaving the bundle wrongly integration-agnostic (is_agnostic() True) instead of surfacing the authoring mistake. Add the same guard so 'integration' is consistent with its sibling mapping fields. Test: integration='copilot' now raises BundlerError (fails before: silently dropped, no raise). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/bundler/models/manifest.py | 5 +++++ tests/contract/test_manifest_schema.py | 10 ++++++++++ 2 files changed, 15 insertions(+) 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)