Files
2026-07-13 12:40:00 +08:00

164 lines
6.7 KiB
Python

"""Regression tests for the PreCompact reminder (v2.38.0, v3 dispatcher).
PreCompact fires on Claude Code's autoCompact and manual /compact. It re-injects
a planning reminder before context compaction. Contract:
- Declared in the canonical SKILL.md frontmatter with a wildcard matcher so
both manual and auto triggers fire.
- The scalar is a thin v3 dispatcher to scripts/inject-plan.sh (build decision
"hooks become thin dispatchers"); it carries the --context=precompact flag.
- inject-plan.sh --context=precompact prints a reminder when task_plan.md
exists, stays silent when absent, and surfaces Plan-SHA256 when an
attestation is set.
History: this file used to extract the inline PreCompact bash scalar and run it
standalone. v3 reduced the scalar to a dispatcher that exits silently without
CLAUDE_SKILL_DIR, so the behavioral assertions now run inject-plan.sh directly
with CLAUDE_SKILL_DIR set. The contract is unchanged.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CANONICAL_SKILL = REPO_ROOT / "skills" / "planning-with-files" / "SKILL.md"
SKILL_DIR = REPO_ROOT / "skills" / "planning-with-files"
INJECT_PLAN = SKILL_DIR / "scripts" / "inject-plan.sh"
def extract_precompact_scalar(text: str) -> str:
"""Pull the PreCompact command scalar out of SKILL.md frontmatter."""
in_section = False
for line in text.splitlines():
stripped = line.strip()
if stripped == "PreCompact:":
in_section = True
continue
if in_section and stripped.startswith("command:"):
m = re.match(r'command:\s*"(.+)"\s*$', stripped)
if m:
return m.group(1).replace('\\"', '"')
if in_section and stripped.endswith(":") and not stripped.startswith("-") and stripped != "hooks:":
break
return ""
class PreCompactHookDeclarationTests(unittest.TestCase):
def setUp(self) -> None:
self.text = CANONICAL_SKILL.read_text(encoding="utf-8")
def test_precompact_is_declared(self) -> None:
self.assertIn("PreCompact:", self.text, "PreCompact hook missing from canonical SKILL.md")
def test_precompact_uses_wildcard_matcher(self) -> None:
# We want both autoCompact and manual /compact to fire the reminder.
self.assertRegex(
self.text,
r"PreCompact:\s*\n\s*-\s*matcher:\s*\"\*\"",
"PreCompact should match all triggers ('*'), got something stricter",
)
def test_precompact_scalar_is_thin_dispatcher(self) -> None:
# The scalar must dispatch to inject-plan.sh with the precompact context,
# carry both install fallbacks, and contain no literal '---' (YAML
# collision class, Discussion #153).
scalar = extract_precompact_scalar(self.text)
self.assertTrue(scalar, "Could not extract PreCompact scalar from SKILL.md")
self.assertIn("${CLAUDE_SKILL_DIR}/scripts/inject-plan.sh", scalar)
self.assertIn("--context=precompact", scalar)
self.assertIn(
"$HOME/.claude/skills/planning-with-files/scripts/inject-plan.sh", scalar
)
self.assertNotIn("---", scalar)
@unittest.skipUnless(shutil.which("sh"), "sh not available on this platform")
class PreCompactDispatcherSilentTests(unittest.TestCase):
"""The dispatcher scalar itself must never break compaction."""
def test_dispatcher_silent_exit_when_script_absent(self) -> None:
scalar = extract_precompact_scalar(CANONICAL_SKILL.read_text(encoding="utf-8"))
with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as fake_home:
script = Path(tmp) / "_precompact_dispatch.sh"
script.write_text(scalar, encoding="utf-8")
env = os.environ.copy()
env.pop("CLAUDE_SKILL_DIR", None)
env["HOME"] = fake_home
result = subprocess.run(
["sh", str(script)],
cwd=tmp,
text=True,
encoding="utf-8",
capture_output=True,
env=env,
check=False,
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stdout.strip())
@unittest.skipUnless(shutil.which("sh"), "sh not available on this platform")
class PreCompactBehaviorTests(unittest.TestCase):
"""Run inject-plan.sh --context=precompact directly (dispatcher target)."""
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp(prefix="pwf-precompact-"))
self.home_dir = self.tmp / "_home"
self.home_dir.mkdir()
self.env = os.environ.copy()
self.env["CLAUDE_SKILL_DIR"] = str(SKILL_DIR)
self.env["HOME"] = str(self.home_dir)
self.env["XDG_CACHE_HOME"] = str(self.tmp / "_cache")
self.env.pop("PLAN_ID", None)
def tearDown(self) -> None:
shutil.rmtree(self.tmp, ignore_errors=True)
def _run(self) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["sh", str(INJECT_PLAN), "--context=precompact"],
cwd=str(self.tmp),
text=True,
encoding="utf-8",
capture_output=True,
env=self.env,
check=False,
)
def test_silent_when_no_plan(self) -> None:
result = self._run()
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stdout.strip(), "PreCompact should be silent without task_plan.md")
def test_emits_reminder_when_plan_exists(self) -> None:
(self.tmp / "task_plan.md").write_text("# Plan\n", encoding="utf-8")
result = self._run()
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("[planning-with-files] PreCompact", result.stdout)
self.assertIn("progress.md", result.stdout, "reminder must mention progress.md")
def test_emits_plan_sha256_when_legacy_attestation_set(self) -> None:
(self.tmp / "task_plan.md").write_text("# Plan\n", encoding="utf-8")
(self.tmp / ".plan-attestation").write_text(
"abc123def456" + "0" * 52 + "\n", encoding="utf-8"
)
result = self._run()
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("Plan-SHA256", result.stdout)
def test_no_sha256_line_when_no_attestation(self) -> None:
(self.tmp / "task_plan.md").write_text("# Plan\n", encoding="utf-8")
result = self._run()
self.assertEqual(0, result.returncode, result.stderr)
self.assertNotIn("Plan-SHA256", result.stdout)
if __name__ == "__main__":
unittest.main()