Skip to content

Commit dfe1c8d

Browse files
mnriemCopilot
andcommitted
fix: preserve preset refactor seams
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 61216b1 commit dfe1c8d

8 files changed

Lines changed: 200 additions & 286 deletions

File tree

‎src/specify_cli/presets/command_add.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def _validate_download_redirect(old_url, new_url):
368368
# exits, so one call here covers --dev, --from, and catalog installs
369369
# alike. Warns rather than fails: the preset is installed and its
370370
# overrides fall through to the core workflow without the extension.
371-
_warn_unmet_extension_dependencies(manager, manifest)
371+
_commands._warn_unmet_extension_dependencies(manager, manifest)
372372

373373
except PresetCompatibilityError as e:
374374
console.print(f"[red]Compatibility Error:[/red] {_escape_markup(str(e))}")
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Shared pytest fixtures for preset domain and command suites."""
2+
3+
from __future__ import annotations
4+
5+
import shutil
6+
import tempfile
7+
from pathlib import Path
8+
9+
import pytest
10+
import yaml
11+
12+
13+
@pytest.fixture
14+
def temp_dir():
15+
"""Create a temporary directory for tests."""
16+
tmpdir = tempfile.mkdtemp()
17+
yield Path(tmpdir)
18+
shutil.rmtree(tmpdir)
19+
20+
21+
@pytest.fixture
22+
def valid_pack_data():
23+
"""Return valid preset manifest data."""
24+
return {
25+
"schema_version": "1.0",
26+
"preset": {
27+
"id": "test-pack",
28+
"name": "Test Preset",
29+
"version": "1.0.0",
30+
"description": "A test preset",
31+
"author": "Test Author",
32+
"repository": "https://github.com/test/test-pack",
33+
"license": "MIT",
34+
},
35+
"requires": {"speckit_version": ">=0.1.0"},
36+
"provides": {
37+
"templates": [
38+
{
39+
"type": "template",
40+
"name": "spec-template",
41+
"file": "templates/spec-template.md",
42+
"description": "Custom spec template",
43+
"replaces": "spec-template",
44+
}
45+
]
46+
},
47+
"tags": ["testing", "example"],
48+
}
49+
50+
51+
@pytest.fixture
52+
def pack_dir(temp_dir, valid_pack_data):
53+
"""Create a complete preset directory structure."""
54+
preset_dir = temp_dir / "test-pack"
55+
preset_dir.mkdir()
56+
(preset_dir / "preset.yml").write_text(
57+
yaml.safe_dump(valid_pack_data), encoding="utf-8"
58+
)
59+
templates_dir = preset_dir / "templates"
60+
templates_dir.mkdir()
61+
(templates_dir / "spec-template.md").write_text(
62+
"# Custom Spec Template\n\nThis is a custom template.\n",
63+
encoding="utf-8",
64+
)
65+
return preset_dir
66+
67+
68+
@pytest.fixture
69+
def project_dir(temp_dir):
70+
"""Create a mock spec-kit project directory."""
71+
project = temp_dir / "project"
72+
project.mkdir()
73+
templates_dir = project / ".specify" / "templates"
74+
templates_dir.mkdir(parents=True)
75+
(templates_dir / "spec-template.md").write_text(
76+
"# Core Spec Template\n", encoding="utf-8"
77+
)
78+
(templates_dir / "plan-template.md").write_text(
79+
"# Core Plan Template\n", encoding="utf-8"
80+
)
81+
(templates_dir / "commands").mkdir()
82+
return project

‎tests/specify_cli/presets/_helpers.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22

33
from __future__ import annotations
44

5+
import json
56
import warnings
7+
from datetime import UTC, datetime
68
from pathlib import Path
79

810
import yaml
911

10-
from specify_cli.presets import PresetManager, PresetManifest
12+
from specify_cli.presets import (
13+
PresetCatalog,
14+
PresetCatalogEntry,
15+
PresetManager,
16+
PresetManifest,
17+
)
1118

1219
REPO_ROOT = Path(__file__).parents[3]
1320
SELF_TEST_PRESET_DIR = REPO_ROOT / "presets" / "self-test"
@@ -26,6 +33,48 @@
2633
]
2734

2835

36+
def seed_catalog(
37+
project_dir: Path,
38+
tags: object,
39+
extra: dict[str, object] | None = None,
40+
) -> PresetCatalog:
41+
"""Seed cached catalog metadata used by search and info command tests."""
42+
catalog = PresetCatalog(project_dir)
43+
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
44+
pack = {
45+
"name": "Numeric Tags",
46+
"description": "Preset with non-string tags",
47+
"version": "1.0.0",
48+
"tags": tags,
49+
}
50+
if extra:
51+
pack.update(extra)
52+
catalog.cache_file.write_text(
53+
json.dumps(
54+
{
55+
"schema_version": "1.0",
56+
"presets": {"numeric-tags": pack},
57+
}
58+
)
59+
)
60+
catalog.cache_metadata_file.write_text(
61+
json.dumps({"cached_at": datetime.now(UTC).isoformat()})
62+
)
63+
return catalog
64+
65+
66+
def default_catalog_entries(catalog: PresetCatalog) -> list[PresetCatalogEntry]:
67+
"""Return the default catalog as the only active catalog."""
68+
return [
69+
PresetCatalogEntry(
70+
url=catalog.DEFAULT_CATALOG_URL,
71+
name="default",
72+
priority=1,
73+
install_allowed=True,
74+
)
75+
]
76+
77+
2978
def install_self_test_preset(
3079
manager: PresetManager, speckit_version: str = "0.1.5"
3180
) -> PresetManifest:
Lines changed: 6 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,10 @@
1-
"""Shared fixtures for mirrored preset command tests."""
1+
"""Load shared fixtures for mirrored preset command tests."""
22

33
from __future__ import annotations
44

5-
import shutil
6-
import tempfile
7-
from pathlib import Path
5+
from . import _fixtures
86

9-
import pytest
10-
import yaml
11-
12-
13-
@pytest.fixture
14-
def temp_dir():
15-
"""Create a temporary directory for tests."""
16-
tmpdir = tempfile.mkdtemp()
17-
yield Path(tmpdir)
18-
shutil.rmtree(tmpdir)
19-
20-
21-
@pytest.fixture
22-
def valid_pack_data():
23-
"""Return valid preset manifest data."""
24-
return {
25-
"schema_version": "1.0",
26-
"preset": {
27-
"id": "test-pack",
28-
"name": "Test Preset",
29-
"version": "1.0.0",
30-
"description": "A test preset",
31-
"author": "Test Author",
32-
"repository": "https://github.com/test/test-pack",
33-
"license": "MIT",
34-
},
35-
"requires": {"speckit_version": ">=0.1.0"},
36-
"provides": {
37-
"templates": [
38-
{
39-
"type": "template",
40-
"name": "spec-template",
41-
"file": "templates/spec-template.md",
42-
"description": "Custom spec template",
43-
"replaces": "spec-template",
44-
}
45-
]
46-
},
47-
"tags": ["testing", "example"],
48-
}
49-
50-
51-
@pytest.fixture
52-
def pack_dir(temp_dir, valid_pack_data):
53-
"""Create a complete preset directory structure."""
54-
preset_dir = temp_dir / "test-pack"
55-
preset_dir.mkdir()
56-
(preset_dir / "preset.yml").write_text(
57-
yaml.safe_dump(valid_pack_data), encoding="utf-8"
58-
)
59-
templates_dir = preset_dir / "templates"
60-
templates_dir.mkdir()
61-
(templates_dir / "spec-template.md").write_text(
62-
"# Custom Spec Template\n\nThis is a custom template.\n",
63-
encoding="utf-8",
64-
)
65-
return preset_dir
66-
67-
68-
@pytest.fixture
69-
def project_dir(temp_dir):
70-
"""Create a mock spec-kit project directory."""
71-
project = temp_dir / "project"
72-
project.mkdir()
73-
templates_dir = project / ".specify" / "templates"
74-
templates_dir.mkdir(parents=True)
75-
(templates_dir / "spec-template.md").write_text(
76-
"# Core Spec Template\n", encoding="utf-8"
77-
)
78-
(templates_dir / "plan-template.md").write_text(
79-
"# Core Plan Template\n", encoding="utf-8"
80-
)
81-
(templates_dir / "commands").mkdir()
82-
return project
7+
temp_dir = _fixtures.temp_dir
8+
valid_pack_data = _fixtures.valid_pack_data
9+
pack_dir = _fixtures.pack_dir
10+
project_dir = _fixtures.project_dir

‎tests/specify_cli/presets/test_command_add.py‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import zipfile
66
from pathlib import Path
77
from types import SimpleNamespace
8-
from unittest.mock import MagicMock
8+
from unittest.mock import ANY, MagicMock
99

1010
import pytest
1111
import yaml
@@ -253,6 +253,38 @@ def fake_install_from_zip(
253253

254254
assert captured == {"priority": 7, "catalog_name": "preset-catalog"}
255255

256+
def test_preset_add_uses_legacy_dependency_warning_seam(
257+
self, project_dir, pack_dir, monkeypatch
258+
):
259+
"""The extracted handler must honor patches at the legacy helper path."""
260+
from specify_cli.presets import _commands as preset_commands
261+
262+
manifest = SimpleNamespace(name="Test Preset", version="1.0.0")
263+
warning = MagicMock()
264+
monkeypatch.setattr(
265+
"specify_cli._require_specify_project", lambda: project_dir
266+
)
267+
monkeypatch.setattr("specify_cli.get_speckit_version", lambda: "1.0.0")
268+
monkeypatch.setattr(
269+
PresetManager,
270+
"install_from_directory",
271+
lambda _self, _path, _version, _priority: manifest,
272+
)
273+
monkeypatch.setattr(
274+
preset_commands,
275+
"_warn_unmet_extension_dependencies",
276+
warning,
277+
)
278+
279+
preset_commands.preset_add(
280+
preset_id=None,
281+
from_url=None,
282+
dev=str(pack_dir),
283+
priority=10,
284+
)
285+
286+
warning.assert_called_once_with(ANY, manifest)
287+
256288
def test_preset_add_from_url_rejects_insecure_redirect(
257289
self, project_dir, monkeypatch
258290
):

0 commit comments

Comments
 (0)