Skip to content

Commit ada6380

Browse files
committed
Ensure that harness install still deletes correct files
1 parent 974ee20 commit ada6380

5 files changed

Lines changed: 94 additions & 138 deletions

File tree

‎harness/cli.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,14 @@ def cleanup(cwd: Path) -> bool:
176176
Returns:
177177
bool True if successful
178178
"""
179-
if not ((cwd / "README.template.md").is_file() and (cwd / "temp.pyproject.toml").is_file()):
179+
if not ((cwd / "README.template.md").is_file() and (cwd / "harness" / "temp.pyproject.toml").is_file()):
180180
return False
181181
clean_tree = not run_git(["status", "--porcelain"], cwd).strip()
182182
(cwd / "README.template.md").replace(cwd / "README.md")
183-
(cwd / "temp.pyproject.toml").replace(cwd / "pyproject.toml")
184-
for file_name in (".github/workflows/publish.yml", "CONTRIBUTING.md"):
183+
(cwd / "harness" / "temp.pyproject.toml").replace(cwd / "pyproject.toml")
184+
for file_name in ("mutation-score.json", ".github/workflows/publish.yml", "CONTRIBUTING.md"):
185185
(cwd / file_name).unlink(missing_ok=True)
186-
for directory in (cwd / "dist", cwd / "harness" / "tests", cwd / ".assets"):
186+
for directory in (cwd / "harness" / "tests", cwd / ".assets", cwd / ".*cache"):
187187
if directory.exists():
188188
rmtree(directory)
189189
if run_git(["rev-parse", "--short", "HEAD"], cwd).startswith("867f2df") and clean_tree:

‎harness/temp.pyproject.toml‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ security = [
9393
".",
9494
]
9595
types = ["pyright", "--outputjson"]
96-
test = [
97-
"pytest", "-p", "no:cacheprovider", "-n", "auto", "--cov", "--cov-report=term-missing", "--cov-fail-under=100", "--hypothesis-show-statistics",
98-
]
96+
test = ["pytest", "-p", "no:cacheprovider", "-n", "auto", "--cov", "--cov-report=term-missing", "--cov-fail-under=100", "--durations=5"]
9997

10098
[tool.harness.FORBIDDEN]
10199
DIRS = ["harness/", ".githooks/", ".github/", ".git/", "preferences/", "tests/preferences/", "mutation", "tests/mutation/"]

‎harness/tests/test_cli.py‎

Lines changed: 88 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -870,151 +870,121 @@ def test_hoist_aborts_before_writing_when_declined(monkeypatch: pytest.MonkeyPat
870870

871871

872872
def 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]\nname = '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

970923
def 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]\nname = "template"\n', encoding="utf-8")
975-
replacement_project = "[project]\nname = '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]\nname = "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(

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ security = [
154154
".",
155155
]
156156
types = ["pyright", "--outputjson"]
157-
test = ["pytest", "-p", "no:cacheprovider", "-n", "auto", "--cov", "--cov-report=term-missing", "--cov-fail-under=100", "--hypothesis-show-statistics", "--durations=5"]
157+
test = ["pytest", "-p", "no:cacheprovider", "-n", "auto", "--cov", "--cov-report=term-missing", "--cov-fail-under=100", "--durations=5"]
158158

159159
[tool.harness.FORBIDDEN]
160160
DIRS = ["harness/", ".githooks/", ".github/", ".git/", "preferences/", "tests/preferences/", "mutation", "tests/mutation/"]

‎tox.toml‎

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)