mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
`resolve_install_plan`'s two FR-019 guards are a truthiness test and an
`is None` test:
if active_integration and required != active_integration: # clash
if active_integration is None and not integration_explicit: # indeterminate
An empty string satisfies neither, so it falls through to
`effective_integration = required` and the bundle's pinned integration is
silently adopted — the exact outcome the docstring says the guard prevents
("resolution fails instead of silently adopting the bundle's required
integration").
active=None -> BundlerError: ... could not be determined
active='' (blank) -> effective_integration='copilot' <-- silent adopt
active='claude' -> BundlerError: ... targets integration 'copilot'
Normalise a blank value to None before the guards, and strip first to
match the writer (`integration_state.clean_integration_key`, which returns
`None` for empty/whitespace and strips otherwise) so a padded value is not
reported as clashing with its own unpadded form.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
"""Unit tests for the resolver: version gate and integration compatibility."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from specify_cli.bundler import BundlerError
|
|
from specify_cli.bundler.models.manifest import BundleManifest
|
|
from specify_cli.bundler.services.resolver import resolve_install_plan
|
|
from tests.bundler_helpers import valid_manifest_dict
|
|
|
|
|
|
def _manifest(**overrides) -> BundleManifest:
|
|
return BundleManifest.from_dict(valid_manifest_dict(**overrides))
|
|
|
|
|
|
def test_plan_expands_all_components():
|
|
plan = resolve_install_plan(
|
|
_manifest(), speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
assert plan.component_count == 4
|
|
assert plan.bundle_id == "demo-bundle"
|
|
|
|
|
|
def test_version_gate_refuses_incompatible():
|
|
manifest = _manifest(requires={"speckit_version": ">=99.0.0"})
|
|
with pytest.raises(BundlerError, match="requires Spec Kit"):
|
|
resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
|
|
|
|
def test_integration_clash_halts():
|
|
manifest = _manifest(integration={"id": "claude"})
|
|
with pytest.raises(BundlerError, match="active integration"):
|
|
resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
|
|
|
|
def test_agnostic_inherits_active_integration():
|
|
plan = resolve_install_plan(
|
|
_manifest(), speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
assert plan.effective_integration == "copilot"
|
|
|
|
|
|
def test_matching_integration_is_allowed():
|
|
manifest = _manifest(integration={"id": "copilot"})
|
|
plan = resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
assert plan.effective_integration == "copilot"
|
|
|
|
|
|
def test_pinned_integration_with_indeterminate_active_fails():
|
|
# FR-019 guard: a bundle that pins an integration must not silently adopt it
|
|
# when the project's active integration cannot be determined.
|
|
manifest = _manifest(integration={"id": "claude"})
|
|
with pytest.raises(BundlerError, match="could not be determined"):
|
|
resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration=None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("blank", ["", " ", "\t"])
|
|
def test_pinned_integration_with_blank_active_fails(blank):
|
|
"""A blank active integration is indeterminate, not a match.
|
|
|
|
The clash guard is a truthiness test and the indeterminate guard is an
|
|
`is None` test, so `""` satisfied neither and fell through to
|
|
`effective_integration = required` — silently adopting the bundle's pinned
|
|
integration, the exact outcome the docstring says the guard prevents.
|
|
"""
|
|
manifest = _manifest(integration={"id": "claude"})
|
|
with pytest.raises(BundlerError, match="could not be determined"):
|
|
resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration=blank
|
|
)
|
|
|
|
|
|
def test_padded_active_integration_is_not_a_clash_with_itself():
|
|
"""A padded value must strip, like the writer's clean_integration_key,
|
|
rather than be reported as clashing with its own unpadded form."""
|
|
manifest = _manifest(integration={"id": "claude"})
|
|
plan = resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration=" claude "
|
|
)
|
|
assert plan.effective_integration == "claude"
|
|
|
|
|
|
def test_pinned_integration_with_indeterminate_active_allows_explicit_override():
|
|
manifest = _manifest(integration={"id": "claude"})
|
|
plan = resolve_install_plan(
|
|
manifest,
|
|
speckit_version="0.11.2",
|
|
active_integration="claude",
|
|
integration_explicit=True,
|
|
)
|
|
assert plan.effective_integration == "claude"
|
|
|
|
|
|
def test_tool_requirements_become_warnings():
|
|
manifest = _manifest(requires={"speckit_version": ">=0.1.0", "tools": ["docker"]})
|
|
plan = resolve_install_plan(
|
|
manifest, speckit_version="0.11.2", active_integration="copilot"
|
|
)
|
|
assert any("docker" in w for w in plan.warnings)
|