Skip to content

fix(bundler): read the authoritative default_integration field, not only its legacy aliases - #3880

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/bundler-default-integration-field
Open

fix(bundler): read the authoritative default_integration field, not only its legacy aliases#3880
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/bundler-default-integration-field

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

active_integration() in src/specify_cli/bundler/lib/project.py resolves a project's active integration like this:

value = data.get("integration") or data.get("id") or data.get("active")

It never reads default_integration — the key the CLI actually writes.

integration_state.set_default_integration persists it (integration_state.py:250):

data["default_integration"] = integration_key

…and the canonical reader in the same module orders it the other way round (integration_state.py:199):

key = state.get("default_integration") or state.get("integration")

default_integration is authoritative; integration / id / active are legacy aliases. 57 references across 12 modules use default_integration. The bundler is the outlier.

Reproduction on current main (81bf741)

{"default_integration": "copilot"}                      -> None       <-- bug
{"integration": "copilot"}                              -> 'copilot'
{"default_integration": "copilot", "integrations": [..]} -> None       <-- bug

So a project initialised by any current version of the CLI looks to the bundler as though it has no active integration.

Why it matters

Anything in the bundler that keys off the active integration silently takes the "indeterminable" path. That includes the FR-019 clash guard in resolve_install_plan, which deliberately treats an unknown integration differently from a known one — so the guard's behaviour depends on whether the project happened to be written with the modern field or the legacy alias.

Fix

Read the authoritative field first, keep all three legacy aliases as fallbacks:

value = (
    data.get("default_integration")
    or data.get("integration")
    or data.get("id")
    or data.get("active")
)

Not a breaking change. Every payload that resolves today resolves to the same value: the aliases are still consulted, only after the field that should have been first. The only inputs whose behaviour changes are the ones that resolved to None incorrectly. A payload carrying both now prefers default_integration, which is what integration_state itself does.

Verification

  • Fail-before / pass-after: the two new tests fail on unpatched src and pass with the fix.
  • tests/integration/test_bundler_security_paths.py → 16 passed, 3 failed; those same 3 fail on clean main (Windows symlink-privilege tests, part of a 61-failure baseline I captured on 81bf741 before starting). Scoped regression check over tests/integration shows a failure set identical to that baseline.
  • uvx ruff@0.15.0 check src tests → clean

Tests go beside the existing test_active_integration_refuses_symlinked_specify_escape, the only current test of this function. Three cases: the authoritative field alone, both present (authoritative wins), and the legacy alias alone (still works).


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main; every value above is from output I ran.

`active_integration()` resolves a project's integration with

    data.get("integration") or data.get("id") or data.get("active")

and never looks at `default_integration` — which is the key the CLI
actually writes. `integration_state.set_default_integration` persists
`data["default_integration"] = integration_key`, and the canonical
reader in that module orders it the other way round:

    key = state.get("default_integration") or state.get("integration")

So a project initialised by any current version of the CLI looks to the
bundler as though it has no active integration:

    {"default_integration": "copilot"}  -> None      (expected "copilot")
    {"integration": "copilot"}          -> "copilot" (legacy alias)

That silently changes bundler behaviour that keys off the active
integration, including the FR-019 clash guard, which treats an
undeterminable integration differently from a known one.

Read `default_integration` first and keep the three legacy aliases as
fallbacks for projects initialised by older versions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates bundler integration detection to prioritize the authoritative project-state field.

Changes:

  • Reads default_integration before legacy aliases.
  • Adds coverage for precedence and backward compatibility.
  • Documentation contains inaccurate claims about the current writer.
Show a summary per file
File Description
src/specify_cli/bundler/lib/project.py Prioritizes default_integration.
tests/integration/test_bundler_security_paths.py Tests modern and legacy fields.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +85 to +91
# ``default_integration`` first: that is the key the CLI actually
# writes (``integration_state.set_default_integration``), and the
# canonical reader orders it the same way --
# ``state.get("default_integration") or state.get("integration")``.
# ``integration``/``id``/``active`` are legacy aliases kept for
# projects initialised by older versions, so reading only those made
# every current project look like it had no active integration.
Comment on lines +139 to +146
"""``default_integration`` is the key the CLI writes, so it must be read.

``integration_state.set_default_integration`` persists
``data["default_integration"] = integration_key``, and the canonical
reader is ``state.get("default_integration") or state.get("integration")``.
Reading only the legacy aliases made every current project look as though
it had no active integration.
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants