Repository navigation
fix(integrations): refresh manifest hashes after init --force and switch rollback - #5193
Merged
mnriem merged 1 commit intoOct 30, 2026
Conversation
…tch rollback `specify init --force` and the rollback of a failed `integration switch` re-register installed extensions and presets after the integration manifest is written, but never refresh the hashes of files those presets rewrite. A preset that overrides a core command then shows up as a modified file in `integration status`, and `integration upgrade` refuses without `--force`. github#5145 fixed the same gap for `switch` and `use`. `init --force` saves a fresh manifest right before registration, so it resyncs every tracked file, like `upgrade` and a switch to a new target. The rollback restores an integration whose manifest was written earlier, so it snapshots the unmodified files first, like `use`, and earlier user edits are still reported as modified. Both steps stay best-effort. Refs github#5078 Assisted-by: Codex CLI (model: gpt-6.1-sol, autonomous) Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the manifest-hash synchronization changes.
No open issues 🎉
What changed in this PR
Closes the remaining manifest-hash gaps after preset registration in init --force and failed integration-switch rollback, extending the fixes from #5145. This prevents legitimate preset overrides from appearing as user modifications and blocking later upgrades.
Changes:
- Refreshes hashes after forced reinitialization while preserving recovered markers on unchanged files.
- Refreshes rollback hashes selectively, preserving pre-existing modification detection and the original switch error.
- Adds regression coverage for preset overrides, user edits, recovered markers, and manifest failures.
Before-and-after regression results are documented in the PR; tests were not rerun here.
| File | Description |
|---|---|
tests/specify_cli/test_command_init.py |
Tests preset-hash synchronization and recovered-marker preservation. |
tests/specify_cli/integrations/test_command_switch.py |
Tests rollback synchronization, user-edit detection, and manifest-failure handling. |
src/specify_cli/integrations/command_switch.py |
Snapshots and selectively refreshes fallback manifest hashes during rollback. |
src/specify_cli/command_init.py |
Refreshes manifest hashes after reinitialization registration. |
Review effort: Balanced (auto)
Collaborator
|
Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Two more paths re-register installed extensions and presets after the integration manifest is written, but never refresh the hashes of files those presets rewrite:
specify init --forceinto an existing project, and the rollback after a failedspecify integration switch. A preset that overrides a core command then leaves a stale hash,integration statusreports the file as modified, andintegration upgraderefuses without--force. This is the gap Copilot named on #5145 ("init --force, and switch rollback still skip them"); #5145 fixedswitchanduse.init --forcesaves a fresh manifest right before registration, the same situation asupgrade(#4696) and a switch to a new target. It now calls_resync_manifest_after_registrationwith every tracked file, so files registration didn't change keep their recovered markers. Afterinit --forcethe project is in sync, including one where an earlier preset command had left a stale hash.The rollback restores an integration whose manifest was written when it was installed, like
use. It now takes_snapshot_manifest_before_registration()before re-registering and refreshes only files that were unmodified before and changed by registration. A file the user edited earlier keeps its old hash and is still reported as modified. Both steps are best-effort: a missing or corrupt manifest, or a failed save, prints a warning, and the switch error stays the primary message with exit code 1.The uninstall fallback (
integration uninstallof the default integration) has the same gap. I left it out because #5100 rewritescommand_uninstall.pyand moves that code into_operation_uninstall.py.Repro with the real CLI and the preset from #5078 (overrides
speckit.tasks). For the rollback, a plain file named.geminimakes the Gemini install fail:main(2a23a84)integration statusafterinit --forceupgrade codexafterinit --forceintegration statusafter the failed switchupgrade codexafter the failed switchRefs #5078
Testing
uv run specify --helpuv sync && uv run pytestNew tests, through the CLI with a real preset:
test_command_init.py::test_init_force_syncs_manifest_hash_for_preset_override: afterinit --forcethe hash matches the preset-rendered skill, status shows 0 modified files andupgradewithout--forceexits 0.init --forcestill replaces a user-edited skill, as onmain.test_command_init.py::test_init_force_keeps_recovered_markers: a recovered file that registration doesn't touch stays recovered.test_command_switch.py::test_failed_switch_syncs_fallback_manifest_without_blessing_user_edits[None|plan|tasks]: after the rollback the fallback's hash matches the file; a skill the user edited before the switch (plan, which the preset doesn't touch, andtasks, which it rewrites) keeps its old hash andupgradestill refuses.test_command_switch.py::test_failed_switch_manifest_refresh_is_best_effort[missing|corrupt|save-error]: registration still runs, a missing manifest isn't created, a corrupt one is left as it is, and the original switch error and exit code survive.With the two source files reverted to
main, 5 of the 8 cases fail. Thetasksedit case, the missing-manifest case and the recovered-marker test pass onmaintoo, sincemainnever re-hashes. With theunmodified_filesargument removed from the rollback, both edit cases fail with "pre-existing edits must not be recorded as pristine"; removed frominit, the recovered-marker test fails. With thefallback_manifest is not Nonecheck removed, the missing and corrupt cases fail.Full suite on Linux, Python 3.13: 13450 passed, 441 skipped (
uv sync --extra test, thenpytest -n 2, 9 min). Ruff 0.15.0 (ruff check src tests) passes. I haven't run it on Windows.AI Disclosure
AI disclosure: Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, offline sandbox) reproduced the paths on
mainand wrote the fix and the tests. Claude Code (model: claude-opus-5-5, autonomous) reviewed the change, reran the repro with the real CLI onmainand the branch, ran the full suite and lint, and drafted this description. The commit carriesAssisted-by:trailers for both.