Skip to content
Open
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
16 changes: 15 additions & 1 deletion src/specify_cli/bundler/lib/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions tests/integration/test_bundler_security_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down