import datetime import json import os import tempfile import unittest import unittest.mock as mock import scratch_hooks import scratch def make_pool(root, names=("01", "02")): for name in names: os.makedirs(os.path.join(root, name)) class AccountNameTests(unittest.TestCase): def test_SecondConfigDirectoryIsAccount2(self): self.assertEqual("account2", scratch_hooks.account_name({"CLAUDE_CONFIG_DIR": "C:\\Users\\x\\.claude-account2"})) def test_AnythingElseIsAccount1(self): self.assertEqual("account1", scratch_hooks.account_name({"CLAUDE_CONFIG_DIR": "C:\\Users\\x\\.claude"})) self.assertEqual("account1", scratch_hooks.account_name({})) class CreateWorktreeTests(unittest.TestCase): def test_LocksAWorkspaceRecordsTheSessionAndRefreshesIt(self): with tempfile.TemporaryDirectory() as root, mock.patch.object(scratch, "refresh") as refresh: make_pool(root) path = scratch_hooks.create_worktree({"session_id": "abc", "name": "bold-oak"}, root, {}) self.assertEqual(os.path.join(root, "01"), path) refresh.assert_called_once_with(path) holder = scratch.read_holder(root, "01") self.assertEqual(("abc", "account1", "bold-oak"), (holder["session_id"], holder["account"], holder["task"])) def test_ReturnsTheWorkspaceToThePoolWhenTheUpdateFails(self): with tempfile.TemporaryDirectory() as root, \ mock.patch.object(scratch, "refresh", side_effect=RuntimeError("svn update failed")): make_pool(root, ("01",)) with self.assertRaises(RuntimeError): scratch_hooks.create_worktree({"session_id": "abc"}, root, {}) self.assertIsNone(scratch.read_holder(root, "01")) def test_FailsWhenEveryWorkspaceIsHeld(self): with tempfile.TemporaryDirectory() as root, mock.patch.object(scratch, "refresh"): make_pool(root, ("01",)) scratch_hooks.create_worktree({"session_id": "first"}, root, {}) with self.assertRaises(scratch.NoFreeWorkspace): scratch_hooks.create_worktree({"session_id": "second"}, root, {}) class RemoveWorktreeTests(unittest.TestCase): def test_UnlocksACleanWorkspaceAndKeepsItsDirectory(self): with tempfile.TemporaryDirectory() as root, mock.patch.object(scratch, "changed_paths", return_value=[]): make_pool(root, ("01",)) path = scratch.take(root, "abc", "account1", "") scratch_hooks.remove_worktree({"worktree_path": path}, root) self.assertIsNone(scratch.read_holder(root, "01")) self.assertTrue(os.path.isdir(path)) def test_RefusesAWorkspaceWithChanges(self): with tempfile.TemporaryDirectory() as root, \ mock.patch.object(scratch, "changed_paths", return_value=["M Foo.cs"]): make_pool(root, ("01",)) path = scratch.take(root, "abc", "account1", "") with self.assertRaises(scratch.WorkspaceHasChanges): scratch_hooks.remove_worktree({"worktree_path": path}, root) self.assertIsNotNone(scratch.read_holder(root, "01")) class StaleLockReportTests(unittest.TestCase): def old_lock(self, root): make_pool(root, ("01",)) scratch.take(root, "ancient", "account2", "a task", now=datetime.datetime.now() - datetime.timedelta(days=30)) def test_NamesOldLocksInTheMainWorkingCopy(self): with tempfile.TemporaryDirectory() as root: self.old_lock(root) report = scratch_hooks.stale_lock_report({"cwd": scratch_hooks.MAIN_ROOT}, root) self.assertIn("ancient", report) def test_StaysQuietInsideAWorkspace(self): with tempfile.TemporaryDirectory() as root: self.old_lock(root) self.assertIsNone(scratch_hooks.stale_lock_report({"cwd": os.path.join(root, "01")}, root)) def test_StaysQuietWhenNothingIsStale(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) scratch.take(root, "recent", "account1", "") self.assertIsNone(scratch_hooks.stale_lock_report({"cwd": scratch_hooks.MAIN_ROOT}, root)) class LogEventTests(unittest.TestCase): def test_AppendsOneLinePerEvent(self): with tempfile.TemporaryDirectory() as root: now = datetime.datetime(2026, 10, 15, 12, 0, 0) scratch_hooks.log_event(root, {"hook_event_name": "WorktreeRemove", "session_id": "abc"}, "ok", now=now) scratch_hooks.log_event(root, {"hook_event_name": "WorktreeCreate", "session_id": "def"}, "refused", now=now) with open(os.path.join(root, "locks", "hooks.log"), encoding="utf-8") as log: self.assertEqual(["2026-10-15T12:00:00 WorktreeRemove session=abc outcome=ok", "2026-10-15T12:00:00 WorktreeCreate session=def outcome=refused"], log.read().splitlines()) class SessionStartOutputTests(unittest.TestCase): def test_ShowsTheMessageToTheUserAndToClaude(self): output = json.loads(scratch_hooks.session_start_output("old locks")) self.assertEqual("old locks", output["systemMessage"]) self.assertEqual("old locks", output["hookSpecificOutput"]["additionalContext"]) self.assertEqual("SessionStart", output["hookSpecificOutput"]["hookEventName"]) if __name__ == "__main__": unittest.main()