diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d1d2e30 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "hooks": { + "SessionStart": [ + {"hooks": [{"type": "command", "command": "uphold hook claude-code", "async": true}]} + ], + "Stop": [ + {"hooks": [{"type": "command", "command": "uphold hook claude-code", "timeout": 600}]} + ] + } +} diff --git a/policy/principles.toml b/policy/principles.toml index 86180f9..493dc05 100644 --- a/policy/principles.toml +++ b/policy/principles.toml @@ -638,3 +638,16 @@ files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go", "policy/**", "READ [override.no-pinned-versioned-fetch] files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go", "policy/**", "README.md", "third_party/**", "vendor/**", ".github/workflows/release.yml"] + +# What an agent's turn may not end red. `uphold hook claude-code`, configured +# for SessionStart and Stop in .claude/settings.json, runs these at session +# start and again when the agent stops, and blocks once on a command that was +# green when the session started. The reconcile and the Python suite, and not +# `cargo test`: these run at the end of every turn, and a gate that adds a +# minute or more to each one is a gate people turn off. The Rust suite stays at +# pre-push and in CI. +[stop] +run = [ + ["uphold", "check"], + ["uv", "run", "--no-project", "python", "-m", "unittest", "discover", "-s", "tests"], +] diff --git a/tests/test_toolchain.py b/tests/test_toolchain.py index 2afd17f..00f11fd 100644 --- a/tests/test_toolchain.py +++ b/tests/test_toolchain.py @@ -16,6 +16,7 @@ import shutil import subprocess +import tempfile import tomllib import unittest from pathlib import Path @@ -60,10 +61,10 @@ class Preflight(unittest.TestCase): reason "not installed" and would pass while proving nothing. """ - def _check(self) -> subprocess.CompletedProcess[str]: + def _check(self, root: Path) -> subprocess.CompletedProcess[str]: return subprocess.run( ["./scripts/deps.sh", "check"], - cwd=REPO, + cwd=root, capture_output=True, text=True, check=False, @@ -75,12 +76,16 @@ def test_a_version_the_host_cannot_satisfy_is_refused(self) -> None: impossible = manifest.replace(f'want = "{want}"', 'want = ">=999.0"', 1) self.assertNotEqual(manifest, impossible, "the rustc `want` line moved") - original = (REPO / "toolchain.toml").read_bytes() - try: - (REPO / "toolchain.toml").write_text(impossible, encoding="utf-8") - done = self._check() - finally: - (REPO / "toolchain.toml").write_bytes(original) + # In a copy, never the tree: this suite runs as the [stop] gate while + # an agent works, and writing the real toolchain.toml and restoring it + # afterwards would revert any edit made to it in between. deps.sh reads + # only the toolchain.toml beside its own scripts/ directory. + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "scripts").mkdir() + shutil.copy2(REPO / "scripts" / "deps.sh", root / "scripts" / "deps.sh") + (root / "toolchain.toml").write_text(impossible, encoding="utf-8") + done = self._check(root) report = done.stdout + done.stderr self.assertNotEqual(done.returncode, 0, report)