firegex/tests/unit/test_installer.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

188 lines
7.7 KiB
Python

"""Installation must select the fork, preserve data and propagate failures."""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
def executable(path, source):
path.write_text(f"#!{sys.executable}\n" + source)
path.chmod(0o755)
@pytest.fixture
def installer_env(tmp_path):
binaries = tmp_path / "bin"
binaries.mkdir()
executable(binaries / "git", """
import os,sys
from pathlib import Path
assert 'https://github.com/Av0idD4rk/firegex.git' in sys.argv
if os.environ.get('CLONE_FAIL'): sys.exit(17)
target=Path(sys.argv[-1]); target.mkdir()
(target/'run.py').write_text('import os,sys,json; from pathlib import Path; '
'Path(os.environ["RESULT"]).write_text(json.dumps(sys.argv[1:])); '
'sys.exit(int(os.environ.get("RUN_EXIT", "0")))')
""")
executable(binaries / "docker", """
import os,sys
info=os.environ.get('DOCKER_INFO','')
print(info)
sys.exit(0 if info else 1)
""")
executable(binaries / "id", "import os; print(os.environ['TEST_UID'])")
executable(binaries / "sudo", """
import os,sys
from pathlib import Path
Path(os.environ['SUDO_USED']).touch()
os.execvp(sys.argv[1],sys.argv[1:])
""")
env = {**os.environ, "PATH": str(binaries) + os.pathsep + os.environ["PATH"],
"FIREGEX_INSTALL_DIR": str(tmp_path / "installation with spaces"),
"RESULT": str(tmp_path / "result.json"), "SUDO_USED": str(tmp_path / "sudo"),
"TEST_UID": "1000", "DOCKER_INFO": '["name=seccomp"]'}
return env
@pytest.mark.parametrize("info,uid,arguments,sudo,forced", [
('["name=seccomp"]', "1000", [], False, False),
('["name=seccomp"]', "1000", ["--standalone"], True, True),
("", "1000", [], True, False),
('["name=rootless"]', "1000", [], True, True),
("", "0", [], False, False),
])
def test_bootstrap_preserves_arguments_and_selects_privileges(installer_env, info, uid, arguments, sudo, forced):
env = {**installer_env, "DOCKER_INFO": info, "TEST_UID": uid}
literal = "EXAMPLE=spaces; $(not-a-command)"
result = subprocess.run(["sh", str(ROOT / "firegex.sh"), *arguments, "--env", literal],
env=env, capture_output=True, text=True)
assert result.returncode == 0, result.stderr
received = json.loads(Path(env["RESULT"]).read_text())
assert received[0] == "start" and literal in received
assert ("--standalone" in received) == forced
assert Path(env["SUDO_USED"]).exists() == sudo
def test_existing_installation_is_untouched(installer_env):
target = Path(installer_env["FIREGEX_INSTALL_DIR"])
target.mkdir()
(target / "important.db").write_bytes(b"keep me")
result = subprocess.run(["sh", str(ROOT / "firegex.sh")], env=installer_env, capture_output=True)
assert result.returncode != 0 and b"already exists" in result.stderr
assert (target / "important.db").read_bytes() == b"keep me"
assert not Path(installer_env["RESULT"]).exists()
@pytest.mark.parametrize("extra,arguments,code", [
({"CLONE_FAIL": "1"}, [], 17),
({"RUN_EXIT": "42"}, [], 42),
({}, ["--prebuilt"], 1),
])
def test_installer_failure_is_reported(installer_env, extra, arguments, code):
result = subprocess.run(["sh", str(ROOT / "firegex.sh"), *arguments],
env={**installer_env, **extra}, capture_output=True)
assert result.returncode == code
@pytest.fixture
def manager(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", [str(ROOT / "run.py"), "status"])
spec = importlib.util.spec_from_file_location("installer_manager", ROOT / "run.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
os.chdir(tmp_path)
source = tmp_path / "source"
(source / "backend/mcp_api").mkdir(parents=True)
(source / "backend/mcp_api/server.py").write_text("# new MCP backend\n")
(source / "backend/pyproject.toml").write_text('[project]\ndependencies=["mcp==2.3.0"]\n')
module.__file__ = str(source / "run.py")
module.g.rootfs_path = str(tmp_path / "rootfs")
monkeypatch.setattr(module, "cleanup_standalone_mounts", lambda: None)
return module
def make_runtime(manager):
rootfs = Path(manager.g.rootfs_path)
for folder in ("execute/db/mcp", "execute/modules", "etc"):
(rootfs / folder).mkdir(parents=True, exist_ok=True)
(rootfs / "execute/app.py").write_text("# existing backend\n")
(rootfs / ".firegex-native-base").write_text(manager.STANDALONE_BASE_RELEASE)
return rootfs
def test_backend_install_preserves_databases_and_native_engine(manager, monkeypatch):
rootfs = make_runtime(manager)
db = rootfs / "execute/db/mcp/control.sqlite3"
engine = rootfs / "execute/modules/fgex-proxy"
db.write_bytes(b"credentials and changes")
engine.write_bytes(b"native engine")
source = Path(manager.__file__).parent / "backend"
(source / "db").mkdir()
(source / "db/unrelated.py").write_text("must not be copied")
calls = []
monkeypatch.setattr(manager.subprocess, "run", lambda *a, **kw: calls.append(a) or SimpleNamespace(returncode=0))
assert manager.prepare_standalone_backend()
assert (rootfs / "execute/mcp_api/server.py").read_text() == "# new MCP backend\n"
assert db.read_bytes() == b"credentials and changes" and engine.read_bytes() == b"native engine"
assert not (rootfs / "execute/db/unrelated.py").exists()
assert len(calls) == 2
assert manager.prepare_standalone_backend()
assert len(calls) == 2 # unchanged requirements do not need a network install
def test_incompatible_runtime_is_refused_before_copy(manager):
rootfs = make_runtime(manager)
(rootfs / ".firegex-native-base").write_text("4.0.0")
assert not manager.prepare_standalone_backend()
assert not (rootfs / "execute/mcp_api").exists()
def test_failed_dependencies_are_not_marked_installed(manager, monkeypatch):
rootfs = make_runtime(manager)
def fail(*a, **kw):
raise subprocess.CalledProcessError(1, "pip")
monkeypatch.setattr(manager.subprocess, "run", fail)
assert not manager.prepare_standalone_backend()
assert not (rootfs / "execute/.firegex-backend-deps").exists()
@pytest.mark.parametrize("download_ok", [False, True])
def test_failed_download_or_checksum_leaves_no_rootfs(manager, monkeypatch, download_ok):
monkeypatch.setattr(manager, "get_architecture", lambda: "amd64")
def download(url, filename):
assert "/5.0.1/" in url
Path(filename).write_bytes(b"truncated or corrupt archive")
return download_ok
monkeypatch.setattr(manager, "download_file", download)
assert not manager.setup_standalone_rootfs()
assert not Path(manager.g.rootfs_path).exists()
assert not list(Path(manager.g.rootfs_path).parent.glob(".firegex-download-*"))
def test_partial_old_runtime_is_not_treated_as_installed(manager):
Path(manager.g.rootfs_path).mkdir()
assert not manager.setup_standalone_rootfs()
@pytest.mark.parametrize("result,code", [(SimpleNamespace(returncode=17), 17), (None, 1)])
def test_docker_build_failure_reaches_the_installer(manager, monkeypatch, result, code):
manager.args = manager.gen_args(["start", "--psw-on-web"])
manager.g.build = True
for name, value in {"should_use_standalone": False, "is_linux": True,
"nfqueue_exists": True, "check_already_running": False}.items():
monkeypatch.setattr(manager, name, lambda value=value: value)
monkeypatch.setattr(manager, "cmd_check", lambda *a, **kw: True)
monkeypatch.setattr(manager, "write_compose", lambda *a, **kw: None)
monkeypatch.setattr(manager, "composecmd", lambda *a, **kw: result)
with pytest.raises(SystemExit) as error:
manager.main()
assert error.value.code == code