@@ -870,151 +870,121 @@ def test_hoist_aborts_before_writing_when_declined(monkeypatch: pytest.MonkeyPat
870870
871871
872872def test_installing_the_template_cleans_the_repo_and_sets_hooks (
873- monkeypatch : pytest .MonkeyPatch , git_repo : Path
873+ monkeypatch : pytest .MonkeyPatch , tmp_path : Path
874874) -> None :
875- """Install cleans template files, syncs dependencies, activates hooks, and is idempotent."""
876- (git_repo / "pyproject.toml" ).write_text (
877- "[project]\n "
878- 'name = "old-name"\n '
879- 'version = "2.3.4"\n '
880- 'description = "the user\' s own project"\n '
881- 'authors = [{ name = "someone" }]\n '
882- 'requires-python = ">=3.11"\n '
883- "\n [project.scripts]\n "
884- 'harness = "harness.cli:main"\n '
885- "\n [tool.pyright]\n "
886- 'typeCheckingMode = "strict"\n '
887- 'include = ["src", "harness"]\n '
888- "\n [tool.pytest.ini_options]\n "
889- 'addopts = ["-ra"]\n '
890- 'testpaths = ["tests", "harness"]\n '
891- 'pythonpath = ["src", "harness"]\n '
892- "\n [tool.coverage]\n "
893- 'run.source = ["src", "harness"]\n '
894- "report.fail_under = 100\n "
895- "\n [tool.complexipy]\n "
896- 'paths = ["src", "harness"]\n '
897- "max-complexity-allowed = 10\n "
898- "\n [tool.ruff]\n "
899- 'exclude = [".git"]\n '
900- "\n [tool.pylint.main]\n "
901- 'ignore = [".git"]\n ' ,
902- encoding = "utf-8" ,
875+ """Install a fresh template checkout with real process, filesystem, and Git boundaries."""
876+ template_repo = tmp_path / "template"
877+ subprocess .run (
878+ ["git" , "clone" , "--quiet" , "--shared" , str (REPO_ROOT ), str (template_repo )],
879+ check = True ,
903880 )
904- (git_repo / "uv.lock" ).touch ()
905- replacement_project = "[project]\n name = 'replacement'\n "
906- (git_repo / "temp.pyproject.toml" ).write_text (replacement_project , encoding = "utf-8" )
907- template_files = (".banner.svg" , ".diagram.png" , ".infin.png" , ".loops_agents.svg" , ".loops.svg" )
908- (git_repo / ".assets" ).mkdir ()
909- for file_name in template_files :
910- (git_repo / ".assets" / file_name ).touch ()
911- (git_repo / ".github" / "workflows" ).mkdir (parents = True )
912- (git_repo / ".github" / "workflows" / "publish.yml" ).touch ()
913- (git_repo / "CONTRIBUTING.md" ).touch ()
914- (git_repo / "dist" ).mkdir ()
915- (git_repo / "dist" / "stale.whl" ).touch ()
916- for directory in ("harness/tests" , "preferences" , "tests/preferences" ):
917- (git_repo / directory ).mkdir (parents = True )
918- monkeypatch .setattr (cli , "which" , which_finds (("timeout" ,)))
919- monkeypatch .setattr (cli , "REPO_ROOT_STR" , str (git_repo ))
920- toolchain = stub_toolchain (git_repo / ".git" )
921- monkeypatch .setattr (subprocess , "run" , toolchain )
881+ expected_project = (template_repo / "harness" / "temp.pyproject.toml" ).read_text (encoding = "utf-8" )
882+ env_bin = template_repo / ".venv" / ("Scripts" if sys .platform == "win32" else "bin" )
883+ monkeypatch .setattr (cli , "REPO_ROOT" , template_repo )
884+ monkeypatch .setattr (cli , "REPO_ROOT_STR" , str (template_repo ))
885+ monkeypatch .setattr (gates (), "repo_root" , template_repo )
886+ monkeypatch .setattr (cli , "infer_env_manager" , Mock (return_value = (env_bin , ["uv" , "sync" ])))
887+ monkeypatch .setattr (cli , "check_for_timeout_and_prompt" , Mock (return_value = "timeout" ))
888+ binary = harness_executable (env_bin )
889+ binary .parent .mkdir (parents = True )
890+ binary .touch ()
891+ run = Mock (
892+ wraps = subprocess .run ,
893+ side_effect = [subprocess .CompletedProcess (("uv" , "sync" ), 0 ), * ([DEFAULT ] * 8 )],
894+ )
895+ monkeypatch .setattr (subprocess , "run" , run )
922896
923- monkeypatch .setattr (Path , "is_file" , create_autospec (Path .is_file , wraps = Path .is_file ))
924- monkeypatch .setattr (Path , "replace" , create_autospec (Path .replace , wraps = Path .replace ))
925- monkeypatch .setattr (Path , "unlink" , create_autospec (Path .unlink , wraps = Path .unlink ))
926- monkeypatch .setattr (cli , "rmtree" , Mock (wraps = cli .rmtree ))
927897 result = runner .invoke (cli .app , ["install" ])
928898
929- assert result .exit_code == 0
930- assert (git_repo / "pyproject.toml" ).read_text (encoding = "utf-8" ) == replacement_project
931- assert not (git_repo / "temp.pyproject.toml" ).exists ()
932- assert (git_repo / "README.md" ).read_text (encoding = "utf-8" ) == "seed\n "
933- assert not (git_repo / "README.template.md" ).exists ()
934- assert not (git_repo / ".assets" ).exists ()
935- assert not (git_repo / ".github" / "workflows" / "publish.yml" ).exists ()
936- assert not (git_repo / "CONTRIBUTING.md" ).exists ()
937- assert not (git_repo / "dist" ).exists ()
938- assert not (git_repo / "harness" / "tests" ).exists ()
939- assert (git_repo / "preferences" ).is_dir ()
940- assert (git_repo / "tests" / "preferences" ).is_dir ()
941- Path .is_file .assert_any_call (git_repo / "README.template.md" )
942- Path .is_file .assert_any_call (git_repo / "temp.pyproject.toml" )
943- Path .replace .assert_any_call (git_repo / "README.template.md" , git_repo / "README.md" )
944- Path .replace .assert_any_call (git_repo / "temp.pyproject.toml" , git_repo / "pyproject.toml" )
945- Path .unlink .assert_any_call (git_repo / ".github" / "workflows" / "publish.yml" , missing_ok = True )
946- Path .unlink .assert_any_call (git_repo / "CONTRIBUTING.md" , missing_ok = True )
947- cli .rmtree .assert_any_call (git_repo / "dist" )
948- cli .rmtree .assert_any_call (git_repo / "harness" / "tests" )
949- cli .rmtree .assert_any_call (git_repo / ".assets" )
950- toolchain .assert_any_call (("uv" , "sync" ), cwd = str (git_repo ), check = True )
951- recorded_harness = (git_repo / ".git" / "harness-path" ).read_text (encoding = "utf-8" ).strip ()
952- env_bin = git_repo / ".venv" / ("Scripts" if sys .platform == "win32" else "bin" )
899+ assert result .exit_code == 0 , result .output
900+ assert run .call_args_list [0 ] == call (("uv" , "sync" ), cwd = str (template_repo ), check = True )
901+ recorded_harness = (template_repo / ".git" / "harness-path" ).read_text (encoding = "utf-8" ).strip ()
953902 assert normalized_path (recorded_harness ) == normalized_path (harness_executable (env_bin ))
954- toolchain .assert_any_call (
955- ["git" , "config" , "core.hooksPath" , ".githooks" ], cwd = cli .REPO_ROOT_STR , check = True
956- )
957-
958- head = gate .run_git (["rev-parse" , "HEAD" ], git_repo )
959- status = gate .run_git (["status" , "--porcelain" ], git_repo )
960- generated_project = (git_repo / "pyproject.toml" ).read_text (encoding = "utf-8" )
961- again = runner .invoke (cli .app , ["install" ])
962-
963- assert again .exit_code == 0
964- assert gate .run_git (["rev-parse" , "HEAD" ], git_repo ) == head
965- assert gate .run_git (["status" , "--porcelain" ], git_repo ) == status
966- assert (git_repo / "pyproject.toml" ).read_text (encoding = "utf-8" ) == generated_project
967- assert (git_repo / "README.md" ).read_text (encoding = "utf-8" ) == "seed\n "
903+ assert gate .run_git (["config" , "--get" , "core.hooksPath" ], template_repo ).strip () == ".githooks"
904+ assert (template_repo / "preferences" ).is_dir ()
905+ assert (template_repo / "tests" / "preferences" ).is_dir ()
906+
907+ leftovers = [
908+ name
909+ for name in (
910+ "README.template.md" ,
911+ ".assets" ,
912+ ".github/workflows/publish.yml" ,
913+ "CONTRIBUTING.md" ,
914+ "mutation-score.json" ,
915+ "harness/tests" ,
916+ )
917+ if (template_repo / name ).exists ()
918+ ]
919+ assert (template_repo / "pyproject.toml" ).read_text (encoding = "utf-8" ) == expected_project
920+ assert not leftovers , f"template leftovers after install: { leftovers } "
968921
969922
970923def test_cleanup_updates_the_pristine_historical_template_commit (
971924 monkeypatch : pytest .MonkeyPatch , git_repo : Path
972925) -> None :
973- """The historical pristine-template probes trigger the real Git commit update after real cleanup."""
926+ """Historical identity amends a real pristine repository without requiring old Git history."""
927+ replacement_project = (REPO_ROOT / "harness" / "temp.pyproject.toml" ).read_text (encoding = "utf-8" )
928+ replacement_readme = "replacement project readme\n "
974929 (git_repo / "pyproject.toml" ).write_text ('[project]\n name = "template"\n ' , encoding = "utf-8" )
975- replacement_project = "[project]\n name = 'replacement'\n "
976- (git_repo / "temp.pyproject.toml" ).write_text (replacement_project , encoding = "utf-8" )
977- replacement_readme = "the project readme\n "
978930 (git_repo / "README.template.md" ).write_text (replacement_readme , encoding = "utf-8" )
979- real_run_git = cli .run_git
980- run_git = Mock (wraps = real_run_git , side_effect = ["" , "867f2df" , DEFAULT ])
931+ (git_repo / "harness" ).mkdir ()
932+ (git_repo / "harness" / "temp.pyproject.toml" ).write_text (replacement_project , encoding = "utf-8" )
933+ gate .run_git (["add" , "-A" ], git_repo )
934+ gate .run_git (["commit" , "-q" , "-m" , "template state" ], git_repo )
935+ assert not gate .run_git (["status" , "--porcelain" ], git_repo )
936+ parent = gate .run_git (["rev-parse" , "HEAD^" ], git_repo ).strip ()
937+ original_head = gate .run_git (["rev-parse" , "HEAD" ], git_repo ).strip ()
938+ original_message = gate .run_git (["log" , "-1" , "--format=%B" ], git_repo ).strip ()
939+ run_git = Mock (wraps = cli .run_git , side_effect = [DEFAULT , "867f2df\n " , DEFAULT ])
981940 monkeypatch .setattr (cli , "run_git" , run_git )
982941
983942 assert cli .cleanup (git_repo ) is True
984- assert run_git .call_args_list [:2 ] == [
985- call (["status" , "--porcelain" ], git_repo ),
986- call (["rev-parse" , "--short" , "HEAD" ], git_repo ),
987- ]
988- wrong_repo_amend = call (["commit" , "-a" , "--amend" , "--no-edit" ])
989- correct_repo_amend = call (["commit" , "-a" , "--amend" , "--no-edit" ], git_repo )
990- assert run_git .call_args_list [- 1 ] != wrong_repo_amend
991- assert run_git .call_args_list [- 1 ] == correct_repo_amend
943+
944+ amended_head = gate .run_git (["rev-parse" , "HEAD" ], git_repo ).strip ()
945+ assert amended_head != original_head
946+ assert gate .run_git (["rev-parse" , "HEAD^" ], git_repo ).strip () == parent
947+ assert gate .run_git (["log" , "-1" , "--format=%B" ], git_repo ).strip () == original_message
948+ assert not gate .run_git (["status" , "--porcelain" ], git_repo )
992949 assert (git_repo / "README.md" ).read_text (encoding = "utf-8" ) == replacement_readme
993950 assert (git_repo / "pyproject.toml" ).read_text (encoding = "utf-8" ) == replacement_project
994- assert not (git_repo / "temp.pyproject.toml" ).exists ()
951+ assert not (git_repo / "harness" / " temp.pyproject.toml" ).exists ()
995952
996953
997- @pytest .mark .parametrize ("present" , [(), ("README.template.md" ,), ("temp.pyproject.toml" ,)])
998- def test_cleanup_requires_both_template_files (present : tuple [str , ...], tmp_path : Path ) -> None :
954+ @pytest .mark .parametrize (
955+ ("has_readme" , "has_project" ),
956+ [(False , False ), (True , False ), (False , True )],
957+ )
958+ def test_cleanup_requires_both_template_files (has_readme : bool , has_project : bool , tmp_path : Path ) -> None :
999959 """Cleanup does nothing unless both template inputs identify a template checkout."""
1000- for name in present :
1001- (tmp_path / name ).touch ()
960+ if has_readme :
961+ (tmp_path / "README.template.md" ).touch ()
962+ if has_project :
963+ (tmp_path / "harness" ).mkdir ()
964+ (tmp_path / "harness" / "temp.pyproject.toml" ).touch ()
1002965
1003966 assert cli .cleanup (tmp_path ) is False
1004967
1005968
1006- def test_cleanup_does_not_amend_a_dirty_template (monkeypatch : pytest .MonkeyPatch , tmp_path : Path ) -> None :
1007- """Cleanup only rewrites the historical template commit when the checkout started clean."""
1008- (tmp_path / "README.template.md" ).write_text ("readme\n " , encoding = "utf-8" )
1009- (tmp_path / "temp.pyproject.toml" ).write_text ("[project]\n " , encoding = "utf-8" )
1010- run_git = Mock (side_effect = [" M README.md\n " , "867f2df" ])
969+ def test_cleanup_does_not_amend_a_dirty_template (monkeypatch : pytest .MonkeyPatch , git_repo : Path ) -> None :
970+ """Real dirty state prevents cleanup from amending the historical template commit."""
971+ replacement_project = (REPO_ROOT / "harness" / "temp.pyproject.toml" ).read_text (encoding = "utf-8" )
972+ (git_repo / "pyproject.toml" ).write_text ('[project]\n name = "template"\n ' , encoding = "utf-8" )
973+ (git_repo / "README.template.md" ).write_text ("replacement project readme\n " , encoding = "utf-8" )
974+ (git_repo / "harness" ).mkdir ()
975+ (git_repo / "harness" / "temp.pyproject.toml" ).write_text (replacement_project , encoding = "utf-8" )
976+ gate .run_git (["add" , "-A" ], git_repo )
977+ gate .run_git (["commit" , "-q" , "-m" , "template state" ], git_repo )
978+ assert not gate .run_git (["status" , "--porcelain" ], git_repo )
979+ original_head = gate .run_git (["rev-parse" , "HEAD" ], git_repo ).strip ()
980+ (git_repo / "README.md" ).write_text ("uncommitted user work\n " , encoding = "utf-8" )
981+ run_git = Mock (wraps = cli .run_git , side_effect = [DEFAULT , "867f2df\n " ])
1011982 monkeypatch .setattr (cli , "run_git" , run_git )
1012983
1013- assert cli .cleanup (tmp_path ) is True
1014- assert run_git .call_args_list == [
1015- call (["status" , "--porcelain" ], tmp_path ),
1016- call (["rev-parse" , "--short" , "HEAD" ], tmp_path ),
1017- ]
984+ assert cli .cleanup (git_repo ) is True
985+
986+ assert gate .run_git (["rev-parse" , "HEAD" ], git_repo ).strip () == original_head
987+ assert gate .run_git (["status" , "--porcelain" ], git_repo )
1018988
1019989
1020990@pytest .mark .parametrize (
0 commit comments