diff --git a/src/specify_cli/bundler/lib/project.py b/src/specify_cli/bundler/lib/project.py index 6b9e9642f7..c895bf579d 100644 --- a/src/specify_cli/bundler/lib/project.py +++ b/src/specify_cli/bundler/lib/project.py @@ -82,7 +82,21 @@ def active_integration(project_root: Path) -> str | None: except BundlerError: return None if isinstance(data, dict): - value = data.get("integration") or data.get("id") or data.get("active") + # ``default_integration`` first, matching the canonical reader in + # ``integration_state`` (line 199): + # ``state.get("default_integration") or state.get("integration")``. + # ``write_integration_json`` writes both keys, so a marker produced by + # the current CLI already resolved through the ``integration`` alias -- + # this is about which field is authoritative when they disagree, and + # about resolving a marker that carries only ``default_integration`` + # (hand-edited, or written by anything that follows the canonical + # reader's shape). ``integration``/``id``/``active`` stay as fallbacks. + value = ( + data.get("default_integration") + or data.get("integration") + or data.get("id") + or data.get("active") + ) if isinstance(value, str) and value: return value return None diff --git a/tests/integration/test_bundler_security_paths.py b/tests/integration/test_bundler_security_paths.py index 0c01fe6406..e575dccb88 100644 --- a/tests/integration/test_bundler_security_paths.py +++ b/tests/integration/test_bundler_security_paths.py @@ -126,6 +126,50 @@ def test_active_integration_refuses_symlinked_specify_escape(tmp_path: Path): assert active_integration(project) is None +def _write_marker(tmp_path: Path, payload: str) -> Path: + project = tmp_path / "proj" + (project / ".specify").mkdir(parents=True) + (project / ".specify" / "integration.json").write_text( + payload, encoding="utf-8" + ) + return project + + +def test_active_integration_reads_default_integration(tmp_path: Path): + """A marker carrying only ``default_integration`` must resolve. + + ``write_integration_json`` writes both ``integration`` and + ``default_integration``, so a marker produced by the current CLI already + resolved through the alias. This covers the authoritative field on its own — + hand-edited, or written by anything that follows the shape of the canonical + reader (``integration_state`` line 199: + ``state.get("default_integration") or state.get("integration")``). + """ + from specify_cli.bundler.lib.project import active_integration + + project = _write_marker(tmp_path, '{"default_integration": "copilot"}') + assert active_integration(project) == "copilot" + + +def test_active_integration_prefers_default_over_legacy_alias(tmp_path: Path): + """When both are present the authoritative field wins, matching + ``integration_state``'s own ordering.""" + from specify_cli.bundler.lib.project import active_integration + + project = _write_marker( + tmp_path, '{"integration": "stale", "default_integration": "copilot"}' + ) + assert active_integration(project) == "copilot" + + +def test_active_integration_still_reads_legacy_alias(tmp_path: Path): + """Projects initialised by older versions carry only ``integration``.""" + from specify_cli.bundler.lib.project import active_integration + + project = _write_marker(tmp_path, '{"integration": "copilot"}') + assert active_integration(project) == "copilot" + + def test_read_catalog_config_refuses_symlinked_specify_escape(tmp_path: Path): from specify_cli.bundler.commands_impl import catalog_config as cc