diff --git a/tests/integrations/test_registry.py b/tests/integrations/test_registry.py index 0110e19ec7..9bcb4b2c44 100644 --- a/tests/integrations/test_registry.py +++ b/tests/integrations/test_registry.py @@ -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 @@ -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)}" )