|
8 | 8 | from pathlib import Path # noqa: F401 |
9 | 9 |
|
10 | 10 | import pytest # noqa: F401 |
| 11 | +import yaml |
11 | 12 |
|
12 | 13 | from specify_cli import app # noqa: F401 |
13 | 14 | from tests.conftest import strip_ansi # noqa: F401 |
|
21 | 22 | runner, # noqa: F401 |
22 | 23 | ) |
23 | 24 |
|
| 25 | + |
| 26 | +def _add_sample_preset(project, tmp_path): |
| 27 | + """Install a dev preset that overrides ``speckit.specify``.""" |
| 28 | + preset = tmp_path / "sample-preset" |
| 29 | + (preset / "commands").mkdir(parents=True) |
| 30 | + (preset / "commands" / "speckit.specify.md").write_text( |
| 31 | + "---\ndescription: Sample preset command\n---\nSample preset guidance\n", |
| 32 | + encoding="utf-8", |
| 33 | + ) |
| 34 | + (preset / "preset.yml").write_text(yaml.safe_dump({ |
| 35 | + "schema_version": "1.0", |
| 36 | + "preset": {"id": "sample-preset", "name": "Sample Preset", "version": "1.0.0", "description": "Sample guidance"}, |
| 37 | + "requires": {"speckit_version": ">=0.1.0"}, |
| 38 | + "provides": {"templates": [{"type": "command", "name": "speckit.specify", "file": "commands/speckit.specify.md"}]}, |
| 39 | + }), encoding="utf-8") |
| 40 | + result = _run_in_project(project, ["preset", "add", "--dev", str(preset)]) |
| 41 | + assert result.exit_code == 0, f"preset add failed: {result.output}" |
| 42 | + |
| 43 | + |
| 44 | +def _registered_agents(project, kind, package_id): |
| 45 | + """Agents whose artifacts the extension or preset registry tracks for ``package_id``.""" |
| 46 | + registry = json.loads((project / ".specify" / kind / ".registry").read_text(encoding="utf-8")) |
| 47 | + return set(registry[kind][package_id]["registered_commands"]) |
| 48 | + |
24 | 49 | class TestIntegrationUninstall: |
25 | 50 | def test_uninstall_requires_speckit_project(self, tmp_path): |
26 | 51 | old_cwd = os.getcwd() |
@@ -167,6 +192,78 @@ def test_uninstall_default_refreshes_templates_for_fallback(self, tmp_path): |
167 | 192 | assert "/speckit-plan" in template.read_text(encoding="utf-8") |
168 | 193 | assert "/speckit-plan" in script.read_text(encoding="utf-8") |
169 | 194 |
|
| 195 | + def test_uninstall_unregisters_extension_and_preset_artifacts(self, tmp_path): |
| 196 | + """Uninstall must not leave extension or preset files orphaned in the agent's directory.""" |
| 197 | + project = _init_project(tmp_path, "claude") |
| 198 | + result = _run_in_project(project, ["extension", "add", "git"]) |
| 199 | + assert result.exit_code == 0, f"extension add failed: {result.output}" |
| 200 | + _add_sample_preset(project, tmp_path) |
| 201 | + claude_git_feature = project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md" |
| 202 | + claude_specify = project / ".claude" / "skills" / "speckit-specify" / "SKILL.md" |
| 203 | + assert claude_git_feature.exists() |
| 204 | + assert "Sample preset guidance" in claude_specify.read_text(encoding="utf-8") |
| 205 | + |
| 206 | + result = _run_in_project(project, ["integration", "uninstall", "claude"]) |
| 207 | + assert result.exit_code == 0, result.output |
| 208 | + |
| 209 | + assert not claude_git_feature.exists(), "Extension skill should be removed with its agent" |
| 210 | + assert not claude_specify.exists(), "Preset skill should be removed with its agent" |
| 211 | + assert "claude" not in _registered_agents(project, "extensions", "git") |
| 212 | + assert "claude" not in _registered_agents(project, "presets", "sample-preset") |
| 213 | + |
| 214 | + @pytest.mark.parametrize("missing_manifest", [False, True]) |
| 215 | + def test_uninstall_default_registers_contributions_for_fallback(self, tmp_path, missing_manifest): |
| 216 | + """The integration promoted to default receives enabled extensions and presets, like ``use``.""" |
| 217 | + project = _init_project(tmp_path, "claude") |
| 218 | + result = _run_in_project(project, ["extension", "add", "git"]) |
| 219 | + assert result.exit_code == 0, f"extension add failed: {result.output}" |
| 220 | + _add_sample_preset(project, tmp_path) |
| 221 | + result = _run_in_project(project, [ |
| 222 | + "integration", "install", "codex", |
| 223 | + "--script", "sh", |
| 224 | + ]) |
| 225 | + assert result.exit_code == 0, result.output |
| 226 | + codex_git_feature = project / ".agents" / "skills" / "speckit-git-feature" / "SKILL.md" |
| 227 | + codex_specify = project / ".agents" / "skills" / "speckit-specify" / "SKILL.md" |
| 228 | + assert not codex_git_feature.exists(), "precondition: secondary install has no extension artifacts" |
| 229 | + assert "Sample preset guidance" not in codex_specify.read_text(encoding="utf-8"), ( |
| 230 | + "precondition: secondary install has no preset artifacts" |
| 231 | + ) |
| 232 | + if missing_manifest: |
| 233 | + (project / ".specify" / "integrations" / "claude.manifest.json").unlink() |
| 234 | + |
| 235 | + result = _run_in_project(project, ["integration", "uninstall", "claude"]) |
| 236 | + assert result.exit_code == 0, result.output |
| 237 | + |
| 238 | + data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8")) |
| 239 | + assert data["integration"] == "codex" |
| 240 | + assert codex_git_feature.exists(), "Promoted default should receive enabled extensions" |
| 241 | + assert "Sample preset guidance" in codex_specify.read_text(encoding="utf-8"), ( |
| 242 | + "Promoted default should receive enabled presets" |
| 243 | + ) |
| 244 | + assert not (project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md").exists() |
| 245 | + assert not (project / ".claude" / "skills" / "speckit-specify" / "SKILL.md").exists() |
| 246 | + |
| 247 | + def test_uninstall_non_default_preserves_default_contributions(self, tmp_path): |
| 248 | + project = _init_project(tmp_path, "claude") |
| 249 | + result = _run_in_project(project, ["extension", "add", "git"]) |
| 250 | + assert result.exit_code == 0, f"extension add failed: {result.output}" |
| 251 | + _add_sample_preset(project, tmp_path) |
| 252 | + result = _run_in_project(project, [ |
| 253 | + "integration", "install", "codex", |
| 254 | + "--script", "sh", |
| 255 | + ]) |
| 256 | + assert result.exit_code == 0, result.output |
| 257 | + |
| 258 | + result = _run_in_project(project, ["integration", "uninstall", "codex"]) |
| 259 | + assert result.exit_code == 0, result.output |
| 260 | + |
| 261 | + assert (project / ".claude" / "skills" / "speckit-git-feature" / "SKILL.md").exists() |
| 262 | + claude_specify = project / ".claude" / "skills" / "speckit-specify" / "SKILL.md" |
| 263 | + assert "Sample preset guidance" in claude_specify.read_text(encoding="utf-8") |
| 264 | + assert "claude" in _registered_agents(project, "extensions", "git") |
| 265 | + assert "claude" in _registered_agents(project, "presets", "sample-preset") |
| 266 | + |
170 | 267 | def test_uninstall_preserves_shared_infra(self, tmp_path): |
171 | 268 | """Shared scripts and templates are not removed by integration uninstall.""" |
172 | 269 | project = _init_project(tmp_path, "claude") |
|
0 commit comments