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: 16 additions & 0 deletions tests/integrations/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
"""Shared test helpers for integration tests."""

import pytest

from specify_cli.integrations.base import MarkdownIntegration


@pytest.fixture(autouse=True)
def _isolate_integration_home(monkeypatch: pytest.MonkeyPatch, tmp_path):
"""Keep integration tests from reading or writing the real user home."""
home = tmp_path / "home"
for path in (home, home / ".cache", home / ".config", home / ".local" / "share"):
path.mkdir(parents=True, exist_ok=True)

monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
monkeypatch.setenv("XDG_CACHE_HOME", str(home / ".cache"))
monkeypatch.setenv("XDG_CONFIG_HOME", str(home / ".config"))
monkeypatch.setenv("XDG_DATA_HOME", str(home / ".local" / "share"))


class StubIntegration(MarkdownIntegration):
"""Minimal concrete integration for testing."""

Expand Down
111 changes: 66 additions & 45 deletions tests/integrations/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def _multi_install_safe_pairs() -> list[tuple[str, str]]:
]


def _multi_install_safe_orders() -> list[list[str]]:
safe_keys = _multi_install_safe_keys()
return [safe_keys, list(reversed(safe_keys))]


def _posix_path(value: str | None) -> str | None:
if not value:
return None
Expand Down Expand Up @@ -230,60 +235,76 @@ def test_safe_context_files_do_not_overlap_other_command_dirs(self, first, secon
f"commands directory {_integration_commands_dir(first)!r}"
)

@pytest.mark.parametrize(("first", "second"), _multi_install_safe_pairs())
@pytest.mark.parametrize(
"ordered_keys",
_multi_install_safe_orders(),
ids=["forward", "reverse"],
)
def test_safe_integrations_have_disjoint_manifests(
self,
tmp_path,
first,
second,
ordered_keys,
):
for initial, additional in ((first, second), (second, first)):
project_root = tmp_path / f"project-{initial}-{additional}"
project_root.mkdir()
runner = CliRunner()

original_cwd = os.getcwd()
try:
os.chdir(project_root)
init_result = runner.invoke(
app,
[
"init",
"--here",
"--integration",
initial,
"--script",
"sh",
"--ignore-agent-tools",
],
catch_exceptions=False,
)
assert init_result.exit_code == 0, init_result.output
# The pairwise disjointness contract is only meaningful with at least
# two safe integrations. Guard so a shrunken registry fails loudly here
# rather than passing vacuously (or tripping over ordered_keys[0] below).
assert len(ordered_keys) >= 2, (
f"expected at least two multi-install-safe integrations, got {ordered_keys}"
)

project_root = tmp_path / "project"
project_root.mkdir()
runner = CliRunner()

# Install every safe integration once into a single project, then assert
# pairwise manifest isolation. Each safe integration writes only to its
# own (disjoint) directories and always records what it writes, so a
# manifest's contents are independent of install order and of which other
# integrations are co-installed. The two parametrized orders therefore
# produce the same manifests; their purpose is to route a different
# integration through the `init` path versus `integration install`
# (forward installs the first key via init, reverse the last).
original_cwd = os.getcwd()
try:
os.chdir(project_root)
init_result = runner.invoke(
app,
[
"init",
"--here",
"--integration",
ordered_keys[0],
"--script",
"sh",
"--ignore-agent-tools",
],
catch_exceptions=False,
)
assert init_result.exit_code == 0, init_result.output

for key in ordered_keys[1:]:
install_result = runner.invoke(
app,
["integration", "install", additional, "--script", "sh"],
["integration", "install", key, "--script", "sh"],
catch_exceptions=False,
)
assert install_result.exit_code == 0, install_result.output
finally:
os.chdir(original_cwd)

initial_manifest = json.loads(
(
project_root / ".specify" / "integrations" / f"{initial}.manifest.json"
).read_text(encoding="utf-8")
)
additional_manifest = json.loads(
(
project_root / ".specify" / "integrations" / f"{additional}.manifest.json"
).read_text(encoding="utf-8")
finally:
os.chdir(original_cwd)

integrations_dir = project_root / ".specify" / "integrations"
manifests = {
key: set(
json.loads(
(integrations_dir / f"{key}.manifest.json").read_text(encoding="utf-8")
).get("files", {})
)

initial_files = set(initial_manifest.get("files", {}))
additional_files = set(additional_manifest.get("files", {}))

assert initial_files.isdisjoint(additional_files), (
f"{initial} and {additional} are declared multi-install safe but both manage "
f"these files: {sorted(initial_files & additional_files)}"
for key in ordered_keys
}

for first, second in _multi_install_safe_pairs():
overlap = manifests[first] & manifests[second]
assert not overlap, (
f"{first} and {second} are declared multi-install safe but both manage "
f"these files: {sorted(overlap)}"
)