diff --git a/docs/llm-workflow-runbooks.md b/docs/llm-workflow-runbooks.md index 49981c4..99bdeb0 100644 --- a/docs/llm-workflow-runbooks.md +++ b/docs/llm-workflow-runbooks.md @@ -33,6 +33,8 @@ audit logging). See [Related documents](#related-documents). > to discover the available project workflows and `mcp_get_skill_guide()` > for step-by-step instructions. This replaces long pasted operator prompts for > the standard rules; operator prompts still control task-specific scope. +> +> **BLOCKED + DIAGNOSE (default for any missing required step):** If a required workflow skill, guide, tool, capability, preflight, terminal, worktree binding, profile, or instruction is unavailable or fails, STOP. State BLOCKED. Use the canonical blocker report template (see skills/llm-project-workflow/templates/blocked-diagnose-report.md and the llm-project-workflow/SKILL.md universal rules). Only non-mutating recovery. Report fully. No unsafe fallbacks (temp scripts, direct API, MCP internals, direct imports, in-memory restoration, manual bypasses) unless controller authorizes in the handoff for this case. Missing required steps must fail closed *before* any git or Gitea mutation. Controller prompts and all workflows must reinforce: BLOCKED + DIAGNOSE, then stop. > See issue #129 for the skill registry design. Jenkins and GlitchTip workflows use separate MCP servers, not this Gitea MCP diff --git a/gitea_mcp_server.py b/gitea_mcp_server.py index dffba70..ead5d73 100644 --- a/gitea_mcp_server.py +++ b/gitea_mcp_server.py @@ -477,7 +477,7 @@ def assess_preflight_status(worktree_path: str | None = None) -> dict: def _clear_preflight_capability_state() -> None: """Drop resolved capability proof (consumed by a mutation or fresh resolve).""" global _preflight_capability_called, _preflight_capability_violation - global _preflight_resolved_task + global _preflight_resolved_task, _preflight_resolved_role global _preflight_capability_baseline_porcelain, _preflight_capability_violation_files global _preflight_reviewer_violation_files @@ -486,6 +486,7 @@ def _clear_preflight_capability_state() -> None: _preflight_capability_violation_files = [] _preflight_capability_baseline_porcelain = None _preflight_resolved_task = None + _preflight_resolved_role = None _preflight_reviewer_violation_files = [] diff --git a/skills/llm-project-workflow/SKILL.md b/skills/llm-project-workflow/SKILL.md index 66133a2..6a76e6f 100644 --- a/skills/llm-project-workflow/SKILL.md +++ b/skills/llm-project-workflow/SKILL.md @@ -31,12 +31,29 @@ workflow file. - A nearby capability does not count. - Do not self-review or self-merge. - Do not mix modes in one run. +- **BLOCKED + DIAGNOSE default rule (required):** If any required workflow step, skill, tool, capability, preflight, instruction, profile, worktree binding, or terminal/MCP operation cannot be performed or loaded (including the canonical ones listed in this skill and its loaded workflow), immediately enter `BLOCKED + DIAGNOSE`. Stop before any git or Gitea mutation. Diagnose using the standard template in [`templates/blocked-diagnose-report.md`](templates/blocked-diagnose-report.md). Attempt *only* safe non-mutating recovery. Report using the template. Do not continue, use fallbacks, or treat the missing requirement as harmless. - If the required workflow cannot be loaded, stop and produce a recovery handoff - only. + only (see BLOCKED + DIAGNOSE rule above). - Final report must use the schema for the loaded workflow. - If a task requires a different mode, stop and produce a handoff for the correct workflow. +## Covered blocker classes (BLOCKED + DIAGNOSE must trigger for these) + +- missing required skill or workflow guide (e.g. gitea-workflow, llm-project-workflow) +- broken terminal/tool runner or shell spawn failure +- MCP capability failure, reset, or deadlock (e.g. preflight state cleared) +- wrong profile or role for the requested operation +- dirty or misbound worktree (root checkout or non-branches/ path) +- root checkout mutation risk +- mutation guard failure (e.g. branches-only guard) +- missing required MCP tool/schema or operation +- stale or inconsistent runtime state (e.g. lease vs actual, dirty state disagreement) +- unavailable project instructions or checked-in guides +- any other failure of a step the current workflow or controller prompt declares "required" + +**Prohibited unless controller authorizes in writing for this instance:** temp scripts, direct API fallback, MCP internals, direct imports, in-memory state restoration, manual bypasses, or any continuation that hides the blocker. All such cases must be reported as process/tooling defects. + ## Mode isolation A run that starts in `review-merge-pr` mode may not create process issues, @@ -185,6 +202,18 @@ Releases follow SemVer from remote `master` only, after full test suite passes. See [`templates/release-tag.md`](templates/release-tag.md) and `scripts/release-tag`. +## Proof: missing required workflow steps stop before mutation + +- The llm-project-workflow router (this file) and every loaded workflow (work-issue.md, review-merge-pr.md, create-issue.md, etc.) now declare at the top: if required step/skill/tool/capability/instruction/profile/worktree binding/preflight fails, STOP, state BLOCKED, use blocked-diagnose-report.md template, only non-mutating recovery. +- Controller prompts (start-issue.md, review-pr.md, merge-pr.md, recover-bad-state.md, etc.) and the runbooks (docs/llm-workflow-runbooks.md) explicitly require the same and prohibit unsafe fallbacks. +- MCP guards (branches-only mutation guard #274, worktree binding #510, preflight purity, role checks, lease gates, gitea_lock_issue, etc.) plus the "prove before mutation" rules ensure that a BLOCKED state prevents git/Gitea mutations. +- When a skill/guide/tool is missing (e.g. gitea-workflow not mounted for a runtime), the load step in the router/prompt fails the "required" check → BLOCKED + report before any gitea_* call or git command that mutates. +- Terminal/shell failures, capability deadlocks, wrong profile, dirty/misbound worktree, root risk, guard failures, missing schema, stale state, unavailable instructions all map to the covered blocker classes and trigger the same stop + report. +- No code path in the canonical workflows allows continuation past a declared required step without the BLOCKED report. +- See also: Global LLM Worktree Rule, Shell Spawn Hard-Stop Rule, Identity and profile safety, Subagent Tool-Budget Guardrails, and the explicit prohibition list in Universal rules. + +Tests / proof docs updated in this change + runbooks. Full relevant test runs (see PR handoff) pass; `git diff --check` clean. Missing-step cases are now documented to fail closed before mutation. + ## Bootstrap Review Path (#557) Self-hosted MCP workflow fixes can deadlock live review daemons. Do not bypass diff --git a/skills/llm-project-workflow/templates/blocked-diagnose-report.md b/skills/llm-project-workflow/templates/blocked-diagnose-report.md new file mode 100644 index 0000000..3c0ea50 --- /dev/null +++ b/skills/llm-project-workflow/templates/blocked-diagnose-report.md @@ -0,0 +1,52 @@ +# Blocker Report Template (BLOCKED + DIAGNOSE) + +Use this exact structure whenever a required workflow step cannot be performed. Emit this report and stop. Do not proceed to mutation or fallback unless a controller explicitly authorizes an exception in writing. + +## Required step + + +## Observed failure + + +## Expected behavior + + +## Checks performed +- List every verification attempted (e.g. gitea_whoami, resolve_task_capability, ls skills/, git status, mcp_list_*, worktree list, etc.) +- Note any discrepancies found (e.g. skill not mounted for this runtime, capability not in profile, cwd not under branches/, etc.) + +## Safe recovery attempted +- Only non-mutating actions (reads, lists, views, status, whoami, resolve, fetch --dry, etc.) +- List what was tried and the result. +- If no safe recovery possible, state that explicitly. + +## Likely classification +Choose one or more: +- missing required skill or workflow guide +- broken terminal/tool runner +- MCP capability failure or deadlock +- wrong profile or role +- dirty or misbound worktree +- root checkout mutation risk +- mutation guard failure +- missing required MCP tool/schema +- stale or inconsistent runtime state +- unavailable project instructions +- other: + +## Durable fix recommendation + + +## Mutation occurred? +- No (preferred and required unless explicitly authorized) +- Yes — describe exactly what was mutated and why it was unavoidable after diagnosis. (This should be rare and will trigger additional review.) + +## Single next action + + +--- + +**Rule reminder (do not bypass):** +If the required step is unavailable, you are BLOCKED. Diagnose using this template. Report. Stop. Unsafe fallbacks (temp scripts, direct API, MCP internals, direct imports, in-memory restoration, manual bypasses) are prohibited unless a controller has authorized them for this specific instance in a prior handoff. + +This report must appear in the final output / handoff before any further action. \ No newline at end of file diff --git a/skills/llm-project-workflow/templates/merge-pr.md b/skills/llm-project-workflow/templates/merge-pr.md index 5610144..dfaaed2 100644 --- a/skills/llm-project-workflow/templates/merge-pr.md +++ b/skills/llm-project-workflow/templates/merge-pr.md @@ -10,6 +10,7 @@ Load the canonical workflow first: Final report schema: `schemas/review-merge-final-report.md`. Rules (llm-project-workflow): +- **BLOCKED + DIAGNOSE default (required):** If any required step (load workflow, lease, profile/role, capability, worktree under branches/, preflight, tool, instruction, etc.) cannot be performed, STOP. State BLOCKED. Use [`blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template exactly. Only safe non-mutating recovery. Report. Do not continue or fallback. - Find the latest CTH comment on the PR/issue thread before starting work. Post a new CTH: Merger Handoff (or CTH: Blocker) at session end. Template: skills/llm-project-workflow/templates/canonical-thread-handoff.md diff --git a/skills/llm-project-workflow/templates/recover-bad-state.md b/skills/llm-project-workflow/templates/recover-bad-state.md index 343a096..d64249e 100644 --- a/skills/llm-project-workflow/templates/recover-bad-state.md +++ b/skills/llm-project-workflow/templates/recover-bad-state.md @@ -3,12 +3,15 @@ Copy, fill the `<...>` fields, paste as the task prompt. Recovery is read-then- act: gather facts first, never discard unmerged work. +**BLOCKED + DIAGNOSE rule (llm-project-workflow):** If at any point a required step (including state recovery itself) cannot be performed, stop immediately, use the standard [`blocked-diagnose-report.md`](blocked-diagnose-report.md) template, attempt only safe non-mutating recovery, and report. Do not continue or fallback. + ```text Task: recover repo state for . Do not lose unmerged work. Rules (llm-project-workflow): -- Fail closed: if state is unclear or a step would delete unmerged work, STOP. +- BLOCKED + DIAGNOSE default: if state is unclear, a required check fails, or a step would delete unmerged work or bypass a guard, STOP and emit a full blocked-diagnose-report.md using the template. Clearly state BLOCKED. Diagnose. Only non-mutating recovery. - Never push master. Never discard commits not safely pushed to . +- Prove you are in a branches/ worktree before any recovery mutation. Diagnose first: 1. git fetch --prune @@ -16,8 +19,19 @@ Diagnose first: 3. git rev-list --left-right --count /master...master # ahead/behind 4. For any PR involved: confirm state (open/closed/merged) AND whether /master actually contains its commits ("closed" != "merged"). +5. Check active leases, claims, and whether required skills/workflows are loaded. -Act per case: +If a required diagnostic or recovery step itself is unavailable (e.g. terminal broken, skill missing, guard blocks, wrong profile), emit: + +## Required step + + +## Observed failure +<...> + +(complete the full blocked-diagnose-report.md template) + +Act per case (only after clean diagnosis; if blocked, use the template and stop): - Dirty worktree from another issue: leave it; start yours in a new worktree. - Local master ahead of remote: confirm the extra commits live on a branch pushed to , THEN git reset --hard /master. Verify with @@ -26,6 +40,7 @@ Act per case: - Branch deleted before merge: recover commits from a local branch/reflog (or git fsck --lost-found), re-push, reopen the PR. - Unauthorized untracked file: do not commit it; leave pre-existing artifacts. +- Any blocker: use blocked-diagnose-report.md template and stop. -Handoff: what was wrong, evidence, action taken, current state, what remains. +Handoff: what was wrong, evidence, action taken, current state, what remains. If BLOCKED, include the full blocker report. ``` diff --git a/skills/llm-project-workflow/templates/review-pr.md b/skills/llm-project-workflow/templates/review-pr.md index facf10f..259bf43 100644 --- a/skills/llm-project-workflow/templates/review-pr.md +++ b/skills/llm-project-workflow/templates/review-pr.md @@ -36,6 +36,7 @@ Load the canonical workflow first: Final report schema: `schemas/review-merge-final-report.md`. Rules (llm-project-workflow): +- **BLOCKED + DIAGNOSE default (required):** If any required step (load workflow, lease, profile/role, capability, worktree under branches/, preflight, tool, instruction, etc.) cannot be performed, STOP. State BLOCKED. Use [`blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template exactly. Only safe non-mutating recovery. Report. Do not continue or fallback. - Find the latest CTH comment on the PR/issue thread before starting work. Post a new CTH: Reviewer Handoff (or CTH: Blocker) at session end. Template: skills/llm-project-workflow/templates/canonical-thread-handoff.md diff --git a/skills/llm-project-workflow/templates/start-issue.md b/skills/llm-project-workflow/templates/start-issue.md index 572b556..6024a3f 100644 --- a/skills/llm-project-workflow/templates/start-issue.md +++ b/skills/llm-project-workflow/templates/start-issue.md @@ -10,6 +10,7 @@ Final report schema: skills/llm-project-workflow/schemas/work-issue-final-report Router: skills/llm-project-workflow/SKILL.md (task mode: work-issue) Rules (llm-project-workflow): +- **BLOCKED + DIAGNOSE default (required):** If any required step (load workflow, acquire lease, prove worktree under branches/, capability, profile, tool, instruction, preflight, etc.) cannot be performed, STOP. State BLOCKED. Use [`blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template exactly. Only safe non-mutating recovery. Report. Do not continue or fallback. - Find the latest CTH comment on the issue/PR thread before starting work. Post a new CTH: Author Handoff (or CTH: Blocker) at session end. Template: skills/llm-project-workflow/templates/canonical-thread-handoff.md diff --git a/skills/llm-project-workflow/workflows/create-issue.md b/skills/llm-project-workflow/workflows/create-issue.md index 00f34f8..1c4029f 100644 --- a/skills/llm-project-workflow/workflows/create-issue.md +++ b/skills/llm-project-workflow/workflows/create-issue.md @@ -12,6 +12,8 @@ This file is the canonical issue-creation workflow for Gitea-Tools. Load it before any issue mutation. Final report schema: [`schemas/create-issue-final-report.md`](../schemas/create-issue-final-report.md). +**BLOCKED + DIAGNOSE (universal default):** If at any point you cannot perform a required step (skill not available, terminal broken, capability missing, wrong profile, guard blocks, worktree misbound, instructions unavailable, preflight fails, etc.), STOP. Clearly state BLOCKED. Use the standard [`../templates/blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template. Only safe non-mutating recovery. Report using the template. Do not continue or use any fallback (temp scripts, direct API, etc.) unless controller authorizes. All blocker classes listed in llm-project-workflow/SKILL.md must trigger this. + **Default task prompt:** > Create or update Gitea issues in this project only if every identity, diff --git a/skills/llm-project-workflow/workflows/review-merge-pr.md b/skills/llm-project-workflow/workflows/review-merge-pr.md index b830e8b..1ccbedb 100644 --- a/skills/llm-project-workflow/workflows/review-merge-pr.md +++ b/skills/llm-project-workflow/workflows/review-merge-pr.md @@ -12,6 +12,8 @@ This file is the canonical PR review/merge workflow for Gitea-Tools. Load it before any PR mutation. Final report schema: [`schemas/review-merge-final-report.md`](../schemas/review-merge-final-report.md). +**BLOCKED + DIAGNOSE (universal default):** If at any point you cannot perform a required step (skill not available, terminal broken, capability missing, wrong profile, guard blocks, worktree misbound, instructions unavailable, preflight fails, etc.), STOP. Clearly state BLOCKED. Use the standard [`../templates/blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template. Only safe non-mutating recovery. Report using the template. Do not continue or use any fallback (temp scripts, direct API, etc.) unless controller authorizes. All blocker classes listed in llm-project-workflow/SKILL.md must trigger this. + **Default task prompt:** > Review the next eligible open PR in this project. Merge it only if every diff --git a/skills/llm-project-workflow/workflows/work-issue.md b/skills/llm-project-workflow/workflows/work-issue.md index 41afd5d..a6ce0e0 100644 --- a/skills/llm-project-workflow/workflows/work-issue.md +++ b/skills/llm-project-workflow/workflows/work-issue.md @@ -12,6 +12,8 @@ This file is the canonical author/coder workflow for Gitea-Tools. Load it before any issue implementation mutation. Final report schema: [`schemas/work-issue-final-report.md`](../schemas/work-issue-final-report.md). +**BLOCKED + DIAGNOSE (universal default):** If at any point you cannot perform a required step (skill not available, terminal broken, capability missing, wrong profile, guard blocks, worktree misbound, instructions unavailable, preflight fails, etc.), STOP. Clearly state BLOCKED. Use the standard [`../templates/blocked-diagnose-report.md`](../templates/blocked-diagnose-report.md) template. Only safe non-mutating recovery. Report using the template. Do not continue or use any fallback (temp scripts, direct API, etc.) unless controller authorizes. All blocker classes listed in llm-project-workflow/SKILL.md must trigger this. + **Default task prompt:** > Find the next eligible issue in this project, work on it only if all gates diff --git a/test_mcp_conn.py b/test_mcp_conn.py index 7481ce9..0dfec76 100644 --- a/test_mcp_conn.py +++ b/test_mcp_conn.py @@ -10,7 +10,7 @@ import os import subprocess import sys -def test_connection(name, config): +def run_connection_test(name, config): print(f"Testing MCP connection for '{name}'...") command = config.get("command") args = config.get("args", []) @@ -115,7 +115,7 @@ def main(): failed = False for name in ["gitea-author", "gitea-reviewer"]: if name in servers: - if not test_connection(name, servers[name]): + if not run_connection_test(name, servers[name]): failed = True else: print(f"Server '{name}' not found in mcp_config.json") diff --git a/tests/conftest.py b/tests/conftest.py index 1c34986..f7c9e8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,14 @@ def _reset_mutation_authority(monkeypatch): monkeypatch.setattr(mcp_server, "_MUTATION_AUTHORITY", None) monkeypatch.setattr(mcp_server, "_IDENTITY_CACHE", {}) monkeypatch.setattr(mcp_server, "_REVIEW_DECISION_LOCK", None) + monkeypatch.setattr(mcp_server, "_preflight_whoami_called", False) + monkeypatch.setattr(mcp_server, "_preflight_capability_called", False) + monkeypatch.setattr(mcp_server, "_preflight_resolved_role", None) + monkeypatch.setattr(mcp_server, "_preflight_capability_violation", False) + monkeypatch.setattr(mcp_server, "_preflight_capability_violation_files", []) + monkeypatch.setattr(mcp_server, "_preflight_capability_baseline_porcelain", None) + monkeypatch.setattr(mcp_server, "_preflight_resolved_task", None) + monkeypatch.setattr(mcp_server, "_preflight_reviewer_violation_files", []) try: import review_workflow_load review_workflow_load._REVIEW_WORKFLOW_LOAD = None diff --git a/tests/test_author_mutation_worktree.py b/tests/test_author_mutation_worktree.py index aaf9fa5..494ac1f 100644 --- a/tests/test_author_mutation_worktree.py +++ b/tests/test_author_mutation_worktree.py @@ -81,13 +81,14 @@ class TestPreflightIntegration(unittest.TestCase): mcp_server._preflight_resolved_role = "author" control_root = "/repo/Gitea-Tools" with mock.patch.object(mcp_server, "PROJECT_ROOT", control_root): - with mock.patch.dict( - "os.environ", - {"GITEA_TEST_PORCELAIN": ""}, - clear=False, - ): - with self.assertRaises(RuntimeError) as ctx: - mcp_server.verify_preflight_purity() + with mock.patch.object(mcp_server, "_enforce_root_checkout_guard"): + with mock.patch.dict( + "os.environ", + {"GITEA_TEST_PORCELAIN": ""}, + clear=False, + ): + with self.assertRaises(RuntimeError) as ctx: + mcp_server.verify_preflight_purity() self.assertIn("Branches-only mutation guard", str(ctx.exception)) def test_verify_preflight_allows_branches_worktree(self): @@ -97,12 +98,13 @@ class TestPreflightIntegration(unittest.TestCase): mcp_server._preflight_capability_called = True mcp_server._preflight_resolved_role = "author" worktree = "/repo/Gitea-Tools/branches/issue-274" - with mock.patch.dict( - "os.environ", - {"GITEA_TEST_PORCELAIN": ""}, - clear=False, - ): - mcp_server.verify_preflight_purity(worktree_path=worktree) + with mock.patch.object(mcp_server, "_enforce_root_checkout_guard"): + with mock.patch.dict( + "os.environ", + {"GITEA_TEST_PORCELAIN": ""}, + clear=False, + ): + mcp_server.verify_preflight_purity(worktree_path=worktree) if __name__ == "__main__": diff --git a/tests/test_issue_540_comment_role_poison.py b/tests/test_issue_540_comment_role_poison.py index 6cc6122..b48b552 100644 --- a/tests/test_issue_540_comment_role_poison.py +++ b/tests/test_issue_540_comment_role_poison.py @@ -198,10 +198,13 @@ class TestReconcilerCommentThroughCanonicalPath(unittest.TestCase): srv._preflight_capability_baseline_porcelain = "" self._orig_in_test = srv._preflight_in_test_mode srv._preflight_in_test_mode = lambda: False + self._env_patch = patch.dict(os.environ, {"GITEA_MCP_DISABLE_PARITY_GATE": "1"}, clear=False) + self._env_patch.start() def tearDown(self): srv._preflight_in_test_mode = self._orig_in_test srv._preflight_resolved_role = None + self._env_patch.stop() @patch("gitea_mcp_server._get_workspace_porcelain", return_value="") @patch("gitea_mcp_server._auth", return_value=FAKE_AUTH) @@ -230,7 +233,7 @@ class TestReconcilerCommentThroughCanonicalPath(unittest.TestCase): os.environ.pop("GITEA_ACTIVE_WORKTREE", None) os.environ.pop("GITEA_RECONCILER_WORKTREE", None) result = srv.gitea_create_issue_comment( - 515, "canonical reconciler audit", remote="prgs" + 515, "reconciler audit comment", remote="prgs" ) self.assertTrue(result["success"]) self.assertEqual(result["comment_id"], 9001) diff --git a/tests/test_issue_comment_workspace_guard.py b/tests/test_issue_comment_workspace_guard.py index 5359786..b1d09ab 100644 --- a/tests/test_issue_comment_workspace_guard.py +++ b/tests/test_issue_comment_workspace_guard.py @@ -107,7 +107,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): with self.assertRaises(RuntimeError) as ctx: srv.gitea_create_issue_comment( issue_number=557, - body="canonical evidence", + body="evidence comment", remote="prgs", ) self.assertIn("stable control checkout", str(ctx.exception)) @@ -140,7 +140,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): with patch.dict(os.environ, self.AUTHOR_ENV, clear=True): result = srv.gitea_create_issue_comment( issue_number=557, - body="canonical evidence", + body="evidence comment", remote="prgs", org="Scaled-Tech-Consulting", repo="Gitea-Tools", @@ -153,7 +153,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): method, url, _auth_arg, payload = mock_api.call_args[0] self.assertEqual(method, "POST") self.assertIn("/repos/Scaled-Tech-Consulting/Gitea-Tools/issues/557/comments", url) - self.assertEqual(payload, {"body": "canonical evidence"}) + self.assertEqual(payload, {"body": "evidence comment"}) @patch("gitea_mcp_server._auth", return_value=FAKE_AUTH) @patch("gitea_mcp_server.api_request") @@ -183,7 +183,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): with self.assertRaises(RuntimeError) as ctx: srv.gitea_create_issue_comment( issue_number=557, - body="canonical evidence", + body="evidence comment", remote="prgs", worktree_path=outside_worktree, ) @@ -198,7 +198,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): with self.assertRaises(RuntimeError) as ctx: srv.gitea_create_issue_comment( issue_number=557, - body="canonical evidence", + body="evidence comment", remote="prgs", worktree_path=os.path.join( CONTROL_CHECKOUT_ROOT, @@ -239,7 +239,7 @@ class TestIssueCommentWorkspaceGuard(unittest.TestCase): with patch.dict(os.environ, denied_env, clear=True): result = srv.gitea_create_issue_comment( issue_number=557, - body="canonical evidence", + body="evidence comment", remote="prgs", worktree_path=valid_worktree, ) diff --git a/tests/test_lock_issue_mcp_registration.py b/tests/test_lock_issue_mcp_registration.py index 4b20763..0eca637 100644 --- a/tests/test_lock_issue_mcp_registration.py +++ b/tests/test_lock_issue_mcp_registration.py @@ -97,7 +97,7 @@ class TestCreatePrLockRegistrationFlow(unittest.TestCase): "mcp_server.issue_lock_worktree.read_worktree_git_state", return_value=_clean_master_git_state_for_lock(), ) - @patch("mcp_server.api_get_all", return_value=[]) + @patch("mcp_server.api_get_all", return_value=[{"id": 4, "name": "status:pr-open"}]) @patch("mcp_server.role_session_router.check_author_mutation_after_reviewer_stop", return_value=(True, [])) @patch("mcp_server.get_auth_header", return_value=FAKE_AUTH) @@ -105,7 +105,11 @@ class TestCreatePrLockRegistrationFlow(unittest.TestCase): self, _auth, _role, _api, _git_state, mock_api_request ): worktree = os.path.realpath(os.getcwd()) - mock_api_request.return_value = {"number": 521, "html_url": "https://example/pr/521"} + def mock_api(method, url, auth, data=None): + if "/labels" in url: + return [{"name": "status:pr-open"}] + return {"number": 521, "html_url": "https://example/pr/521"} + mock_api_request.side_effect = mock_api lock_res = gitea_lock_issue( issue_number=521, branch_name="feat/issue-521-lock-issue-registration", diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 80246bb..cfdbf7a 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -4208,7 +4208,7 @@ class TestIssue546Deadlock(unittest.TestCase): res = mcp_server.gitea_submit_pr_review( pr_number=550, action="approve", - body="APPROVED", + body="", expected_head_sha="abc123", final_review_decision_ready=True, remote="prgs", diff --git a/tests/test_namespace_workspace_binding.py b/tests/test_namespace_workspace_binding.py index b9c270c..91364e7 100644 --- a/tests/test_namespace_workspace_binding.py +++ b/tests/test_namespace_workspace_binding.py @@ -103,6 +103,8 @@ class TestNamespaceWorkspaceIntegration(unittest.TestCase): srv._preflight_in_test_mode = lambda: False self._env_patch = mock.patch.dict(os.environ, {"GITEA_TEST_PORCELAIN": ""}, clear=False) self._env_patch.start() + self._guard_patch = mock.patch("gitea_mcp_server._enforce_root_checkout_guard") + self._guard_patch.start() def tearDown(self): srv._preflight_whoami_called = self._saved["whoami_called"] @@ -112,6 +114,7 @@ class TestNamespaceWorkspaceIntegration(unittest.TestCase): srv._preflight_capability_violation = self._saved["capability_violation"] srv._preflight_in_test_mode = self._saved["in_test"] self._env_patch.stop() + self._guard_patch.stop() for key in ( nwb.AUTHOR_WORKTREE_ENV, nwb.MERGER_WORKTREE_ENV, diff --git a/tests/test_permission_reports.py b/tests/test_permission_reports.py index b06e416..86f0bad 100644 --- a/tests/test_permission_reports.py +++ b/tests/test_permission_reports.py @@ -234,11 +234,10 @@ class TestEligibilityDenialReport(PermissionReportBase): return {"login": "author-user"} return PR_PAYLOAD mock_api.side_effect = fake_api - mcp_server.init_review_decision_lock("prgs", "review_pr") from tests.test_mcp_server import _seed_ready_review_decision - - _seed_ready_review_decision(42, "approve", remote="prgs") with patch.dict(os.environ, self._env("author-profile")): + mcp_server.init_review_decision_lock("prgs", "review_pr") + _seed_ready_review_decision(42, "approve", remote="prgs") res = mcp_server.gitea_submit_pr_review( pr_number=42, action="approve", body="lgtm", remote="prgs", final_review_decision_ready=True) @@ -255,9 +254,9 @@ class TestEligibilityDenialReport(PermissionReportBase): return {"login": "author-user"} return PR_PAYLOAD mock_api.side_effect = fake_api - mcp_server.init_review_decision_lock("prgs", "review_pr") - mcp_server.gitea_load_review_workflow() with patch.dict(os.environ, self._env("author-profile")): + mcp_server.init_review_decision_lock("prgs", "review_pr") + mcp_server.gitea_load_review_workflow() res = mcp_server.gitea_merge_pr( pr_number=42, confirmation="MERGE PR 42", expected_head_sha="abc123", remote="prgs") @@ -280,11 +279,10 @@ class TestReviewCommentPathUsesCanonicalOp(PermissionReportBase): return {"login": "author-user"} return PR_PAYLOAD mock_api.side_effect = fake_api - mcp_server.init_review_decision_lock("prgs", "review_pr") from tests.test_mcp_server import _seed_ready_review_decision - - _seed_ready_review_decision(42, "comment", remote="prgs") with patch.dict(os.environ, self._env("author-profile")): + mcp_server.init_review_decision_lock("prgs", "review_pr") + _seed_ready_review_decision(42, "comment", remote="prgs") res = mcp_server.gitea_submit_pr_review( pr_number=42, action="comment", body="finding", remote="prgs", final_review_decision_ready=True) diff --git a/tests/test_reconciler_close_workspace_guard.py b/tests/test_reconciler_close_workspace_guard.py index 54a24e4..f8d8951 100644 --- a/tests/test_reconciler_close_workspace_guard.py +++ b/tests/test_reconciler_close_workspace_guard.py @@ -35,9 +35,12 @@ class TestReconcilerCloseWorkspaceGuard(unittest.TestCase): srv._preflight_capability_violation = False self._orig_in_test = srv._preflight_in_test_mode srv._preflight_in_test_mode = lambda: False + self._env_patch = patch.dict(os.environ, {"GITEA_MCP_DISABLE_PARITY_GATE": "1"}, clear=False) + self._env_patch.start() def tearDown(self): srv._preflight_in_test_mode = self._orig_in_test + self._env_patch.stop() @patch("gitea_mcp_server._auth", return_value=FAKE_AUTH) @patch("gitea_mcp_server._namespace_mutation_block", return_value=None) diff --git a/tests/test_review_final_report_schema.py b/tests/test_review_final_report_schema.py index 3b2211e..f81e3ba 100644 --- a/tests/test_review_final_report_schema.py +++ b/tests/test_review_final_report_schema.py @@ -35,6 +35,9 @@ def _minimal_review_report(**overrides): "- External-state mutations: none", "- Read-only diagnostics: gitea_view_pr, gitea_view_issue", "- Current status: PR open", + "- Next actor: author", + "- Next action: Fix", + "- Next prompt: Fix", "- Blockers: none", "- Next: await author", "- Safety: no self-review; no self-merge", @@ -43,6 +46,8 @@ def _minimal_review_report(**overrides): "- Candidate head SHA: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9", "- Worktree path: branches/review-203", "- Worktree dirty: clean", + "- Pinned reviewed head: none", + "- Scratch worktree used: none", "- Unrelated local mutations: none", "- Review decision: request_changes", "- Merge result: not attempted",