mirror of
https://github.com/github/spec-kit.git
synced 2026-07-11 18:59:53 +08:00
* fix(bundler): enforce version pin on bundled preset/extension installs The bundled install branch of _PresetKindManager/_ExtensionKindManager called install_from_directory and returned before _assert_pinned_version, so a bundle manifest pinning e.g. 2.0.0 would silently install the bundled asset's own version (1.0.0) — the pin was only enforced on the catalog path. _WorkflowKindManager already enforces it unconditionally. Read the bundled asset's declared version from its manifest (best-effort; None => cannot enforce, matching the catalog 'advertises no version' escape hatch) and call _assert_pinned_version before install, in both bundled branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bundler): address review on bundled version-pin check - Make _assert_pinned_version's error source-agnostic ('resolved version' / 'the source') so bundled preset.yml/extension.yml mismatches read correctly, not just catalog ones. - Type-guard _bundled_manifest_version: only a non-empty string version is usable; missing/non-string/whitespace -> None ('cannot enforce'). - Add bundled-preset success-path test (matching pin + version=None both proceed to install_from_directory), mirroring the extension test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
218 lines
8.5 KiB
Python
218 lines
8.5 KiB
Python
"""Unit tests for the primitive-dispatch bridge (T044).
|
|
|
|
Covers routing, offline gating, and the network-aware ``DefaultPrimitiveInstaller``
|
|
seam — without touching real catalogs or the network (Constitution Principle II,
|
|
offline-first).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from specify_cli.bundler import BundlerError
|
|
from specify_cli.bundler.models.manifest import ComponentRef
|
|
from specify_cli.bundler.services.adapters import DefaultPrimitiveInstaller
|
|
from specify_cli.bundler.services.primitives import (
|
|
_ExtensionKindManager,
|
|
_PresetKindManager,
|
|
_StepKindManager,
|
|
_WorkflowKindManager,
|
|
primitive_manager,
|
|
)
|
|
|
|
|
|
def _component(kind: str, cid: str = "x") -> ComponentRef:
|
|
return ComponentRef(kind=kind, id=cid)
|
|
|
|
|
|
def test_primitive_manager_routes_each_kind(tmp_path: Path):
|
|
assert isinstance(primitive_manager("presets", tmp_path), _PresetKindManager)
|
|
assert isinstance(primitive_manager("extensions", tmp_path), _ExtensionKindManager)
|
|
assert isinstance(primitive_manager("workflows", tmp_path), _WorkflowKindManager)
|
|
assert isinstance(primitive_manager("steps", tmp_path), _StepKindManager)
|
|
|
|
|
|
def test_primitive_manager_rejects_unknown_kind(tmp_path: Path):
|
|
with pytest.raises(BundlerError, match="Unknown component kind"):
|
|
primitive_manager("bogus", tmp_path)
|
|
|
|
|
|
def test_offline_preset_not_bundled_refuses(tmp_path: Path):
|
|
manager = primitive_manager("presets", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="network access is disabled"):
|
|
manager.install(_component("presets", "definitely-not-bundled"))
|
|
|
|
|
|
def test_offline_extension_not_bundled_refuses(tmp_path: Path):
|
|
manager = primitive_manager("extensions", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="network access is disabled"):
|
|
manager.install(_component("extensions", "definitely-not-bundled"))
|
|
|
|
|
|
def test_offline_workflow_refuses_without_network(tmp_path: Path):
|
|
manager = primitive_manager("workflows", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="network access is disabled"):
|
|
manager.install(_component("workflows"))
|
|
|
|
|
|
def test_offline_step_refuses_without_network(tmp_path: Path):
|
|
manager = primitive_manager("steps", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="network access is disabled"):
|
|
manager.install(_component("steps"))
|
|
|
|
|
|
def test_default_installer_threads_allow_network(tmp_path: Path):
|
|
installer = DefaultPrimitiveInstaller(allow_network=False)
|
|
with pytest.raises(BundlerError, match="network access is disabled"):
|
|
installer.install(tmp_path, _component("workflows"))
|
|
|
|
|
|
def test_offline_workflow_allows_bundled(tmp_path: Path, monkeypatch):
|
|
# A workflow that ships with Spec Kit must install even with --offline.
|
|
import specify_cli
|
|
import specify_cli._assets as assets
|
|
|
|
monkeypatch.setattr(
|
|
assets, "_locate_bundled_workflow", lambda wid: tmp_path / "wf"
|
|
)
|
|
calls: list[str] = []
|
|
monkeypatch.setattr(specify_cli, "workflow_add", lambda wid: calls.append(wid))
|
|
|
|
manager = primitive_manager("workflows", tmp_path, allow_network=False)
|
|
manager.install(_component("workflows", "bundled-wf"))
|
|
|
|
assert calls == ["bundled-wf"]
|
|
|
|
|
|
def test_assert_pinned_version_matches_passes():
|
|
from specify_cli.bundler.services.primitives import _assert_pinned_version
|
|
|
|
# Equal (including v-prefix/normalization) is accepted; no version pins are no-ops.
|
|
_assert_pinned_version("Preset", "p", "2.0.0", "2.0.0")
|
|
_assert_pinned_version("Preset", "p", "2.0.0", "v2.0.0")
|
|
_assert_pinned_version("Preset", "p", None, "9.9.9")
|
|
_assert_pinned_version("Preset", "p", "2.0.0", None)
|
|
|
|
|
|
def test_assert_pinned_version_mismatch_raises():
|
|
from specify_cli.bundler.services.primitives import _assert_pinned_version
|
|
|
|
with pytest.raises(BundlerError, match="pinned to version 2.0.0"):
|
|
_assert_pinned_version("Preset", "preset-a", "2.0.0", "3.1.0")
|
|
|
|
|
|
def test_workflow_version_mismatch_refuses(tmp_path: Path, monkeypatch):
|
|
from specify_cli.workflows.catalog import WorkflowCatalog
|
|
|
|
monkeypatch.setattr(
|
|
WorkflowCatalog, "get_workflow_info", lambda self, wid: {"version": "9.9.9"}
|
|
)
|
|
manager = primitive_manager("workflows", tmp_path, allow_network=True)
|
|
component = ComponentRef(kind="workflows", id="wf-a", version="0.3.0")
|
|
with pytest.raises(BundlerError, match="pinned to version 0.3.0"):
|
|
manager.install(component)
|
|
|
|
|
|
def test_preset_install_preserves_explicit_zero_priority(tmp_path: Path, monkeypatch):
|
|
import specify_cli._assets as assets
|
|
|
|
calls = {}
|
|
|
|
class _FakeManager:
|
|
def install_from_directory(self, directory, speckit_version, priority):
|
|
calls["priority"] = priority
|
|
|
|
monkeypatch.setattr(assets, "_locate_bundled_preset", lambda cid: tmp_path)
|
|
|
|
manager = primitive_manager("presets", tmp_path, allow_network=False)
|
|
manager._manager = _FakeManager()
|
|
manager.install(ComponentRef(kind="presets", id="p", priority=0))
|
|
|
|
# An explicit priority of 0 must be passed through, not replaced by default.
|
|
assert calls["priority"] == 0
|
|
|
|
|
|
def _write_manifest(path: Path, root_key: str, version: str) -> Path:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
(path / f"{root_key}.yml").write_text(
|
|
f"{root_key}:\n id: x\n version: {version}\n", encoding="utf-8"
|
|
)
|
|
return path
|
|
|
|
|
|
def test_bundled_extension_pin_mismatch_refuses(tmp_path: Path, monkeypatch):
|
|
"""A bundled extension whose version != the manifest pin must be refused
|
|
(the bundled path previously skipped the pin the catalog path enforces)."""
|
|
import specify_cli._assets as assets
|
|
from specify_cli.extensions import ExtensionManager
|
|
|
|
bundled = _write_manifest(tmp_path / "ext", "extension", "1.0.0")
|
|
monkeypatch.setattr(assets, "_locate_bundled_extension", lambda cid: bundled)
|
|
called: list = []
|
|
monkeypatch.setattr(
|
|
ExtensionManager, "install_from_directory",
|
|
lambda self, *a, **k: called.append(a),
|
|
)
|
|
|
|
manager = primitive_manager("extensions", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="pinned to version 2.0.0"):
|
|
manager.install(ComponentRef(kind="extensions", id="my-ext", version="2.0.0"))
|
|
assert called == [] # install must not proceed
|
|
|
|
|
|
def test_bundled_extension_pin_match_installs(tmp_path: Path, monkeypatch):
|
|
import specify_cli._assets as assets
|
|
from specify_cli.extensions import ExtensionManager
|
|
|
|
bundled = _write_manifest(tmp_path / "ext", "extension", "1.0.0")
|
|
monkeypatch.setattr(assets, "_locate_bundled_extension", lambda cid: bundled)
|
|
called: list = []
|
|
monkeypatch.setattr(
|
|
ExtensionManager, "install_from_directory",
|
|
lambda self, *a, **k: called.append(a),
|
|
)
|
|
|
|
manager = primitive_manager("extensions", tmp_path, allow_network=False)
|
|
# matching pin, and unpinned, both install cleanly
|
|
manager.install(ComponentRef(kind="extensions", id="my-ext", version="1.0.0"))
|
|
manager.install(ComponentRef(kind="extensions", id="my-ext", version=None))
|
|
assert len(called) == 2
|
|
|
|
|
|
def test_bundled_preset_pin_mismatch_refuses(tmp_path: Path, monkeypatch):
|
|
import specify_cli._assets as assets
|
|
from specify_cli.presets import PresetManager
|
|
|
|
bundled = _write_manifest(tmp_path / "preset", "preset", "1.0.0")
|
|
monkeypatch.setattr(assets, "_locate_bundled_preset", lambda cid: bundled)
|
|
called: list = []
|
|
monkeypatch.setattr(
|
|
PresetManager, "install_from_directory",
|
|
lambda self, *a, **k: called.append(a),
|
|
)
|
|
|
|
manager = primitive_manager("presets", tmp_path, allow_network=False)
|
|
with pytest.raises(BundlerError, match="pinned to version 2.0.0"):
|
|
manager.install(ComponentRef(kind="presets", id="my-preset", version="2.0.0"))
|
|
assert called == []
|
|
|
|
|
|
def test_bundled_preset_pin_match_installs(tmp_path: Path, monkeypatch):
|
|
import specify_cli._assets as assets
|
|
from specify_cli.presets import PresetManager
|
|
|
|
bundled = _write_manifest(tmp_path / "preset", "preset", "1.0.0")
|
|
monkeypatch.setattr(assets, "_locate_bundled_preset", lambda cid: bundled)
|
|
called: list = []
|
|
monkeypatch.setattr(
|
|
PresetManager, "install_from_directory",
|
|
lambda self, *a, **k: called.append(a),
|
|
)
|
|
|
|
manager = primitive_manager("presets", tmp_path, allow_network=False)
|
|
# matching pin, and unpinned, both proceed to install
|
|
manager.install(ComponentRef(kind="presets", id="my-preset", version="1.0.0"))
|
|
manager.install(ComponentRef(kind="presets", id="my-preset", version=None))
|
|
assert len(called) == 2
|