Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions heart/checks/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,6 @@ def run(results_dir: Path | None = None, fetch_cloud: bool | None = None) -> dic
summary["cloud_url"] = cloud["url"]
summary["source"] = "cloud"

sys.path.insert(0, str(HEART_HOME))
from heart import state

state.atomic_write_json(HEART_STATE_DIR / "test_run.json", summary)
return summary


Expand All @@ -237,6 +233,12 @@ def main(argv: list[str]) -> int:
summary = run(results_dir)

sys.path.insert(0, str(HEART_HOME))
from heart import state

# Persist only here, at the tick/CLI entrypoint — run() is side-effect-free
# so library callers (and the test suite) can never clobber live state.
state.atomic_write_json(HEART_STATE_DIR / "test_run.json", summary)

from heart.heart_color import c_ok, c_warn, c_fail, c_info, c_meta, glyph_ok, glyph_warn, glyph_fail

if not summary:
Expand Down
10 changes: 6 additions & 4 deletions heart/checks/version_skew.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,18 @@ def run(root: Path = PYAUTO_ROOT) -> dict[str, Any]:
}
)
result = {"workspaces": workspaces}
sys.path.insert(0, str(HEART_HOME))
from heart import state

state.atomic_write_json(HEART_STATE_DIR / "version_skew.json", result)
return result


def main(argv: list[str]) -> int:
result = run()
sys.path.insert(0, str(HEART_HOME))
from heart import state

# Persist only here, at the tick/CLI entrypoint — run() is side-effect-free
# so library callers (and the test suite) can never clobber live state.
state.atomic_write_json(HEART_STATE_DIR / "version_skew.json", result)

from heart.heart_color import c_ok, c_warn, c_fail, c_info, c_meta, glyph_ok, glyph_warn, glyph_fail

workspaces = result["workspaces"]
Expand Down
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""tests/conftest.py — sandbox HEART_STATE_DIR for the whole suite.

heart/checks/* resolve HEART_STATE_DIR at import time, so this assignment must
happen before any test module imports them; conftest import is pytest's
earliest hook, which is why this is a module-level statement and not a fixture.
Belt-and-braces on top of the checks' run()/main() split (state writes live
only in main()): no test, present or future, can reach the developer's live
~/.pyauto-heart — that state is the input to the release gate.
"""

from __future__ import annotations

import os
import tempfile

os.environ["HEART_STATE_DIR"] = tempfile.mkdtemp(prefix="pyauto-heart-test-state-")
27 changes: 27 additions & 0 deletions tests/test_test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,30 @@ def test_server_verdict_prefers_agent_file_over_gh(monkeypatch, tmp_path):
def test_agent_supplied_verdict_absent_is_none(monkeypatch, tmp_path):
monkeypatch.setattr(tr, "VALIDATION_FILE", tmp_path / "nope.json")
assert tr._agent_supplied_verdict() is None


# --- state-dir isolation (the 2026-07-15 clobber incident) ---------------------

def test_run_writes_nothing_to_state_dir(tmp_path):
"""run() must be side-effect-free: the write lives in main() only, so tests
(and any library caller) can never clobber live Heart state."""
import os
from pathlib import Path
state_dir = Path(os.environ["HEART_STATE_DIR"])
before = set(state_dir.glob("**/*")) if state_dir.exists() else set()
(tmp_path / "report.json").write_text(json.dumps({
"ready": True, "run_label": "R1", "summary": {"passed": 1}}))
tr.run(results_dir=tmp_path)
after = set(state_dir.glob("**/*")) if state_dir.exists() else set()
assert after == before


def test_main_persists_summary_to_state_dir(tmp_path):
"""The tick path (python -m heart.checks.test_run) must still persist."""
import os
from pathlib import Path
(tmp_path / "report.json").write_text(json.dumps({
"ready": True, "run_label": "R1", "summary": {"passed": 1}}))
assert tr.main(["test_run", str(tmp_path)]) == 0
written = json.loads((Path(os.environ["HEART_STATE_DIR"]) / "test_run.json").read_text())
assert written["ready"] is True and written["passed"] == 1
25 changes: 25 additions & 0 deletions tests/test_version_skew.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,28 @@ def test_autolens_assistant_is_a_pinned_workspace():
mapping = vs.workspace_library()
assert "autolens_assistant" in mapping
assert mapping["autolens_assistant"] == ("PyAutoLens", "autolens")


# --- state-dir isolation (the 2026-07-15 clobber incident's sibling) -----------

def test_run_writes_nothing_to_state_dir(tmp_path):
"""run() must be side-effect-free: the write lives in main() only, so tests
(and any library caller) can never clobber live Heart state."""
import os
from pathlib import Path
state_dir = Path(os.environ["HEART_STATE_DIR"])
before = set(state_dir.glob("**/*")) if state_dir.exists() else set()
vs.run(root=tmp_path)
after = set(state_dir.glob("**/*")) if state_dir.exists() else set()
assert after == before


def test_main_persists_result_to_state_dir(monkeypatch):
"""The tick path (python -m heart.checks.version_skew) must still persist."""
import json
import os
from pathlib import Path
monkeypatch.setattr(vs, "run", lambda root=vs.PYAUTO_ROOT: {"workspaces": []})
assert vs.main(["version_skew"]) == 0
written = json.loads((Path(os.environ["HEART_STATE_DIR"]) / "version_skew.json").read_text())
assert written == {"workspaces": []}
Loading