cc-os/plugins/os-adr/tests/hook_test.py

161 lines
5.8 KiB
Python
Raw Normal View History

"""Tests for hooks/session_start.py. Run: python3 tests/hook_test.py"""
import io
import json
import subprocess
import sys
import unittest
from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
PLUGIN_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PLUGIN_ROOT / "hooks"))
from session_start import ( # noqa: E402
ABSENT_NOTE,
PRESENT_NOTE,
StateDir,
adr_system_present,
decide,
find_project_root,
run,
)
TODAY = date(2026, 7, 3)
YESTERDAY = date(2026, 7, 2)
class DecideTest(unittest.TestCase):
def test_present_injects_context_naming_both_surfaces(self):
outcome, note = decide(True, False, None, TODAY)
self.assertEqual("context", outcome)
self.assertIn("/os-adr:create", note)
self.assertIn("/os-adr:find", note)
def test_present_wins_even_if_suppressed(self):
self.assertEqual("context", decide(True, True, None, TODAY)[0])
def test_absent_unsuppressed_banners_with_init_and_migrate(self):
outcome, note = decide(False, False, None, TODAY)
self.assertEqual("banner", outcome)
self.assertIn("/os-adr:init", note)
self.assertIn("/os-adr:migrate", note)
def test_absent_suppressed_is_silent(self):
self.assertEqual(("silent", None), decide(False, True, None, TODAY))
def test_absent_reminded_today_is_silent(self):
self.assertEqual(("silent", None), decide(False, False, TODAY, TODAY))
def test_absent_reminded_yesterday_banners_again(self):
self.assertEqual("banner", decide(False, False, YESTERDAY, TODAY)[0])
class RunTest(unittest.TestCase):
def project(self, tmp, with_adr=False):
root = Path(tmp)
(root / ".git").mkdir()
if with_adr:
(root / "docs" / "adr").mkdir(parents=True)
(root / "docs" / "adr" / "README.md").write_text("index")
return root
def test_present_emits_additional_context(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp, with_adr=True)
out = io.StringIO()
self.assertEqual("context", run(root, TODAY, out))
payload = json.loads(out.getvalue())
self.assertEqual(
PRESENT_NOTE,
payload["hookSpecificOutput"]["additionalContext"],
)
self.assertFalse((root / ".os-adr").exists(), "present case must not create state")
def test_absent_emits_banner_and_stamps_snooze(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp)
out = io.StringIO()
self.assertEqual("banner", run(root, TODAY, out))
self.assertEqual(ABSENT_NOTE, json.loads(out.getvalue())["systemMessage"])
self.assertEqual(
TODAY.isoformat(), (root / ".os-adr" / "last_reminded").read_text()
)
def test_second_run_same_day_is_silent(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp)
run(root, TODAY, io.StringIO())
out = io.StringIO()
self.assertEqual("silent", run(root, TODAY, out))
self.assertEqual("", out.getvalue())
def test_next_day_banners_again(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp)
run(root, YESTERDAY, io.StringIO())
self.assertEqual("banner", run(root, TODAY, io.StringIO()))
def test_suppressed_project_stays_silent(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp)
(root / ".os-adr").mkdir()
(root / ".os-adr" / "suppress").write_text("")
out = io.StringIO()
self.assertEqual("silent", run(root, TODAY, out))
self.assertEqual("", out.getvalue())
def test_corrupt_snooze_stamp_is_treated_as_unset(self):
with TemporaryDirectory() as tmp:
root = self.project(tmp)
(root / ".os-adr").mkdir()
(root / ".os-adr" / "last_reminded").write_text("not-a-date")
self.assertEqual("banner", run(root, TODAY, io.StringIO()))
class ProjectRootTest(unittest.TestCase):
def test_walks_up_to_git_root(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
(root / ".git").mkdir()
nested = root / "a" / "b"
nested.mkdir(parents=True)
self.assertEqual(root, find_project_root(nested))
def test_none_outside_any_git_project(self):
with TemporaryDirectory() as tmp:
self.assertIsNone(find_project_root(Path(tmp)))
class PythonRubyAgreementTest(unittest.TestCase):
"""Shared fixture tree: the Python existence check and Ruby
Adr::Repository#exists? must give the same answer (design D3 risk)."""
def ruby_exists(self, root):
script = (
f'require "{PLUGIN_ROOT}/lib/adr"; '
f'puts Adr::Repository.new(root: {str(root)!r}).exists?'
)
out = subprocess.run(
["ruby", "-e", script], capture_output=True, text=True, check=True
)
return out.stdout.strip() == "true"
def assert_agreement(self, root, expected):
self.assertEqual(expected, adr_system_present(root), "python check")
self.assertEqual(expected, self.ruby_exists(root), "ruby check")
def test_agreement_across_fixture_states(self):
with TemporaryDirectory() as tmp:
root = Path(tmp)
self.assert_agreement(root, False) # nothing
(root / "docs" / "adr").mkdir(parents=True)
self.assert_agreement(root, False) # dir, no index
(root / "docs" / "adr" / "README.md").write_text("index")
self.assert_agreement(root, True) # dir + index
if __name__ == "__main__":
unittest.main()