Read-only MCP tool gitea_workflow_dashboard plus mcp-menu entry so operators and LLMs can see PR/issue queues, leases, terminal locks, blockers, and exact next-safe prompts without reconstructing state from comments. Never assigns work or presents blocked/terminal-locked items as safe.
155 lines
6.0 KiB
Python
155 lines
6.0 KiB
Python
"""Hermetic tests for repository-root MCP operator menu (#478)."""
|
|
import os
|
|
import stat
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SCRIPT = REPO_ROOT / "mcp-menu.sh"
|
|
DOCS = REPO_ROOT / "docs" / "mcp-menu.md"
|
|
|
|
REQUIRED_MENU_LABELS = (
|
|
"Project status / root checkout health",
|
|
"Workflow dashboard (queue, leases, next safe action)",
|
|
"Author workflow prompts",
|
|
"Reviewer workflow prompts",
|
|
"Merger workflow prompts",
|
|
"Reconciler workflow prompts",
|
|
"Onboarding new project to this MCP workflow",
|
|
"Proxmox deployment menu placeholder",
|
|
"Create Proxmox LXC placeholder",
|
|
"Run tests",
|
|
"Exit",
|
|
)
|
|
|
|
DANGEROUS_PATTERNS = (
|
|
"git push --force",
|
|
"git push -f",
|
|
"--force-with-lease",
|
|
"delete-branch",
|
|
"gitea_delete_branch",
|
|
"issue-locks",
|
|
"issue_lock_store",
|
|
"curl ",
|
|
"wget ",
|
|
)
|
|
|
|
|
|
class TestMcpMenuScript(unittest.TestCase):
|
|
def setUp(self):
|
|
self.assertTrue(SCRIPT.is_file(), "mcp-menu.sh must exist at repo root")
|
|
self.content = SCRIPT.read_text(encoding="utf-8")
|
|
|
|
def test_script_exists_at_repo_root(self):
|
|
self.assertEqual(SCRIPT.name, "mcp-menu.sh")
|
|
self.assertEqual(SCRIPT.parent, REPO_ROOT)
|
|
|
|
def test_executable_bit_is_set(self):
|
|
mode = SCRIPT.stat().st_mode
|
|
self.assertTrue(mode & stat.S_IXUSR, "mcp-menu.sh must be executable by owner")
|
|
|
|
def test_shebang(self):
|
|
first_line = self.content.splitlines()[0]
|
|
self.assertEqual(first_line, "#!/usr/bin/env bash")
|
|
|
|
def test_uses_set_euo_pipefail(self):
|
|
self.assertIn("set -euo pipefail", self.content)
|
|
|
|
def test_no_dangerous_commands(self):
|
|
lowered = self.content.lower()
|
|
for pattern in DANGEROUS_PATTERNS:
|
|
with self.subTest(pattern=pattern):
|
|
self.assertNotIn(pattern.lower(), lowered)
|
|
|
|
def test_no_branch_deletion_verbs(self):
|
|
for token in ("git branch -D", "git branch -d", "push :refs"):
|
|
with self.subTest(token=token):
|
|
self.assertNotIn(token, self.content)
|
|
|
|
def test_contains_required_menu_labels(self):
|
|
for label in REQUIRED_MENU_LABELS:
|
|
with self.subTest(label=label):
|
|
self.assertIn(label, self.content)
|
|
|
|
def test_run_tests_prefers_run_tests_sh(self):
|
|
self.assertIn('run-tests.sh', self.content)
|
|
run_tests_idx = self.content.index("run_tests()")
|
|
run_tests_body = self.content[run_tests_idx : run_tests_idx + 800]
|
|
pytest_idx = run_tests_body.find("pytest")
|
|
run_tests_sh_idx = run_tests_body.find("run-tests.sh")
|
|
self.assertGreater(pytest_idx, 0)
|
|
self.assertGreater(run_tests_sh_idx, 0)
|
|
self.assertLess(run_tests_sh_idx, pytest_idx)
|
|
|
|
def test_run_tests_fail_closed_without_runner(self):
|
|
self.assertIn("fail closed", self.content.lower())
|
|
self.assertIn("exit 1", self.content)
|
|
|
|
def test_proxmox_entries_are_placeholders(self):
|
|
self.assertIn("TODO / issue-backed", self.content)
|
|
self.assertIn("NOT implemented", self.content)
|
|
|
|
def test_root_health_shows_required_fields(self):
|
|
health_fn = self._extract_function("show_root_checkout_health")
|
|
for snippet in (
|
|
"status --short --branch",
|
|
"rev-parse HEAD",
|
|
"prgs/master",
|
|
"WARNING: root checkout",
|
|
):
|
|
with self.subTest(snippet=snippet):
|
|
self.assertIn(snippet, health_fn)
|
|
|
|
def test_docs_mention_mcp_menu_sh(self):
|
|
self.assertTrue(DOCS.is_file(), "docs/mcp-menu.md must exist")
|
|
docs_text = DOCS.read_text(encoding="utf-8")
|
|
self.assertIn("./mcp-menu.sh", docs_text)
|
|
self.assertIn("placeholder", docs_text.lower())
|
|
|
|
def test_workflow_dashboard_menu_entry_is_read_only(self):
|
|
# #605: dashboard entry documents gitea_workflow_dashboard and never
|
|
# mutates Gitea / assigns work from the shell menu.
|
|
label = "Workflow dashboard (queue, leases, next safe action)"
|
|
self.assertIn(label, self.content)
|
|
dash_fn = self._extract_function("show_workflow_dashboard_help")
|
|
self.assertIn("gitea_workflow_dashboard", dash_fn)
|
|
self.assertIn("gitea_allocate_next_work", dash_fn)
|
|
self.assertIn("Read-only", dash_fn)
|
|
self.assertIn("never presented as safe", dash_fn.lower())
|
|
for bad in ("gitea_merge_pr", "gitea_submit_pr_review", "git push"):
|
|
with self.subTest(bad=bad):
|
|
self.assertNotIn(bad, dash_fn)
|
|
docs_text = DOCS.read_text(encoding="utf-8")
|
|
self.assertIn("gitea_workflow_dashboard", docs_text)
|
|
self.assertIn("Workflow dashboard", docs_text)
|
|
|
|
def test_reviewer_skip_stale_request_changes_prompt_discoverable(self):
|
|
# #482: the skip-already-reviewed-stale-REQUEST_CHANGES reviewer prompt
|
|
# must be reachable from the reviewer menu and documented.
|
|
label = "Skip already-reviewed stale REQUEST_CHANGES PR and hand off to author"
|
|
reviewer_fn = self._extract_function("show_reviewer_prompts")
|
|
self.assertIn(label, reviewer_fn, "new reviewer prompt label must appear in the reviewer menu")
|
|
# The prompt must instruct: no duplicate terminal review mutation for a
|
|
# non-stale REQUEST_CHANGES head, and must carry the author handoff.
|
|
self.assertIn("do NOT submit another terminal review mutation", reviewer_fn)
|
|
self.assertIn("author-ready fix prompt", reviewer_fn)
|
|
docs_text = DOCS.read_text(encoding="utf-8")
|
|
self.assertIn("REQUEST_CHANGES", docs_text)
|
|
|
|
def _extract_function(self, name: str) -> str:
|
|
marker = f"{name}() {{"
|
|
start = self.content.index(marker)
|
|
depth = 0
|
|
for idx in range(start, len(self.content)):
|
|
char = self.content[idx]
|
|
if char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return self.content[start : idx + 1]
|
|
self.fail(f"Could not parse function {name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |