Skip to content

Commit 197dde6

Browse files
jawwad-aliclaude
andauthored
fix(bundler): treat a blank active integration as indeterminate in FR-019 (#3886)
`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>
1 parent f2583e6 commit 197dde6

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/specify_cli/bundler/services/resolver.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ def resolve_install_plan(
7777

7878
# FR-019: integration-compatibility — a bundle that pins a different
7979
# integration than the project's active one halts (no silent change).
80+
#
81+
# A blank integration arrives as ``""``, not ``None`` — which is not a usable
82+
# integration id but satisfied NEITHER guard below (the first is a truthiness
83+
# test, the second an ``is None`` test), so a pinned bundle was silently
84+
# adopted: precisely the outcome this guard exists to prevent. Treat blank as
85+
# indeterminate, and strip first like the writer
86+
# (``integration_state.clean_integration_key``) so a padded value is not
87+
# reported as clashing with itself.
88+
if active_integration is not None:
89+
active_integration = active_integration.strip() or None
8090
effective_integration = active_integration
8191
if manifest.integration is not None:
8292
required = manifest.integration.id

tests/unit/test_bundler_resolver.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ def test_pinned_integration_with_indeterminate_active_fails():
6262
)
6363

6464

65+
@pytest.mark.parametrize("blank", ["", " ", "\t"])
66+
def test_pinned_integration_with_blank_active_fails(blank):
67+
"""A blank active integration is indeterminate, not a match.
68+
69+
The clash guard is a truthiness test and the indeterminate guard is an
70+
`is None` test, so `""` satisfied neither and fell through to
71+
`effective_integration = required` — silently adopting the bundle's pinned
72+
integration, the exact outcome the docstring says the guard prevents.
73+
"""
74+
manifest = _manifest(integration={"id": "claude"})
75+
with pytest.raises(BundlerError, match="could not be determined"):
76+
resolve_install_plan(
77+
manifest, speckit_version="0.11.2", active_integration=blank
78+
)
79+
80+
81+
def test_padded_active_integration_is_not_a_clash_with_itself():
82+
"""A padded value must strip, like the writer's clean_integration_key,
83+
rather than be reported as clashing with its own unpadded form."""
84+
manifest = _manifest(integration={"id": "claude"})
85+
plan = resolve_install_plan(
86+
manifest, speckit_version="0.11.2", active_integration=" claude "
87+
)
88+
assert plan.effective_integration == "claude"
89+
90+
6591
def test_pinned_integration_with_indeterminate_active_allows_explicit_override():
6692
manifest = _manifest(integration={"id": "claude"})
6793
plan = resolve_install_plan(

0 commit comments

Comments
 (0)