From 47a7424ecaa42de3529199d6c5f050f52e7b3acf Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 00:03:20 +0400 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D1=8F=209;=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B6=D1=91=D1=81=D1=82=D0=BA=D0=B8=D1=85=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D0=B9=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/agent.py | 2 +- tests/test_clients.py | 2 +- tests/test_folder.py | 4 ++-- tests/test_runner.py | 10 ++++---- tests/test_self_update_e2e.py | 16 ++++++++----- tests/test_update.py | 43 +++++++++++++++++++---------------- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/agent.py b/src/agent.py index 3fabb69..b0058a8 100644 --- a/src/agent.py +++ b/src/agent.py @@ -33,7 +33,7 @@ import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional -AGENT_VERSION = 5 +AGENT_VERSION = 9 EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new HERE = os.path.dirname(os.path.abspath(__file__)) API = "/rest/api/2" diff --git a/tests/test_clients.py b/tests/test_clients.py index 0af5015..5f88271 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -214,7 +214,7 @@ class ClientsTest(unittest.TestCase): with contextlib.redirect_stdout(out): # хаб недоступен (порт 1), Jira доступен — selftest печатает версию и возвращает 3, а не 1 self.assertEqual(agent.main(["--config", path, "selftest"]), 3) - self.assertIn("jira-agent v5", out.getvalue()) + self.assertIn("jira-agent v%d" % agent.AGENT_VERSION, out.getvalue()) self.assertIn("Jira: доступен", out.getvalue()) if __name__ == "__main__": diff --git a/tests/test_folder.py b/tests/test_folder.py index b73d9c0..59f928f 100644 --- a/tests/test_folder.py +++ b/tests/test_folder.py @@ -157,8 +157,8 @@ class FolderIntegrationTest(unittest.TestCase): def cfg(self): return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token) - def test_version_is_5(self): - self.assertEqual(agent.AGENT_VERSION, 5) + def test_version_is_9(self): + self.assertEqual(agent.AGENT_VERSION, 9) def test_runner_through_folder(self): self.fake.add("AURORA-1", duedate="2026-10-20") diff --git a/tests/test_runner.py b/tests/test_runner.py index 1c43343..9839fc8 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -59,7 +59,7 @@ class RunnerTest(unittest.TestCase): self.assertEqual(body["launcher_version"], 1) self.assertEqual(body["update_state"]["state"], "trial") self.assertEqual(agent.read_update_state(self.runner.updater.state_path)["state"], "ok") # первый успешный claim - self.assertEqual(self.hub.claim_bodies[0]["agent_version"], 5) + self.assertEqual(self.hub.claim_bodies[0]["agent_version"], agent.AGENT_VERSION) def test_claim_without_updater_sends_nulls(self): self.runner.loop() @@ -67,17 +67,17 @@ class RunnerTest(unittest.TestCase): self.assertIsNone(self.hub.claim_bodies[0]["launcher_version"]) def test_idle_with_release_exits_for_update(self): - rel = {"version": 6, "sha256": "s", "content": "x"} + rel = {"version": agent.AGENT_VERSION + 1, "sha256": "s", "content": "x"} self.hub.release = rel self.runner.updater = agent.Updater(self.runner.hub, self.dir.name, 1) - self.runner.updater.check = lambda r: r == {"version": 6, "sha256": "s"} + self.runner.updater.check = lambda r: r == {"version": agent.AGENT_VERSION + 1, "sha256": "s"} self.runner.idle_limit = 5 self.runner.loop() self.assertEqual(self.runner.exit_code, agent.EXIT_UPDATE) self.assertEqual(self.hub.claims, 1) def test_stop_during_check_exits_0(self): - self.hub.release = {"version": 6, "sha256": "s", "content": "x"} + self.hub.release = {"version": agent.AGENT_VERSION + 1, "sha256": "s", "content": "x"} self.runner.updater = agent.Updater(self.runner.hub, self.dir.name, 1) def check(r): self.runner.stop = True # Ctrl-C пришёл, когда версия уже staged @@ -88,7 +88,7 @@ class RunnerTest(unittest.TestCase): self.assertEqual(self.runner.exit_code, 0) # остановка человеком; staged доделает run.py при старте def test_command_first_then_update(self): - self.hub.release = {"version": 6, "sha256": "s", "content": "x"} + self.hub.release = {"version": agent.AGENT_VERSION + 1, "sha256": "s", "content": "x"} self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.runner.updater = agent.Updater(self.runner.hub, self.dir.name, 1) seen = [] diff --git a/tests/test_self_update_e2e.py b/tests/test_self_update_e2e.py index 0421389..b0d3e68 100644 --- a/tests/test_self_update_e2e.py +++ b/tests/test_self_update_e2e.py @@ -1,6 +1,7 @@ import hashlib import json import os +import re import shutil import signal import subprocess @@ -24,7 +25,7 @@ def wait_for(pred, timeout=30): class SelfUpdateE2E(unittest.TestCase): - """run.py + настоящий agent.py: v5 видит публикацию v6, ставит её, v6 подтверждает; Ctrl-C группе — выход 0.""" + """run.py + настоящий agent.py: v видит публикацию v, ставит её, v подтверждает; Ctrl-C группе — выход 0.""" def setUp(self): self.jira = FakeJira() @@ -39,9 +40,12 @@ class SelfUpdateE2E(unittest.TestCase): "auth": {"type": "basic", "user": "me", "password": "secret"}}}, f) with open(os.path.join(SRC, "agent.py"), encoding="utf-8") as f: src = f.read() - content = src.replace("AGENT_VERSION = 5\n", "AGENT_VERSION = 6\n", 1) + self.cur = int(re.search(r"^AGENT_VERSION = (\d+)", src, re.M).group(1)) + cur = "AGENT_VERSION = %d\n" % self.cur + content = src.replace(cur, "AGENT_VERSION = %d\n" % (self.cur + 1), 1) self.assertNotEqual(content, src) - self.hub.release = {"version": 6, "sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(), "content": content} + self.hub.release = {"version": self.cur + 1, "sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(), + "content": content} self.content = content self.log = open(os.path.join(d, "out.log"), "wb") self.proc = subprocess.Popen([sys.executable, os.path.join(d, "run.py")], stdout=self.log, @@ -63,7 +67,7 @@ class SelfUpdateE2E(unittest.TestCase): except (OSError, ValueError): return None - def test_update_to_v6_and_stop_by_ctrl_c(self): + def test_update_to_next_and_stop_by_ctrl_c(self): ok = wait_for(lambda: (self.state() or {}).get("state") == "ok", 60) with open(os.path.join(self.tmp.name, "out.log"), encoding="utf-8", errors="replace") as f: out = f.read() @@ -71,8 +75,8 @@ class SelfUpdateE2E(unittest.TestCase): with open(os.path.join(self.tmp.name, "agent.py"), encoding="utf-8") as f: self.assertEqual(f.read(), self.content) self.assertTrue(os.path.exists(os.path.join(self.tmp.name, "agent.py.prev"))) - self.assertTrue(wait_for(lambda: any(b.get("agent_version") == 6 for b in self.hub.claim_bodies))) - last = [b for b in self.hub.claim_bodies if b.get("agent_version") == 6][-1] + self.assertTrue(wait_for(lambda: any(b.get("agent_version") == self.cur + 1 for b in self.hub.claim_bodies))) + last = [b for b in self.hub.claim_bodies if b.get("agent_version") == self.cur + 1][-1] self.assertEqual(last["launcher_version"], 1) os.killpg(self.proc.pid, signal.SIGINT) # Ctrl-C в терминале — всей группе self.assertEqual(self.proc.wait(30), 0) diff --git a/tests/test_update.py b/tests/test_update.py index 88c95d8..fa84b01 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -7,6 +7,9 @@ import unittest from _common import SETTINGS, agent from fake_hub import FakeHub +CUR = agent.AGENT_VERSION +NEXT = CUR + 1 + class UpdateStateTest(unittest.TestCase): def setUp(self): @@ -76,50 +79,50 @@ class UpdaterCheckTest(unittest.TestCase): return {"version": rel["version"], "sha256": rel["sha256"]} def test_good_version_is_staged(self): - rel = make_release(6) + rel = make_release(NEXT) self.hub.release = rel self.assertTrue(self.u.check(self.ref(rel))) with open(self.u.new_path, encoding="utf-8") as f: self.assertEqual(f.read(), rel["content"]) st = self.u.state() - self.assertEqual((st["state"], st["from_version"], st["to_version"], st["sha256"]), ("staged", 5, 6, rel["sha256"])) + self.assertEqual((st["state"], st["from_version"], st["to_version"], st["sha256"]), ("staged", CUR, NEXT, rel["sha256"])) self.assertEqual(self.selftests, [self.u.new_path]) def test_not_newer_or_disabled_or_no_release(self): - self.hub.release = make_release(5) + self.hub.release = make_release(CUR) self.assertFalse(self.u.check(self.ref(self.hub.release))) self.assertFalse(self.u.check(None)) - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) self.u.enabled = False self.assertFalse(self.u.check(self.ref(self.hub.release))) self.assertEqual(self.hub.release_requests, 0) def test_sha_mismatch_rejected_once(self): - rel = make_release(6) + rel = make_release(NEXT) self.hub.release = dict(rel, content=rel["content"] + "# подмена\n") ref = self.ref(rel) self.hub.release["sha256"] = rel["sha256"] self.assertFalse(self.u.check(ref)) st = self.u.state() - self.assertEqual((st["state"], st["to_version"], st["error"]), ("rejected", 6, "sha256 не сходится")) + self.assertEqual((st["state"], st["to_version"], st["error"]), ("rejected", NEXT, "sha256 не сходится")) self.assertFalse(os.path.exists(self.u.new_path)) self.assertFalse(self.u.check(ref)) self.assertEqual(self.hub.release_requests, 1) # отвергнутую версию больше не качает def test_syntax_error_and_wrong_version_rejected(self): - bad = make_release(6, body="def (:\n") + bad = make_release(NEXT, body="def (:\n") self.hub.release = bad self.assertFalse(self.u.check(self.ref(bad))) self.assertIn("не компилируется", self.u.state()["error"]) - wrong = make_release(7) - wrong = dict(wrong, version=8) + wrong = make_release(NEXT + 1) + wrong = dict(wrong, version=NEXT + 2) self.hub.release = wrong self.assertFalse(self.u.check(self.ref(wrong))) self.assertIn("AGENT_VERSION", self.u.state()["error"]) self.assertEqual(self.selftests, []) def test_selftest_bad_code_rejected(self): - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) self.code = 1 self.assertFalse(self.u.check(self.ref(self.hub.release))) st = self.u.state() @@ -129,7 +132,7 @@ class UpdaterCheckTest(unittest.TestCase): self.assertFalse(os.path.exists(self.u.new_path)) def test_selftest_unavailable_is_transient(self): - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) self.code = 3 ref = self.ref(self.hub.release) self.assertFalse(self.u.check(ref)) @@ -143,35 +146,35 @@ class UpdaterCheckTest(unittest.TestCase): self.assertTrue(self.u.check(ref)) def test_selftest_timeout_is_transient(self): - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) self.code = None self.assertFalse(self.u.check(self.ref(self.hub.release))) self.assertIsNone(self.u.state()) def test_download_failure_is_transient(self): - rel = make_release(6) # в claim есть, а /agent/release — 404 + rel = make_release(NEXT) # в claim есть, а /agent/release — 404 self.assertFalse(self.u.check(self.ref(rel))) self.assertIsNone(self.u.state()) def test_release_changed_between_requests_is_transient(self): - old, new = make_release(6), make_release(7) + old, new = make_release(NEXT), make_release(NEXT + 1) self.hub.release = new self.assertFalse(self.u.check(self.ref(old))) self.assertIsNone(self.u.state()) self.assertEqual(self.selftests, []) def test_rolled_back_same_sha_not_retried(self): - rel = make_release(6) + rel = make_release(NEXT) self.hub.release = rel - agent.write_update_state(self.u.state_path, "rolled_back", 5, 6, rel["sha256"], "код 1 через 3 с") + agent.write_update_state(self.u.state_path, "rolled_back", CUR, NEXT, rel["sha256"], "код 1 через 3 с") self.assertFalse(self.u.check(self.ref(rel))) - fixed = make_release(6, body="print('fixed')\n") + fixed = make_release(NEXT, body="print('fixed')\n") self.hub.release = fixed self.assertTrue(self.u.check(self.ref(fixed))) # тот же номер, другой sha — пробуем def test_without_launcher_only_hint(self): self.u.launcher_version = None - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) logs = [] orig = agent.log agent.log = logs.append @@ -182,11 +185,11 @@ class UpdaterCheckTest(unittest.TestCase): self.assertFalse(self.u.check(self.ref(self.hub.release))) finally: agent.log = orig - self.assertEqual(logs.count("доступна v6; для самообновления запускайте через run.py"), 2) + self.assertEqual(logs.count("доступна v%d; для самообновления запускайте через run.py" % NEXT), 2) self.assertEqual(self.hub.release_requests, 0) def test_stop_during_selftest_is_not_reject(self): - self.hub.release = make_release(6) + self.hub.release = make_release(NEXT) stop = [False] self.u.should_stop = lambda: stop[0]