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
2 changes: 1 addition & 1 deletion .github/capsule-pipeline/capsule.dot
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ digraph CapsulePipeline {
// as rival_reset; the RC-10 purge retires with the reset idiom it
// extends).
author_reset [shape=parallelogram, class="glue", max_retries=0,
tool_command="BASE=$(cat .ai/base-sha 2>/dev/null); n=$(grep -c '\"gate\": \"round\"' .ai/convergence.jsonl 2>/dev/null); n=$((${n:-0}+1)); DIRT=$(git status --porcelain 2>/dev/null | grep -vE '^.{3}\\.ai(/|$)' | head -20); if [ -n \"$DIRT\" ] || { [ -n \"$BASE\" ] && [ \"$(git rev-parse HEAD)\" != \"$BASE\" ]; }; then dirty=true; mkdir -p .ai/findings; [ -f .ai/findings/author-tree-dirt.md ] || echo '# Finding: the author left the pinned tree dirty (mechanically recovered)' > .ai/findings/author-tree-dirt.md; { echo; echo \"AUTHOR TREE-DIRT (iteration $n): the author lane ended with target_dir not pristine at the pinned base SHA (${BASE:-MISSING .ai/base-sha}); HEAD=$(git rev-parse HEAD). Self-testing fix-shaped edits against the pinned tree is legitimate; leaving them applied is a recorded fact, never silently absorbed. The tree was hard-reset and the reset PROVEN before any measurement. Dirty entries (git status --porcelain, .ai/ excluded, first 20):\"; echo \"$DIRT\"; } >> .ai/findings/author-tree-dirt.md; else dirty=false; fi; git checkout --quiet -- .; git clean -qfd -e .ai; PYCN=$(find . \\( -name .git -o -name .venv -o -name venv \\) -prune -o \\( -name __pycache__ -type d -o -name '*.py[co]' -type f \\) -prune -print 2>/dev/null | grep -c .); find . \\( -name .git -o -name .venv -o -name venv \\) -prune -o \\( -name __pycache__ -type d -o -name '*.py[co]' -type f \\) -prune -print 2>/dev/null | while IFS= read -r p; do rm -rf \"$p\"; done; printf '{\"iteration\": %s, \"gate\": \"author_reset\", \"dirty\": %s, \"pyc_purged\": %s}\\n' \"$n\" \"$dirty\" \"$PYCN\" >> .ai/convergence.jsonl; if [ -n \"$BASE\" ] && [ -z \"$(git status --porcelain 2>/dev/null | grep -vE '^.{3}\\.ai(/|$)')\" ] && [ \"$(git rev-parse HEAD)\" = \"$BASE\" ]; then printf reset_proven; else { echo \"AUTHOR-RESET: RESET NOT PROVEN after the author maker -- porcelain and/or HEAD does not match the pinned base SHA (${BASE:-MISSING .ai/base-sha}) even after a hard reset (git checkout --quiet -- . && git clean -qfd -e .ai). Halting loud: an unreset tree poisons every gate verdict downstream (redgate would measure base+author-edits, not base -- the RC-7 class). Dirty entries (git status --porcelain, first 20):\"; git status --porcelain 2>/dev/null | head -20; } > .ai/gate.log; printf reset_unproven; fi"]
tool_command="BASE=$(cat .ai/base-sha 2>/dev/null); n=$(grep -c '\"gate\": \"round\"' .ai/convergence.jsonl 2>/dev/null); n=$((${n:-0}+1)); DIRT=$(git status --porcelain 2>/dev/null | grep -vE '^.{3}\\.ai(/|$)' | head -20); if [ -n \"$DIRT\" ] || { [ -n \"$BASE\" ] && [ \"$(git rev-parse HEAD)\" != \"$BASE\" ]; }; then dirty=true; mkdir -p .ai/findings; [ -f .ai/findings/author-tree-dirt.md ] || echo '# Finding: the author left the pinned tree dirty (mechanically recovered)' > .ai/findings/author-tree-dirt.md; { echo; echo \"AUTHOR TREE-DIRT (iteration $n): the author lane ended with target_dir not pristine at the pinned base SHA (${BASE:-MISSING .ai/base-sha}); HEAD=$(git rev-parse HEAD). Self-testing fix-shaped edits against the pinned tree is legitimate; leaving them applied is a recorded fact, never silently absorbed. The tree was hard-reset and the reset PROVEN before any measurement. Dirty entries (git status --porcelain, .ai/ excluded, first 20):\"; echo \"$DIRT\"; } >> .ai/findings/author-tree-dirt.md; else dirty=false; fi; git checkout --quiet -- .; git clean -qfd -e .ai; rm -f .ai/hypothesis.patch; PYCN=$(find . \\( -name .git -o -name .venv -o -name venv \\) -prune -o \\( -name __pycache__ -type d -o -name '*.py[co]' -type f \\) -prune -print 2>/dev/null | grep -c .); find . \\( -name .git -o -name .venv -o -name venv \\) -prune -o \\( -name __pycache__ -type d -o -name '*.py[co]' -type f \\) -prune -print 2>/dev/null | while IFS= read -r p; do rm -rf \"$p\"; done; printf '{\"iteration\": %s, \"gate\": \"author_reset\", \"dirty\": %s, \"pyc_purged\": %s}\\n' \"$n\" \"$dirty\" \"$PYCN\" >> .ai/convergence.jsonl; if [ -n \"$BASE\" ] && [ -z \"$(git status --porcelain 2>/dev/null | grep -vE '^.{3}\\.ai(/|$)')\" ] && [ \"$(git rev-parse HEAD)\" = \"$BASE\" ] && [ ! -e .ai/hypothesis.patch ] && [ ! -L .ai/hypothesis.patch ]; then printf reset_proven; else { echo \"AUTHOR-RESET: RESET NOT PROVEN after the author maker -- porcelain and/or HEAD does not match the pinned base SHA (${BASE:-MISSING .ai/base-sha}) even after a hard reset (git checkout --quiet -- . && git clean -qfd -e .ai). Halting loud: an unreset tree poisons every gate verdict downstream (redgate would measure base+author-edits, not base -- the RC-7 class). Dirty entries (git status --porcelain, first 20):\"; git status --porcelain 2>/dev/null | head -20; } > .ai/gate.log; printf reset_unproven; fi"]

// GATE: enforces "a publishable capsule pair exists in checkable shape" +
// walls the budget (ledger-derived round count; every entry spends).
Expand Down
160 changes: 160 additions & 0 deletions tests/test_capsule_author_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""Regression coverage for the capsule author-reset scratch-state contract.

The reset command is extracted from the shipped graph and executed in a disposable
Git repository. It is intentionally never copied into this test: the graph is
the executable source of truth.
"""

from __future__ import annotations

import os
from pathlib import Path
import re
import subprocess

import pytest


REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DOT = REPO_ROOT / ".github" / "capsule-pipeline" / "capsule.dot"


def _dot_path() -> Path:
"""Allow the red-control invocation to supply pre-change graph bytes."""
return Path(os.environ.get("CAPSULE_AUTHOR_RESET_DOT", DEFAULT_DOT))


def _author_reset_command() -> str:
dot = _dot_path().read_text(encoding="utf-8")
match = re.search(
r'author_reset\s*\[[^\]]*?tool_command="((?:[^"\\]|\\.)*)"', dot, re.DOTALL
)
assert match, f"could not extract author_reset tool_command from {_dot_path()}"
return re.sub(r"\\(.)", lambda escaped: escaped.group(1), match.group(1))


def _fixture_env(cwd: Path) -> dict[str, str]:
"""Confine git and the extracted shell command to the disposable fixture."""
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
home = cwd / ".ai" / ".test-home"
home.mkdir(parents=True, exist_ok=True)
xdg_config_home = home / ".config"
xdg_config_home.mkdir(exist_ok=True)
env["HOME"] = str(home)
env["XDG_CONFIG_HOME"] = str(xdg_config_home)
env["GIT_CONFIG_NOSYSTEM"] = "1"
return env


def _run(command: str, cwd: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["sh", "-c", command],
cwd=cwd,
check=False,
text=True,
capture_output=True,
env=_fixture_env(cwd),
)


def _git(cwd: Path, *args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=cwd,
check=False,
text=True,
capture_output=True,
env=_fixture_env(cwd),
)
assert result.returncode == 0, result.stderr
return result.stdout.strip()


def _fixture(tmp_path: Path) -> tuple[Path, str, bytes]:
repo = tmp_path / "target"
repo.mkdir()
(repo / "tracked.txt").write_text("base\n", encoding="utf-8")
_git(repo, "init", "-q")
_git(repo, "config", "user.email", "test@example.invalid")
_git(repo, "config", "user.name", "Capsule reset test")
_git(repo, "add", "tracked.txt")
_git(repo, "commit", "-qm", "base")
base = _git(repo, "rev-parse", "HEAD")

feedback = b"feedback\x00bytes\r\n\xffmust survive exactly\n"
(repo / ".ai" / "feedback").mkdir(parents=True)
(repo / ".ai" / "base-sha").write_text(f"{base}\n", encoding="utf-8")
(repo / ".ai" / "convergence.jsonl").write_text(
'{"iteration": 1, "gate": "round", "shape": "ok"}\n', encoding="utf-8"
)
(repo / ".ai" / "feedback" / "critique-001.md").write_bytes(feedback)

# Simulate author self-test residue in the tracked tree; reset must recover it.
(repo / "tracked.txt").write_text("author self-test edit\n", encoding="utf-8")
(repo / "untracked.tmp").write_text("remove me\n", encoding="utf-8")
return repo, base, feedback


def _assert_pinned_clean_tree(repo: Path, base: str) -> None:
status = _git(repo, "status", "--porcelain")
non_ai_entries = [
line
for line in status.splitlines()
if not re.match(r"^.{3}\.ai(?:/|$)", line)
]
assert not non_ai_entries, status
assert _git(repo, "diff", "--quiet", "--") == ""
assert _git(repo, "rev-parse", "HEAD") == base


def test_author_reset_removes_stale_hypothesis_patch_and_preserves_feedback(
tmp_path: Path,
) -> None:
repo, base, feedback = _fixture(tmp_path)
patch = repo / ".ai" / "hypothesis.patch"
patch.write_text("diff --git a/tracked.txt b/tracked.txt\nstale patch\n", encoding="utf-8")

result = _run(_author_reset_command(), repo)

assert result.returncode == 0, result.stderr
assert result.stdout.endswith("reset_proven")
assert not patch.exists()
assert not patch.is_symlink()
_assert_pinned_clean_tree(repo, base)
assert (repo / ".ai" / "feedback" / "critique-001.md").read_bytes() == feedback


def test_author_reset_removes_dangling_hypothesis_patch_symlink(tmp_path: Path) -> None:
repo, base, feedback = _fixture(tmp_path)
patch = repo / ".ai" / "hypothesis.patch"
patch.symlink_to("missing-hypothesis.patch")
assert patch.is_symlink() and not patch.exists()

result = _run(_author_reset_command(), repo)

assert result.returncode == 0, result.stderr
assert result.stdout.endswith("reset_proven")
assert not patch.exists()
assert not patch.is_symlink()
_assert_pinned_clean_tree(repo, base)
assert (repo / ".ai" / "feedback" / "critique-001.md").read_bytes() == feedback


def test_author_reset_reports_unproven_when_patch_cleanup_cannot_remove_directory(
tmp_path: Path,
) -> None:
repo, base, feedback = _fixture(tmp_path)
patch = repo / ".ai" / "hypothesis.patch"
patch.mkdir()
(patch / "cannot-be-unlinked-by-rm-f").write_text("stale\n", encoding="utf-8")

result = _run(_author_reset_command(), repo)

assert result.returncode == 0, result.stderr
assert result.stdout.endswith("reset_unproven")
assert patch.is_dir()
assert "AUTHOR-RESET: RESET NOT PROVEN" in (repo / ".ai" / "gate.log").read_text(
encoding="utf-8"
)
_assert_pinned_clean_tree(repo, base)
assert (repo / ".ai" / "feedback" / "critique-001.md").read_bytes() == feedback
Loading