mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 14:31:00 +00:00
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.
188 lines
7.7 KiB
Python
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
|