firegex/tests/standalone/test_installer_rootfs.py
av0idd4rk f214cecf9f Add one-command installer for the MCP fork
Bootstrap the fork through run.py, build Docker from source, and prepare standalone with a checksum-pinned native runtime plus the MCP backend and Python dependencies. Preserve existing installations and data, propagate Docker build failures, and update README/setup guides. Validation: 347 unit tests and a real standalone-rootfs MCP smoke test passed in Docker.
2026-10-23 10:11:37 +03:00

92 lines
3.8 KiB
Python

"""Opt-in packaging smoke: a real upstream rootfs must start this fork's MCP.
Run in a disposable Linux namespace with NET_ADMIN and SYS_ADMIN:
FIREGEX_TEST_INSTALLER=1 ./run_tests.sh standalone/test_installer_rootfs.py
FIREGEX_STANDALONE_ARCHIVE may name a cached archive; its checksum is still checked.
"""
import asyncio
import importlib.util
import json
import os
from pathlib import Path
import shutil
import signal
import subprocess
import sys
import time
import pytest
import requests
from mcp import Client
from mcp.client.streamable_http import streamable_http_client, create_mcp_http_client
from helpers.net import free_port
ROOT = Path(__file__).resolve().parents[2]
@pytest.mark.root
def test_standalone_runtime_exposes_fork_mcp(tmp_path, monkeypatch):
if os.getenv("FIREGEX_TEST_INSTALLER") != "1":
pytest.skip("Opt in with FIREGEX_TEST_INSTALLER=1 in a disposable namespace")
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", [str(ROOT / "run.py"), "status"])
spec = importlib.util.spec_from_file_location("standalone_installer", ROOT / "run.py")
manager = importlib.util.module_from_spec(spec)
spec.loader.exec_module(manager)
os.chdir(tmp_path)
manager.g.rootfs_path = str(tmp_path / "rootfs")
manager.g.pid_file = str(tmp_path / "backend.pid")
manager.g.configfile = str(tmp_path / "config.json")
manager.g.standalone_mode = True
port = free_port()
manager.args = manager.gen_args(["start", "--standalone", "--host", "127.0.0.1",
"--port", str(port), "--threads", "2", "--psw-on-web"])
cached = os.getenv("FIREGEX_STANDALONE_ARCHIVE")
if cached:
monkeypatch.setattr(manager, "download_file", lambda url, path: bool(shutil.copyfile(cached, path)))
assert manager.setup_standalone_rootfs()
rootfs = Path(manager.g.rootfs_path)
pid = None
try:
assert manager.setup_standalone_mounts()
manager.run_standalone()
pid = manager.read_pid_file()
address = f"http://127.0.0.1:{port}"
for _ in range(60):
try:
if requests.get(address + "/api/status", timeout=1).status_code == 200:
break
except requests.RequestException:
pass
time.sleep(0.5)
else:
pytest.fail(Path("/tmp/app.log").read_text()[-8000:])
token = json.loads(subprocess.check_output([
"chroot", str(rootfs), "/bin/sh", "-c",
"cd /execute && python3 -m mcp_api.tokens create --name installer-test --role operator --hours 1",
], text=True))
assert requests.post(address + "/mcp/", json={}, timeout=5).status_code == 401
async def scenario():
async with create_mcp_http_client(headers={"Authorization": "Bearer " + token["token"]}) as http:
async with Client(streamable_http_client(address + "/mcp/", http_client=http)) as client:
catalog = await client.list_tools()
assert len(catalog.tools) == 28
overview = await client.call_tool("fgx_overview", {})
assert overview.structured_content["ok"], overview
checked = await client.call_tool("fgx_python_check", {"code": "pass"})
assert checked.structured_content["data"]["ok"], checked
asyncio.run(scenario())
assert (rootfs / "execute/.firegex-backend-deps").is_file()
assert (rootfs / "execute/db/mcp/control.sqlite3").is_file()
finally:
pid = pid or manager.read_pid_file()
if pid:
try:
os.killpg(pid, signal.SIGTERM)
os.waitpid(pid, 0)
except ProcessLookupError:
pass
manager.cleanup_standalone_mounts()
manager.remove_pid_file()