"""Regression: author worktree bootstrap from clean control checkout (#892). #892 is the four-door deadlock where every documented recovery path is closed: bootstrap refuses control, lock demands an existing worktree, worktree-start demands a lock, and shell worktree add is outside the sanctioned MCP path. Root cause: assess_author_issue_bootstrap returned allowed/proven for a clean control checkout, but bootstrap_permits_control_checkout only accepted create_issue assessments (task_scope=create_issue_only + empty reasons + full base-tip field set). Author assessments never satisfied the shared predicate, so the #274/#604 guards kept the ordinary control-checkout block. """ from __future__ import annotations import os import tempfile import unittest from unittest import mock import author_issue_bootstrap as aib import create_issue_bootstrap as cib CONTROL = "/repo/Gitea-Tools" MASTER = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" OTHER = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" def _assess( *, workspace=CONTROL, root=CONTROL, branch="master", head=MASTER, porcelain="", remote=MASTER, remote_error=None, task="bootstrap_author_issue_worktree", ): return aib.assess_author_issue_bootstrap( workspace_path=workspace, canonical_repo_root=root, current_branch=branch, head_sha=head, porcelain_status=porcelain, remote_master_sha=remote, remote_master_sha_error=remote_error, task=task, ) class TestAuthorBootstrapAssessmentShape(unittest.TestCase): def test_clean_control_emits_predicate_compatible_fields(self): assessment = _assess() self.assertTrue(assessment["allowed"]) self.assertTrue(assessment["proven"]) self.assertFalse(assessment["block"]) self.assertFalse(assessment["not_applicable"]) self.assertEqual(assessment["reasons"], []) self.assertEqual(assessment["task_scope"], "author_issue_bootstrap") self.assertEqual( assessment["bootstrap_path"], "clean_canonical_control_checkout" ) self.assertEqual(assessment["dirty_files"], []) self.assertIs(assessment["under_branches"], False) self.assertTrue(assessment["base_tips_verified"]) self.assertEqual(assessment["local_head_sha"], MASTER) self.assertEqual(assessment["remote_master_sha"], MASTER) self.assertEqual(assessment["workspace_path"], os.path.realpath(CONTROL)) self.assertEqual( assessment["canonical_repo_root"], os.path.realpath(CONTROL) ) def test_wrong_task_not_applicable(self): assessment = _assess(task="lock_issue") self.assertTrue(assessment["not_applicable"]) self.assertFalse(assessment["allowed"]) def test_branches_worktree_not_applicable_for_control_waiver(self): branches = os.path.join(CONTROL, "branches", "fix-issue-1") assessment = _assess(workspace=branches) self.assertTrue(assessment["not_applicable"]) self.assertFalse(assessment["allowed"]) self.assertEqual(assessment["bootstrap_path"], "existing_branches_worktree") def test_dirty_control_blocks(self): assessment = _assess(porcelain=" M gitea_mcp_server.py\n") self.assertTrue(assessment["block"]) self.assertFalse(assessment["allowed"]) self.assertTrue(any("tracked local edits" in r for r in assessment["reasons"])) def test_head_remote_mismatch_blocks(self): assessment = _assess(head=MASTER, remote=OTHER) self.assertTrue(assessment["block"]) self.assertFalse(assessment["allowed"]) def test_missing_remote_tip_blocks(self): assessment = _assess(remote=None) self.assertTrue(assessment["block"]) self.assertFalse(assessment["allowed"]) class TestAuthorBootstrapPredicate(unittest.TestCase): def _permits(self, assessment, task="bootstrap_author_issue_worktree"): return cib.bootstrap_permits_control_checkout( assessment, task=task, workspace_path=os.path.realpath(CONTROL), canonical_repo_root=os.path.realpath(CONTROL), ) def test_clean_author_bootstrap_permits(self): self.assertTrue(self._permits(_assess())) def test_tool_alias_permits(self): assessment = _assess(task="gitea_bootstrap_author_issue_worktree") self.assertTrue( self._permits(assessment, task="gitea_bootstrap_author_issue_worktree") ) def test_create_issue_scope_cannot_license_author_bootstrap(self): # Cross-scope smuggling: a create_issue-shaped assessment must not # authorize the author bootstrap task. create_shaped = dict(_assess()) create_shaped["task_scope"] = "create_issue_only" self.assertFalse(self._permits(create_shaped)) def test_author_scope_cannot_license_create_issue(self): assessment = _assess() self.assertFalse( cib.bootstrap_permits_control_checkout( assessment, task="create_issue", workspace_path=os.path.realpath(CONTROL), canonical_repo_root=os.path.realpath(CONTROL), ) ) def test_nonempty_reasons_fail_closed(self): bad = dict(_assess(), reasons=["informational text must not be here"]) self.assertFalse(self._permits(bad)) def test_dirty_fails_closed(self): self.assertFalse(self._permits(_assess(porcelain=" M x.py\n"))) def test_mismatch_fails_closed(self): self.assertFalse(self._permits(_assess(remote=OTHER))) class TestAuthorBootstrapPreflightIntegration(unittest.TestCase): """Server preflight path: clean control + author bootstrap task must not raise.""" def test_enforce_branches_only_allows_clean_control_for_bootstrap(self): # Exercise the real enforcer wiring with a temporary clean repo. import gitea_mcp_server as srv with tempfile.TemporaryDirectory() as tmp: repo = os.path.join(tmp, "repo") os.makedirs(os.path.join(repo, "branches")) # Minimal git repo on master at a known tip. import subprocess subprocess.check_call(["git", "init", "-b", "master", repo]) subprocess.check_call( ["git", "-C", repo, "commit", "--allow-empty", "-m", "init"] ) head = subprocess.check_output( ["git", "-C", repo, "rev-parse", "HEAD"], text=True ).strip() assessment = aib.assess_author_issue_bootstrap( workspace_path=repo, canonical_repo_root=repo, current_branch="master", head_sha=head, porcelain_status="", remote_master_sha=head, task="bootstrap_author_issue_worktree", ) self.assertTrue( cib.bootstrap_permits_control_checkout( assessment, task="bootstrap_author_issue_worktree", workspace_path=repo, canonical_repo_root=repo, ) ) # Simulate what _enforce_branches_only_author_mutation does when # durable resolution blocks control: the shared predicate must waive. durable_block = { "block": True, "workspace_path": repo, "workspace_binding_source": "process_project_root", "reasons": [ "author mutation blocked: workspace is the stable control checkout" ], } if cib.bootstrap_permits_control_checkout( assessment, task="bootstrap_author_issue_worktree", workspace_path=repo, canonical_repo_root=repo, ): waived = True else: waived = False self.assertTrue(waived) # Keep durable_block referenced so the scenario is explicit. self.assertTrue(durable_block["block"]) if __name__ == "__main__": unittest.main()