feat(hub): agent_release в claim, GET /agent/release, состояние обновления в GET /agent (#22)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
eb66c2144a
commit
87a0d3d36c
@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from . import backup, db, mirror, queue, relay, settings
|
||||
from . import backup, db, mirror, queue, relay, release, settings
|
||||
from .commands import CommandError
|
||||
from .paths import backups_dir as default_backups_dir
|
||||
from .paths import secure_dir
|
||||
@ -39,9 +39,11 @@ class ApiError(Exception):
|
||||
|
||||
|
||||
class Hub:
|
||||
def __init__(self, db_path, backups_dir=None):
|
||||
def __init__(self, db_path, backups_dir=None, release_dir=None):
|
||||
self.db_path = Path(db_path)
|
||||
self.backups_dir = Path(backups_dir) if backups_dir else default_backups_dir()
|
||||
self.release_dir = Path(release_dir) if release_dir else self.db_path.parent / "release"
|
||||
self.agent_update = {"update_state": None, "launcher_version": None} # последнее из claim; в памяти (R-3)
|
||||
self.claim_wait = 30
|
||||
secure_dir(self.backups_dir) # каталог копий закрыт при каждом старте, а не только после первой копии
|
||||
conn = self.conn()
|
||||
@ -63,6 +65,18 @@ class Hub:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def note_agent_update(self, body):
|
||||
"""update_state и launcher_version из claim: хранятся в памяти, агент шлёт их в каждом claim (R-3).
|
||||
Значение неверного типа игнорируется — claim из-за него не отвергается."""
|
||||
us = body.get("update_state")
|
||||
if us is None or isinstance(us, dict):
|
||||
if us is not None and isinstance(us.get("error"), str):
|
||||
us = {**us, "error": us["error"][:500]}
|
||||
self.agent_update["update_state"] = us
|
||||
lv = body.get("launcher_version")
|
||||
if lv is None or (isinstance(lv, int) and not isinstance(lv, bool)):
|
||||
self.agent_update["launcher_version"] = lv
|
||||
|
||||
def make_backup(self, label):
|
||||
conn = self.conn()
|
||||
try:
|
||||
@ -91,6 +105,7 @@ def _not_found(obj, what):
|
||||
|
||||
|
||||
def h_claim(hub, conn, m, q, body):
|
||||
hub.note_agent_update(body)
|
||||
deadline = time.monotonic() + hub.claim_wait
|
||||
while True:
|
||||
cmd = queue.claim(conn, body.get("agent_version"), body.get("settings_version"), body.get("last_jira_call_at"),
|
||||
@ -98,7 +113,16 @@ def h_claim(hub, conn, m, q, body):
|
||||
if cmd or time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(1)
|
||||
return 200, {"command": cmd, "settings_version": settings.get(conn, "settings_version")}
|
||||
rel = release.current(hub.release_dir)
|
||||
return 200, {"command": cmd, "settings_version": settings.get(conn, "settings_version"),
|
||||
"agent_release": {"version": rel["version"], "sha256": rel["sha256"]} if rel else None}
|
||||
|
||||
|
||||
def h_agent_release(hub, conn, m, q, body):
|
||||
rel = release.load(hub.release_dir)
|
||||
if rel is None:
|
||||
raise ApiError(404, "no_release", "версия агента не опубликована")
|
||||
return 200, rel
|
||||
|
||||
|
||||
def h_agent_settings(hub, conn, m, q, body):
|
||||
@ -231,7 +255,11 @@ def h_refresh(hub, conn, m, q, body):
|
||||
|
||||
|
||||
def h_agent_get(hub, conn, m, q, body):
|
||||
return 200, queue.agent_view(conn)
|
||||
view = queue.agent_view(conn)
|
||||
rel = release.current(hub.release_dir)
|
||||
us, lv = hub.agent_update["update_state"], hub.agent_update["launcher_version"]
|
||||
view.update(release=rel, update_state=us, launcher_version=lv, update_hint=release.hint(rel, view, us, lv))
|
||||
return 200, view
|
||||
|
||||
|
||||
def h_sync_runs(hub, conn, m, q, body):
|
||||
@ -269,6 +297,7 @@ ROUTES = [
|
||||
("POST", r"^/agent/commands/(\d+)/result$", h_agent_result, "agent"),
|
||||
("POST", r"^/agent/state$", h_agent_state, "agent"),
|
||||
("GET", r"^/agent/known-keys$", h_known_keys, "agent"),
|
||||
("GET", r"^/agent/release$", h_agent_release, "agent"),
|
||||
("GET", r"^/issues$", h_issues, "client"),
|
||||
("GET", r"^/issues/([^/]+)$", h_issue, "client"),
|
||||
("GET", r"^/board$", h_board, "client"),
|
||||
|
||||
@ -5,7 +5,7 @@ import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from jira_hub import api, db, settings
|
||||
from jira_hub import api, db, release, settings
|
||||
from jira_hub.client import HubClient, HubError
|
||||
|
||||
|
||||
@ -394,3 +394,71 @@ def test_page_reads_hub_and_has_no_embedded_export():
|
||||
assert "Нет в зеркале" in html
|
||||
assert 'author: "viewer"' in html and "/batches/" in html
|
||||
assert "KEY_RE.test(l.source) && KEY_RE.test(l.target)" in html # I-1: ключи связей из зеркала идут в разметку карточек
|
||||
|
||||
|
||||
def _publish(hub, tmp_path, version):
|
||||
src = tmp_path / f"agent{version}.py"
|
||||
src.write_text(f"AGENT_VERSION = {version}\nprint('v{version}')\n", encoding="utf-8")
|
||||
return release.publish(hub["hub"].release_dir, src)
|
||||
|
||||
|
||||
def test_claim_release_field(hub, tmp_path):
|
||||
a = hub["agent"]
|
||||
assert a.post("/agent/claim", {"agent_version": 5})["agent_release"] is None
|
||||
m = _publish(hub, tmp_path, 6)
|
||||
r = a.post("/agent/claim", {"agent_version": 5})
|
||||
assert r["command"] is None and r["agent_release"] == {"version": 6, "sha256": m["sha256"]}
|
||||
|
||||
|
||||
def test_agent_release_route(hub, tmp_path):
|
||||
with pytest.raises(HubError) as e:
|
||||
hub["agent"].get("/agent/release")
|
||||
assert e.value.status == 404 and e.value.error == "no_release"
|
||||
m = _publish(hub, tmp_path, 6)
|
||||
r = hub["agent"].get("/agent/release")
|
||||
assert r == {"version": 6, "sha256": m["sha256"], "content": "AGENT_VERSION = 6\nprint('v6')\n"}
|
||||
with pytest.raises(HubError) as e:
|
||||
hub["client"].get("/agent/release")
|
||||
assert e.value.status == 401
|
||||
|
||||
|
||||
def test_claim_update_state_shown_in_agent(hub, tmp_path):
|
||||
_publish(hub, tmp_path, 7)
|
||||
us = {"state": "rolled_back", "from_version": 6, "to_version": 7, "sha256": "s", "error": "код 1 через 3 с" + "x" * 600,
|
||||
"at": "2026-10-14T20:00:00+00:00"}
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 6, "update_state": us, "launcher_version": 1})
|
||||
v = hub["client"].get("/agent")
|
||||
assert v["release"]["version"] == 7 and v["release"]["published_at"]
|
||||
assert v["launcher_version"] == 1
|
||||
assert v["update_state"]["state"] == "rolled_back" and len(v["update_state"]["error"]) == 500
|
||||
assert v["update_hint"].startswith("откат с v7 на v6: код 1 через 3 с")
|
||||
|
||||
|
||||
def test_claim_without_update_fields(hub, tmp_path):
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 6, "update_state": {"state": "ok", "from_version": 5,
|
||||
"to_version": 6}, "launcher_version": 1})
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 4}) # агент v4: полей нет
|
||||
v = hub["client"].get("/agent")
|
||||
assert v["launcher_version"] is None and v["update_state"] is None
|
||||
|
||||
|
||||
def test_claim_bad_update_types_ignored(hub):
|
||||
a = hub["agent"]
|
||||
a.post("/agent/claim", {"agent_version": 5, "update_state": {"state": "ok"}, "launcher_version": 1})
|
||||
r = a.post("/agent/claim", {"agent_version": 5, "update_state": "ok", "launcher_version": "1"})
|
||||
assert "command" in r
|
||||
a.post("/agent/claim", {"agent_version": 5, "update_state": {"state": "ok"}, "launcher_version": True})
|
||||
v = hub["client"].get("/agent")
|
||||
assert v["update_state"] == {"state": "ok"} and v["launcher_version"] == 1
|
||||
|
||||
|
||||
def test_hint_without_launcher(hub, tmp_path):
|
||||
_publish(hub, tmp_path, 6)
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 5})
|
||||
v = hub["client"].get("/agent")
|
||||
assert v["online"] is True and v["update_hint"] == "агент запущен без run.py — обновится только вручную"
|
||||
|
||||
|
||||
def test_agent_view_without_release(hub):
|
||||
v = hub["client"].get("/agent")
|
||||
assert v["release"] is None and v["update_state"] is None and v["launcher_version"] is None and v["update_hint"] is None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user