Skip to content

Commit f31b2b4

Browse files
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>
1 parent f8a448f commit f31b2b4

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

‎src/specify_cli/commands/init.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,30 @@ def init(
635635
)
636636
manifest.save()
637637

638+
if force:
639+
from ..integrations._helpers import (
640+
_register_extensions_for_agent,
641+
_register_presets_for_agent,
642+
)
643+
644+
_register_extensions_for_agent(
645+
project_path,
646+
resolved_integration.key,
647+
force=True,
648+
continuing=(
649+
"The project was re-initialized, but installed extensions"
650+
" may need re-registration."
651+
),
652+
)
653+
_register_presets_for_agent(
654+
project_path,
655+
resolved_integration.key,
656+
continuing=(
657+
"The project was re-initialized, but installed presets"
658+
" may need re-registration."
659+
),
660+
)
661+
638662
integration_settings = _with_integration_setting(
639663
{},
640664
resolved_integration.key,

‎tests/integrations/test_cli.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,99 @@ def test_init_here_without_force_preserves_shared_infra(self, tmp_path):
10671067
assert "not updated" in result.output
10681068

10691069

1070+
def test_init_here_force_reapplies_installed_presets(self, tmp_path, monkeypatch):
1071+
"""Regression for #3990: init --here --force must call _register_presets_for_agent
1072+
after setup() so preset-composed files are not silently reverted to core."""
1073+
from unittest.mock import MagicMock, patch
1074+
1075+
from typer.testing import CliRunner
1076+
1077+
from specify_cli import app
1078+
1079+
project = tmp_path / "force-preset-reapply"
1080+
project.mkdir()
1081+
1082+
old_cwd = os.getcwd()
1083+
try:
1084+
os.chdir(project)
1085+
runner = CliRunner()
1086+
1087+
# First init to create a valid project structure.
1088+
result = runner.invoke(app, [
1089+
"init", "--here", "--force",
1090+
"--integration", "claude",
1091+
"--script", "sh",
1092+
"--ignore-agent-tools",
1093+
], catch_exceptions=False)
1094+
assert result.exit_code == 0, result.output
1095+
1096+
# Second init --here --force: verify _register_presets_for_agent is called.
1097+
# Patch at the source module since init.py does a lazy import of these functions.
1098+
mock_presets = MagicMock()
1099+
mock_extensions = MagicMock()
1100+
with (
1101+
patch(
1102+
"specify_cli.integrations._helpers._register_presets_for_agent",
1103+
mock_presets,
1104+
),
1105+
patch(
1106+
"specify_cli.integrations._helpers._register_extensions_for_agent",
1107+
mock_extensions,
1108+
),
1109+
):
1110+
result2 = runner.invoke(app, [
1111+
"init", "--here", "--force",
1112+
"--integration", "claude",
1113+
"--script", "sh",
1114+
"--ignore-agent-tools",
1115+
], catch_exceptions=False)
1116+
finally:
1117+
os.chdir(old_cwd)
1118+
1119+
assert result2.exit_code == 0, result2.output
1120+
assert mock_presets.called, (
1121+
"_register_presets_for_agent was not called during init --here --force"
1122+
)
1123+
assert mock_extensions.called, (
1124+
"_register_extensions_for_agent was not called during init --here --force"
1125+
)
1126+
1127+
def test_init_here_without_force_does_not_reapply_presets(self, tmp_path):
1128+
"""Without --force (fresh project), _register_presets_for_agent should NOT be called."""
1129+
from unittest.mock import MagicMock, patch
1130+
1131+
from typer.testing import CliRunner
1132+
1133+
from specify_cli import app
1134+
1135+
project = tmp_path / "no-force-preset"
1136+
project.mkdir()
1137+
1138+
old_cwd = os.getcwd()
1139+
try:
1140+
os.chdir(project)
1141+
runner = CliRunner()
1142+
mock_presets = MagicMock()
1143+
with patch(
1144+
"specify_cli.integrations._helpers._register_presets_for_agent",
1145+
mock_presets,
1146+
):
1147+
result = runner.invoke(app, [
1148+
"init", "--here",
1149+
"--integration", "claude",
1150+
"--script", "sh",
1151+
"--ignore-agent-tools",
1152+
], catch_exceptions=False)
1153+
finally:
1154+
os.chdir(old_cwd)
1155+
1156+
assert result.exit_code == 0, result.output
1157+
# On a fresh project without --force the reapply guard should not fire.
1158+
assert not mock_presets.called, (
1159+
"_register_presets_for_agent should not be called on a fresh init without --force"
1160+
)
1161+
1162+
10701163
class TestForceExistingDirectory:
10711164
"""Tests for --force merging into an existing named directory."""
10721165

0 commit comments

Comments
 (0)