Files
EvoScientist/tests/test_system_update_http.py

197 lines
7.1 KiB
Python

"""HTTP-layer tests for the system update routes."""
import json
from pathlib import Path
import pytest
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from EvoScientist import update_check
from EvoScientist.langgraph_dev import http as http_mod
async def _allow(request):
return None
@pytest.fixture
def client(monkeypatch, tmp_path):
monkeypatch.setattr(http_mod, "_authenticate_system", _allow, raising=False)
monkeypatch.setattr(http_mod, "_authenticate_system_write", _allow, raising=False)
monkeypatch.setattr(http_mod, "_schedule_self_exit", lambda delay=0.5: None)
monkeypatch.setenv("EVOSCIENTIST_UPDATE_STAGING_DIR", str(tmp_path))
app = Starlette(
routes=[
Route("/internal/system/update", http_mod.post_system_update, methods=["POST"]),
Route(
"/internal/system/update/status",
http_mod.get_system_update_status,
methods=["GET"],
),
Route(
"/internal/system/rollback-versions",
http_mod.get_system_rollback_versions,
methods=["GET"],
),
Route(
"/internal/system/update/rollback",
http_mod.post_system_update_rollback,
methods=["POST"],
),
]
)
return TestClient(app)
def _info(has_update, breaking_db=False):
return {
"current_version": "0.2.8",
"latest_version": "0.3.0",
"has_update": has_update,
"release_url": None,
"release_notes": None,
"published_at": None,
"cached": False,
"warning": None,
"breaking_db": breaking_db,
}
def _probe_docker():
from EvoScientist.updater import SystemProbe
return SystemProbe(in_container=True, invocation_id=None, uv_tool_pkg=False, pipx_pkg=False)
def _probe_uvtool():
from EvoScientist.updater import SystemProbe
return SystemProbe(in_container=False, invocation_id=None, uv_tool_pkg=True, pipx_pkg=False)
def test_update_409_when_no_update(client, monkeypatch):
monkeypatch.setattr(update_check, "get_update_info", lambda force=False: _info(has_update=False))
res = client.post("/internal/system/update")
assert res.status_code == 409
assert res.json()["code"] == "ALREADY_UP_TO_DATE"
def test_update_docker_returns_guidance(client, monkeypatch):
monkeypatch.setattr(update_check, "get_update_info", lambda force=False: _info(has_update=True))
monkeypatch.setattr(http_mod, "probe_system", lambda environ=None: _probe_docker())
res = client.post("/internal/system/update")
assert res.status_code == 200
assert res.json()["status"] == "manual"
assert "docker compose" in res.json()["guidance"]
def test_update_202_spawns_updater(client, monkeypatch, tmp_path):
spawned = {}
monkeypatch.setattr(update_check, "get_update_info", lambda force=False: _info(has_update=True))
monkeypatch.setattr(http_mod, "probe_system", lambda environ=None: _probe_uvtool())
monkeypatch.setattr(
update_check,
"download_update",
lambda version=None: {
"version": "0.3.0",
"file": "EvoScientist-0.3.0-py3-none-any.whl",
"path": str(tmp_path / "v0.3.0" / "EvoScientist-0.3.0-py3-none-any.whl"),
"suggested_command": "...",
},
)
(tmp_path / "v0.3.0").mkdir(parents=True)
(tmp_path / "v0.3.0" / "EvoScientist-0.3.0-py3-none-any.whl").write_bytes(b"x")
monkeypatch.setattr(
http_mod,
"spawn_updater",
lambda u, p, pid: spawned.setdefault("ok", (str(u), str(p), pid)),
)
res = client.post("/internal/system/update")
assert res.status_code == 202
body = res.json()
assert body["status"] == "applying"
assert body["need_restart"] is True
assert spawned["ok"][2] > 0 # parent pid
plan = json.loads((tmp_path / "v0.3.0" / "plan.json").read_text())
assert plan["version"] == "0.3.0"
assert plan["respawn_command"]
def test_update_409_when_update_in_progress(client, monkeypatch, tmp_path):
monkeypatch.setattr(update_check, "get_update_info", lambda force=False: _info(has_update=True))
monkeypatch.setattr(http_mod, "probe_system", lambda environ=None: _probe_uvtool())
monkeypatch.setattr(
update_check,
"download_update",
lambda version=None: {
"version": "0.3.0",
"file": "f.whl",
"path": str(tmp_path / "v0.3.0" / "f.whl"),
"suggested_command": "...",
},
)
(tmp_path / "v0.3.0").mkdir(parents=True)
(tmp_path / "v0.3.0" / "f.whl").write_bytes(b"x")
(tmp_path / "v0.3.0" / "plan.lock").write_text("") # lock held
monkeypatch.setattr(http_mod, "spawn_updater", lambda u, p, pid: None)
res = client.post("/internal/system/update")
assert res.status_code == 409
assert res.json()["code"] == "UPDATE_IN_PROGRESS"
def test_update_breaking_requires_confirm(client, monkeypatch):
monkeypatch.setattr(
update_check,
"get_update_info",
lambda force=False: _info(has_update=True, breaking_db=True),
)
res = client.post("/internal/system/update")
assert res.status_code == 409
assert res.json()["code"] == "BREAKING_DB_CONFIRM_REQUIRED"
def test_status_none_then_result(client, tmp_path):
assert client.get("/internal/system/update/status").json()["status"] == "none"
(tmp_path / "last-result.json").write_text(json.dumps({"status": "success", "version": "0.3.0"}))
assert client.get("/internal/system/update/status").json()["status"] == "success"
def test_rollback_versions_passthrough(client, monkeypatch):
monkeypatch.setattr(
update_check,
"list_rollback_versions",
lambda limit=3: [{"version": "0.2.9", "published_at": "p", "release_url": "u"}],
)
res = client.get("/internal/system/rollback-versions")
assert res.json()["versions"][0]["version"] == "0.2.9"
def test_rollback_rejects_disallowed_version(client, monkeypatch):
monkeypatch.setattr(update_check, "is_allowed_rollback", lambda v: False)
res = client.post("/internal/system/update/rollback", json={"version": "0.0.1"})
assert res.status_code == 400
def test_rollback_202_uses_same_pipeline(client, monkeypatch, tmp_path):
monkeypatch.setattr(update_check, "is_allowed_rollback", lambda v: True)
monkeypatch.setattr(http_mod, "probe_system", lambda environ=None: _probe_uvtool())
monkeypatch.setattr(
update_check,
"download_update",
lambda version=None: {
"version": version,
"file": "f.whl",
"path": str(tmp_path / f"v{version}" / "f.whl"),
"suggested_command": "...",
},
)
(tmp_path / "v0.2.9").mkdir(parents=True)
(tmp_path / "v0.2.9" / "f.whl").write_bytes(b"x")
monkeypatch.setattr(http_mod, "spawn_updater", lambda u, p, pid: None)
res = client.post("/internal/system/update/rollback", json={"version": "0.2.9"})
assert res.status_code == 202
plan = json.loads((tmp_path / "v0.2.9" / "plan.json").read_text())
assert plan["version"] == "0.2.9"