"""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()