firegex/tests/docker_mcp.py
av0idd4rk 863422f202 Add authenticated MCP server and agent guide
Expose 28 tools with shared REST operations, role-scoped bearer authentication, revisions, durable retries, snapshots, jobs and timed rollback. Add Docker verification harnesses and setup/workflow documentation in docs/mcp.md and inst.md. Validated with 330 unit tests, two consecutive 25-case MCP/import runs, and restart recovery; the full upstream regression run remains incomplete.
2026-10-23 09:46:44 +03:00

105 lines
4.9 KiB
Python

"""Reproducible isolated Docker tests. --vm supplies a kernel when WSL lacks NFT_SOCKET."""
import argparse
import json
from pathlib import Path
import subprocess
import time
import uuid
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
def docker(*args, check=True, **kw):
return subprocess.run(["docker", *map(str, args)], cwd=ROOT, check=check, **kw)
def reports_ok(results):
valid = True
for path in sorted(results.glob("mcp-*.xml")):
cases = list(ET.parse(path).iter("testcase"))
failed = [c for c in cases if c.find("failure") is not None or c.find("error") is not None]
skipped = [c for c in cases if c.find("skipped") is not None]
mcp_skipped = [c for c in skipped if "test_mcp" in c.get("classname", "")]
print(f"{path.name}: {len(cases)} cases, {len(failed)} failures, {len(skipped)} skipped", flush=True)
for case in mcp_skipped:
print(f"INCOMPLETE: {case.get('name')}: {case.find('skipped').get('message')}", flush=True)
valid &= not failed and not mcp_skipped
return valid and len(list(results.glob("mcp-*.xml"))) == 2
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vm", action="store_true")
parser.add_argument("--no-build", action="store_true")
parser.add_argument("--full", action="store_true", help="Run all unit and integration regressions")
parser.add_argument("--tests", nargs="+", help="Run selected pytest paths instead of the default selection")
parser.add_argument("--timeout", type=int, default=3600, help="VM runtime limit in seconds")
args = parser.parse_args()
tag = uuid.uuid4().hex[:10]
results = ROOT / ".test-results" / f"mcp-{tag}"
results.mkdir(parents=True)
selection = (args.tests or ["unit", "integration" if args.full else "integration/test_mcp.py"]) + ["-q", "-x"]
(results / "selection.json").write_text(json.dumps(selection), encoding="utf-8")
if not args.no_build:
docker("build", "-t", "firegex-mcp:dev", ".")
docker("build", "-f", "tests/Dockerfile.mcp", "-t", "firegex-mcp:test", ".")
if args.vm:
docker("build", "-f", "tests/Dockerfile.mcp-vm", "-t", "firegex-mcp:vm", ".")
name = f"firegex-mcp-{tag}"
created = False
try:
command = ["create", "--name", name, "--mount", f"type=bind,source={results},target=/results"]
if args.vm:
command += ["firegex-mcp:vm"]
else:
command += ["--cap-add", "NET_ADMIN", "--cap-add", "NET_RAW", "--cap-add", "SYS_NICE",
"--sysctl", "net.ipv4.conf.all.route_localnet=1", "-e", "NTHREADS=2", "firegex-mcp:test"]
docker(*command)
created = True
docker("start", name)
if args.vm:
# Poll in bounded intervals, so Ctrl-C always cleans up our container.
started = time.monotonic()
print(f"VM running; logs and reports: {results}", flush=True)
while docker("inspect", "-f", "{{.State.Running}}", name, capture_output=True,
text=True).stdout.strip() == "true":
if time.monotonic() - started > args.timeout:
raise TimeoutError("VM test deadline exceeded; inspect the saved logs")
time.sleep(5)
exits = [results / f"exit-{i}" for i in (1, 2)] + [results / "exit-restart"]
passed = all(p.exists() and p.read_text().strip() == "0" for p in exits)
else:
passed = True
for i in (1, 2):
result = docker("exec", "-u", "root", name, "bash", "/test/tests/run_tests.sh",
*(["--set-pass"] if i == 1 else []), *selection,
f"--junitxml=/results/mcp-{i}.xml", check=False)
passed &= result.returncode == 0
if not passed:
break
py = ["exec", "-u", "root", name, "/test/tests/.venv/bin/python", "/test/tests/mcp_restart_probe.py"]
docker(*py, "prepare")
docker("stop", "-t", "1", name)
time.sleep(6)
docker("start", name)
# Health polling does not create/revoke any credentials.
for _ in range(60):
if docker("exec", name, "curl", "-fsS", "http://127.0.0.1:4444/api/status",
check=False, capture_output=True).returncode == 0:
break
time.sleep(1)
docker(*py, "verify")
passed &= reports_ok(results)
print(f"{'PASS' if passed else 'FAILED/INCOMPLETE'}; reports: {results}", flush=True)
return 0 if passed else 1
finally:
if created:
with (results / "container.log").open("w", encoding="utf-8") as log:
docker("logs", name, check=False, stdout=log, stderr=subprocess.STDOUT)
docker("rm", "-f", name, check=False)
if __name__ == "__main__":
raise SystemExit(main())