Add ./mcp-menu.sh for safe, read-only onboarding and workflow prompt discovery. Includes root checkout health, role prompts, onboarding checklist, Proxmox placeholders, and run-tests fallback. Document in docs/mcp-menu.md with hermetic tests. Closes #478. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
124 lines
4.2 KiB
Python
124 lines
4.2 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",
|
|
"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 _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() |