Fix init-force-preset-desync: reapply presets/extensions on init --here --force (#3995)

Apply the remediation from the bug assessment on issue #3990.

After integration setup() and manifest.save(), when --force is used
(re-initializing an existing project), call _register_presets_for_agent
and _register_extensions_for_agent so that previously-installed presets
and extensions are recomposed on top of the freshly-regenerated core
files. Without this, preset-composed files reverted to pure core while
the preset registry continued to report them as installed.

This mirrors the same pattern already present in integration_upgrade()
(added in PR #3853 / issue #3849 for the upgrade path).

Refs #3990

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-08-05 11:23:18 -05:00
committed by GitHub
co-authored by github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Copilot
parent f8a448f0a9
commit f31b2b45eb
2 changed files with 117 additions and 0 deletions
+24
View File
@@ -635,6 +635,30 @@ def register(app: typer.Typer) -> None:
)
manifest.save()
if force:
from ..integrations._helpers import (
_register_extensions_for_agent,
_register_presets_for_agent,
)
_register_extensions_for_agent(
project_path,
resolved_integration.key,
force=True,
continuing=(
"The project was re-initialized, but installed extensions"
" may need re-registration."
),
)
_register_presets_for_agent(
project_path,
resolved_integration.key,
continuing=(
"The project was re-initialized, but installed presets"
" may need re-registration."
),
)
integration_settings = _with_integration_setting(
{},
resolved_integration.key,
+93
View File
@@ -1067,6 +1067,99 @@ class TestInitIntegrationFlag:
assert "not updated" in result.output
def test_init_here_force_reapplies_installed_presets(self, tmp_path, monkeypatch):
"""Regression for #3990: init --here --force must call _register_presets_for_agent
after setup() so preset-composed files are not silently reverted to core."""
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / "force-preset-reapply"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
runner = CliRunner()
# First init to create a valid project structure.
result = runner.invoke(app, [
"init", "--here", "--force",
"--integration", "claude",
"--script", "sh",
"--ignore-agent-tools",
], catch_exceptions=False)
assert result.exit_code == 0, result.output
# Second init --here --force: verify _register_presets_for_agent is called.
# Patch at the source module since init.py does a lazy import of these functions.
mock_presets = MagicMock()
mock_extensions = MagicMock()
with (
patch(
"specify_cli.integrations._helpers._register_presets_for_agent",
mock_presets,
),
patch(
"specify_cli.integrations._helpers._register_extensions_for_agent",
mock_extensions,
),
):
result2 = runner.invoke(app, [
"init", "--here", "--force",
"--integration", "claude",
"--script", "sh",
"--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result2.exit_code == 0, result2.output
assert mock_presets.called, (
"_register_presets_for_agent was not called during init --here --force"
)
assert mock_extensions.called, (
"_register_extensions_for_agent was not called during init --here --force"
)
def test_init_here_without_force_does_not_reapply_presets(self, tmp_path):
"""Without --force (fresh project), _register_presets_for_agent should NOT be called."""
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / "no-force-preset"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
runner = CliRunner()
mock_presets = MagicMock()
with patch(
"specify_cli.integrations._helpers._register_presets_for_agent",
mock_presets,
):
result = runner.invoke(app, [
"init", "--here",
"--integration", "claude",
"--script", "sh",
"--ignore-agent-tools",
], catch_exceptions=False)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0, result.output
# On a fresh project without --force the reapply guard should not fire.
assert not mock_presets.called, (
"_register_presets_for_agent should not be called on a fresh init without --force"
)
class TestForceExistingDirectory:
"""Tests for --force merging into an existing named directory."""