From 892dd656f25ab34204d2516e3555d51be10b898f Mon Sep 17 00:00:00 2001 From: Ali jawwad <33836051+jawwad-ali@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:49:29 +0500 Subject: [PATCH] fix(shared-infra): refresh_shared_templates preserves recovered user files (#3378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(shared-infra): refresh_shared_templates preserves recovered user files refresh_shared_templates skipped a shared template only when it was untracked or modified, ignoring the manifest's is_recovered marker that install_shared_infra already honors. So a pre-existing user template (adopted via record_existing(recovered=True), hence tracked and hash-unmodified) was silently overwritten with bundled content on refresh — the exact data-loss class that #2918 fixed for install_shared_infra. Add the is_recovered check to the skip predicate. Co-Authored-By: Claude Fable 5 * docs(shared-infra): include recovered files in refresh skip warning The skip predicate now also skips recovered (pre-existing user) files, so the warning saying only 'modified or untracked' could mislead a user into thinking they edited a file that was simply recorded as recovered. Reword to 'modified, untracked, or preserved (recovered)'. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- src/specify_cli/shared_infra.py | 7 ++++-- tests/integrations/test_cli.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/shared_infra.py b/src/specify_cli/shared_infra.py index 0685b6c9b..581abfb51 100644 --- a/src/specify_cli/shared_infra.py +++ b/src/specify_cli/shared_infra.py @@ -328,7 +328,10 @@ def refresh_shared_templates( _ensure_safe_shared_destination(project_path, dst) rel = dst.relative_to(project_path).as_posix() if dst.exists() and not force: - if rel not in tracked_files or rel in modified: + if rel not in tracked_files or rel in modified or manifest.is_recovered(rel): + # Never overwrite a recovered (pre-existing user) file without + # --force, matching install_shared_infra's is_recovered gate + # (#2918). Without this, refresh clobbers user content. skipped_files.append(rel) continue @@ -344,7 +347,7 @@ def refresh_shared_templates( if skipped_files: console.print( - f"[yellow]⚠[/yellow] {len(skipped_files)} modified or untracked shared template file(s) were not updated:" + f"[yellow]⚠[/yellow] {len(skipped_files)} modified, untracked, or preserved (recovered) shared template file(s) were not updated:" ) for rel in skipped_files: console.print(f" {rel}") diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index ed978cbb5..cb2d136e6 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -2073,3 +2073,44 @@ class TestIntegrationCatalogDiscoveryCLI: assert listing.exit_code == 0, listing.output assert "default" in listing.output assert "community" in listing.output + + +def test_refresh_shared_templates_preserves_recovered_user_file(tmp_path): + """refresh_shared_templates must not overwrite a recovered (pre-existing + user) template without --force, matching install_shared_infra's gate (#2918). + """ + from specify_cli.shared_infra import ( + load_speckit_manifest, + refresh_shared_templates, + ) + + project = tmp_path / "proj" + templates_dir = project / ".specify" / "templates" + templates_dir.mkdir(parents=True) + user_file = templates_dir / "spec-template.md" + user_file.write_text("# USER CUSTOM CONTENT\n", encoding="utf-8") + + # Record the pre-existing file as recovered (its hash was adopted, not written). + manifest = load_speckit_manifest(project, version="test", console=_NoopConsole()) + rel = ".specify/templates/spec-template.md" + manifest.record_existing(rel, recovered=True) + manifest.save() + + # Bundled source ships a different body for the same template. + core_pack = tmp_path / "core-pack" + src = core_pack / "templates" + src.mkdir(parents=True) + (src / "spec-template.md").write_text("# BUNDLED CONTENT v2\n", encoding="utf-8") + + refresh_shared_templates( + project, + version="test", + core_pack=core_pack, + repo_root=tmp_path / "unused", + console=_NoopConsole(), + invoke_separator=".", + force=False, + ) + + # Recovered user content must survive (fail-before: replaced by bundled body). + assert user_file.read_text(encoding="utf-8") == "# USER CUSTOM CONTENT\n"