Skip to content

Commit b8d27e4

Browse files
authored
test: reduce registry manifest test repetition (#3146)
* test: isolate integration test home Assisted-by: Codex (model: GPT-5, autonomous) * test: reduce registry manifest test repetition Assisted-by: Codex (model: GPT-5, autonomous) * test: clarify disjoint-manifest order rationale and guard safe set Add a >=2 precondition, explain why two install orders are tested (manifests are order-independent; the orders only vary the init path), and build the manifest map with a comprehension. * test: rotate init coverage for manifest isolation Assisted-by: Codex (model: GPT-5, autonomous) * test: assert integration home isolation Assisted-by: Codex (model: GPT-5, autonomous) * test: guard multi-install manifest rotations Assisted-by: Codex (model: GPT-5, autonomous)
1 parent 587b185 commit b8d27e4

3 files changed

Lines changed: 118 additions & 54 deletions

File tree

tests/integrations/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
"""Shared test helpers for integration tests."""
22

3+
import pytest
4+
35
from specify_cli.integrations.base import MarkdownIntegration
46

57

8+
@pytest.fixture(autouse=True)
9+
def _isolate_integration_home(monkeypatch: pytest.MonkeyPatch, tmp_path):
10+
"""Keep integration tests from reading or writing the real user home."""
11+
home = tmp_path / "home"
12+
for path in (home, home / ".cache", home / ".config", home / ".local" / "share"):
13+
path.mkdir(parents=True, exist_ok=True)
14+
15+
monkeypatch.setenv("HOME", str(home))
16+
monkeypatch.setenv("USERPROFILE", str(home))
17+
monkeypatch.setenv("XDG_CACHE_HOME", str(home / ".cache"))
18+
monkeypatch.setenv("XDG_CONFIG_HOME", str(home / ".config"))
19+
monkeypatch.setenv("XDG_DATA_HOME", str(home / ".local" / "share"))
20+
21+
622
class StubIntegration(MarkdownIntegration):
723
"""Minimal concrete integration for testing."""
824

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Regression tests for integration-test environment isolation."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from pathlib import Path
7+
8+
9+
def test_integration_tests_use_tmp_home(tmp_path: Path) -> None:
10+
home = tmp_path / "home"
11+
12+
assert Path(os.environ["HOME"]) == home
13+
assert Path(os.environ["USERPROFILE"]) == home
14+
assert Path(os.environ["XDG_CACHE_HOME"]) == home / ".cache"
15+
assert Path(os.environ["XDG_CONFIG_HOME"]) == home / ".config"
16+
assert Path(os.environ["XDG_DATA_HOME"]) == home / ".local" / "share"
17+
18+
assert home.is_dir()
19+
assert (home / ".cache").is_dir()
20+
assert (home / ".config").is_dir()
21+
assert (home / ".local" / "share").is_dir()

tests/integrations/test_registry.py

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ def _multi_install_safe_pairs() -> list[tuple[str, str]]:
4848
]
4949

5050

51+
def _multi_install_safe_orders() -> list[list[str]]:
52+
safe_keys = _multi_install_safe_keys()
53+
if len(safe_keys) < 2:
54+
return [safe_keys]
55+
return [safe_keys[index:] + safe_keys[:index] for index in range(len(safe_keys))]
56+
57+
58+
def _multi_install_safe_order_id(ordered_keys: list[str]) -> str:
59+
if not ordered_keys:
60+
return "no-safe-integrations"
61+
return f"init-{ordered_keys[0]}"
62+
63+
5164
def _posix_path(value: str | None) -> str | None:
5265
if not value:
5366
return None
@@ -87,16 +100,6 @@ def _paths_overlap(first: str | None, second: str | None) -> bool:
87100
return False
88101

89102

90-
def _path_is_inside(path: str | None, directory: str | None) -> bool:
91-
if not path or not directory:
92-
return False
93-
try:
94-
PurePosixPath(path).relative_to(PurePosixPath(directory))
95-
return True
96-
except ValueError:
97-
return False
98-
99-
100103
class TestRegistry:
101104
def test_registry_is_dict(self):
102105
assert isinstance(INTEGRATION_REGISTRY, dict)
@@ -162,6 +165,15 @@ def test_no_stale_cursor_shorthand(self):
162165
class TestMultiInstallSafeContracts:
163166
"""Declared safe integrations must stay isolated from each other."""
164167

168+
def test_safe_install_orders_rotate_each_integration_through_init(self):
169+
safe_keys = _multi_install_safe_keys()
170+
orders = _multi_install_safe_orders()
171+
172+
assert len(safe_keys) >= 2
173+
assert [order[0] for order in orders] == safe_keys
174+
assert len({tuple(order) for order in orders}) == len(safe_keys)
175+
assert all(sorted(order) == safe_keys for order in orders)
176+
165177
@pytest.mark.parametrize("key", _multi_install_safe_keys())
166178
def test_safe_integrations_have_static_isolated_paths(self, key):
167179
assert _integration_root_dir(key), (
@@ -187,62 +199,77 @@ def test_safe_integrations_have_distinct_command_dirs(self, first, second):
187199
f"{_integration_commands_dir(second)!r}"
188200
)
189201

190-
@pytest.mark.parametrize(("first", "second"), _multi_install_safe_pairs())
202+
@pytest.mark.parametrize(
203+
"ordered_keys",
204+
_multi_install_safe_orders(),
205+
ids=_multi_install_safe_order_id,
206+
)
191207
def test_safe_integrations_have_disjoint_manifests(
192208
self,
193209
tmp_path,
194-
first,
195-
second,
210+
ordered_keys,
196211
):
197-
for initial, additional in ((first, second), (second, first)):
198-
project_root = tmp_path / f"project-{initial}-{additional}"
199-
project_root.mkdir()
200-
runner = CliRunner()
201-
202-
original_cwd = os.getcwd()
203-
try:
204-
os.chdir(project_root)
205-
init_result = runner.invoke(
206-
app,
207-
[
208-
"init",
209-
"--here",
210-
"--integration",
211-
initial,
212-
"--script",
213-
"sh",
214-
"--ignore-agent-tools",
215-
],
216-
catch_exceptions=False,
217-
)
218-
assert init_result.exit_code == 0, init_result.output
212+
# The pairwise disjointness contract is only meaningful with at least
213+
# two safe integrations. Guard so a shrunken registry fails loudly here
214+
# rather than passing vacuously (or tripping over ordered_keys[0] below).
215+
assert len(ordered_keys) >= 2, (
216+
f"expected at least two multi-install-safe integrations, got {ordered_keys}"
217+
)
219218

219+
project_root = tmp_path / "project"
220+
project_root.mkdir()
221+
runner = CliRunner()
222+
223+
# Install every safe integration once into a single project, then assert
224+
# pairwise manifest isolation. Each safe integration writes only to its
225+
# own (disjoint) directories and always records what it writes, so a
226+
# manifest's contents are independent of install order and of which other
227+
# integrations are co-installed. The parametrized rotations keep the
228+
# aggregate setup while placing each safe integration first once, so each
229+
# one still exercises the `specify init --integration ...` path.
230+
original_cwd = os.getcwd()
231+
try:
232+
os.chdir(project_root)
233+
init_result = runner.invoke(
234+
app,
235+
[
236+
"init",
237+
"--here",
238+
"--integration",
239+
ordered_keys[0],
240+
"--script",
241+
"sh",
242+
"--ignore-agent-tools",
243+
],
244+
catch_exceptions=False,
245+
)
246+
assert init_result.exit_code == 0, init_result.output
247+
248+
for key in ordered_keys[1:]:
220249
install_result = runner.invoke(
221250
app,
222-
["integration", "install", additional, "--script", "sh"],
251+
["integration", "install", key, "--script", "sh"],
223252
catch_exceptions=False,
224253
)
225254
assert install_result.exit_code == 0, install_result.output
226-
finally:
227-
os.chdir(original_cwd)
255+
finally:
256+
os.chdir(original_cwd)
228257

229-
initial_manifest = json.loads(
230-
(
231-
project_root / ".specify" / "integrations" / f"{initial}.manifest.json"
232-
).read_text(encoding="utf-8")
258+
integrations_dir = project_root / ".specify" / "integrations"
259+
manifests = {}
260+
for key in ordered_keys:
261+
manifest = json.loads(
262+
(integrations_dir / f"{key}.manifest.json").read_text(encoding="utf-8")
233263
)
234-
additional_manifest = json.loads(
235-
(
236-
project_root / ".specify" / "integrations" / f"{additional}.manifest.json"
237-
).read_text(encoding="utf-8")
238-
)
239-
240-
initial_files = set(initial_manifest.get("files", {}))
241-
additional_files = set(additional_manifest.get("files", {}))
242-
243-
assert initial_files.isdisjoint(additional_files), (
244-
f"{initial} and {additional} are declared multi-install safe but both manage "
245-
f"these files: {sorted(initial_files & additional_files)}"
264+
files = manifest.get("files", {})
265+
assert isinstance(files, dict), f"{key} manifest files must be an object"
266+
manifests[key] = set(files.keys())
267+
268+
for first, second in _multi_install_safe_pairs():
269+
overlap = manifests[first] & manifests[second]
270+
assert not overlap, (
271+
f"{first} and {second} are declared multi-install safe but both manage "
272+
f"these files: {sorted(overlap)}"
246273
)
247274

248275

0 commit comments

Comments
 (0)