From fd4288ffd2cc3bcd12c2a18a99af64fff6798dab Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Fri, 11 Sep 2026 22:52:59 +0500 Subject: [PATCH] fix(bundler): normalize the recorded integration key like the canonical reader `active_integration`'s own comment says it matches the canonical reader in `integration_state`, but that reader runs every value through `clean_integration_key` while this one only checked: if isinstance(value, str) and value: return value A whitespace-only key is truthy, so it was returned as a real integration -- and, being non-None, it also suppressed the "not determinable" fallback the docstring promises. A padded key was returned verbatim and matches no registered integration. Reproduced on main: recorded='copilot' -> 'copilot' canonical='copilot' recorded=' copilot ' -> ' copilot ' canonical='copilot' DIVERGES recorded=' ' -> ' ' canonical=None DIVERGES recorded='\t\n' -> '\t\n' canonical=None DIVERGES Now delegates to `clean_integration_key` rather than re-implementing the check, so the two readers cannot drift again. Co-Authored-By: Claude Opus 5 (1M context) --- src/specify_cli/bundler/lib/project.py | 11 ++++++-- tests/contract/test_bundle_cli.py | 36 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/bundler/lib/project.py b/src/specify_cli/bundler/lib/project.py index c895bf579d..6560b1c474 100644 --- a/src/specify_cli/bundler/lib/project.py +++ b/src/specify_cli/bundler/lib/project.py @@ -4,6 +4,7 @@ from pathlib import Path from ..._project import _resolve_init_dir_override +from ...integration_state import clean_integration_key from .. import BundlerError from .yamlio import ensure_within, load_json @@ -97,6 +98,12 @@ def active_integration(project_root: Path) -> str | None: or data.get("id") or data.get("active") ) - if isinstance(value, str) and value: - return value + # Normalize through the same helper the canonical reader uses rather + # than re-implementing the check. ``isinstance(value, str) and value`` + # accepted a whitespace-only key as a real one -- truthy, so it also + # suppressed the "not determinable" fallback -- and returned a padded + # key verbatim, which matches no registered integration: + # ' copilot ' -> ' copilot ' (canonical: 'copilot') + # ' ' -> ' ' (canonical: None) + return clean_integration_key(value) return None diff --git a/tests/contract/test_bundle_cli.py b/tests/contract/test_bundle_cli.py index 6db4dab769..a3a6a3bd70 100644 --- a/tests/contract/test_bundle_cli.py +++ b/tests/contract/test_bundle_cli.py @@ -1075,3 +1075,39 @@ def fake_open_url(url, timeout=None, extra_headers=None, redirect_validator=None # Rich may wrap the message across lines; normalise whitespace before checking. output_flat = " ".join(result.output.split()) assert "exceeds maximum size of 100 bytes" in output_flat + + +@pytest.mark.parametrize( + "recorded,expected", + [ + ("copilot", "copilot"), + (" copilot ", "copilot"), # padded: previously returned verbatim + (" ", None), # whitespace-only: previously truthy + ("\t\n", None), + ("", None), + (None, None), + (5, None), + ], + ids=["plain", "padded", "spaces", "tabs", "empty", "null", "non_string"], +) +def test_active_integration_matches_the_canonical_key_reader( + tmp_path: Path, recorded, expected +): + """`active_integration` must normalize the way the canonical reader does. + + Its own comment says it matches `integration_state`'s reader, but that + reader runs every value through `clean_integration_key`, while this one + only checked `isinstance(value, str) and value`. A whitespace-only key is + truthy, so it was returned as a real integration *and* suppressed the + "not determinable" fallback; a padded key was returned verbatim and + matches no registered integration. + """ + from specify_cli.bundler.lib.project import active_integration + + project = tmp_path / "proj" + (project / ".specify").mkdir(parents=True) + (project / ".specify" / "integration.json").write_text( + json.dumps({"default_integration": recorded}), encoding="utf-8" + ) + + assert active_integration(project) == expected