import os import tempfile import time import unittest from _common import SETTINGS, agent, jira_cfg, no_sleep from fake_hub import FakeHub from fake_jira import FakeJira class RunnerTest(unittest.TestCase): def setUp(self): self.fake = FakeJira() self.hub = FakeHub(SETTINGS) self.dir = tempfile.TemporaryDirectory() self.journal = agent.Journal(os.path.join(self.dir.name, "journal.jsonl")) cfg = jira_cfg(self.fake, hub_url=self.hub.base_url, hub_token=self.hub.token) self.runner = agent.Runner(cfg, agent.HubClient(cfg), agent.JiraClient(cfg), self.journal) self.sleeps = [] self.runner.sleep = self.sleeps.append self.runner.jira.sleep = no_sleep self.runner.idle_limit = 1 self.fake.add("AURORA-1", duedate="2026-10-20") def tearDown(self): self.fake.stop() self.hub.stop() self.dir.cleanup() def kinds(self): return [(r["kind"], r["command_id"]) for r in self.journal.records()] def claim_in_flight(self, ctype, params, pause=0): cid = self.hub.enqueue(ctype, params, pause=pause) self.hub.queue.remove(cid) self.hub.commands[cid]["status"] = "claimed" self.hub.in_flight = cid return cid def test_processes_queue(self): self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.hub.enqueue("refresh", {"keys": ["AURORA-1"]}) self.runner.loop() self.assertEqual(self.hub.results[1]["status"], "done") self.assertEqual(self.hub.results[1]["result"], {"changed": ["duedate"]}) self.assertEqual(self.hub.results[1]["jira_calls"], 2) self.assertEqual(self.hub.results[2]["status"], "done") self.assertEqual(len(self.hub.states), 1) self.assertEqual(self.hub.states[0]["issues"][0]["duedate"], "2026-10-23") self.assertEqual(self.kinds(), [("started", 1), ("finished", 1), ("acked", 1), ("started", 2), ("finished", 2), ("acked", 2)]) self.assertEqual(self.sleeps, []) # пауз не было: last_call_at пуст, потом pause 0 def test_pause_waits_from_last_jira_call(self): self.runner.jira.last_call_at = time.monotonic() self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=3) self.runner.loop() self.assertEqual(len(self.sleeps), 3) self.assertAlmostEqual(sum(self.sleeps), 3, delta=0.1) self.assertEqual(self.hub.results[1]["status"], "done") def test_result_retried_when_hub_down(self): self.hub.fail_results = 2 self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.runner.loop() self.assertEqual(self.hub.results[1]["status"], "done") self.assertEqual(self.sleeps, [10, 10]) self.assertEqual(self.journal.unacked_finished(), []) def test_result_retried_on_connection_error(self): calls = [] real = self.runner.hub.result def flaky(cid, payload): calls.append(1) if len(calls) < 3: raise ConnectionError("нет связи") return real(cid, payload) self.runner.hub.result = flaky self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.runner.loop() self.assertEqual(self.sleeps, [10, 10]) self.assertEqual(self.hub.results[1]["status"], "done") def test_result_rejected_is_acked(self): for cid, code in ((1, 404), (2, 409), (3, 422)): self.hub.reject_results = {cid: code} self.journal.finished(cid, "done", {}, None, 0, 0) self.assertTrue(self.runner.send_result(self.journal.unacked_finished()[0])) self.assertEqual(self.journal.unacked_finished(), []) def test_result_rejected_in_loop(self): self.hub.reject_results = {1: 404} self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.runner.loop() self.assertNotIn(1, self.hub.results) self.assertEqual(self.journal.unacked_finished(), []) self.assertEqual(self.kinds()[-1], ("acked", 1)) def test_recover_unfinished_started(self): cid = self.claim_in_flight("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-20"}, pause=99) self.journal.started(cid) self.runner.loop() self.assertEqual(self.hub.results[cid]["result"], {"changed": []}) # уже так в Jira — записи нет self.assertEqual(self.journal.unfinished_started(), []) self.assertEqual(self.sleeps, []) # пауза при восстановлении не применяется self.assertEqual([r[0] for r in self.fake.writes()], []) def test_recover_ensure_issue_finds_duplicate(self): self.fake.add("AURORA-7", reporter="me", summary="Новая задача") before = len(self.fake.issues) cid = self.claim_in_flight("ensure_issue", {"temp_key": "new:1", "issue_type": "task", "summary": "Новая задача"}) self.journal.started(cid) self.runner.loop() self.assertEqual(self.hub.results[cid]["status"], "done") self.assertEqual(self.hub.results[cid]["result"], {"key": "AURORA-7", "existed": True}) self.assertEqual(len(self.fake.issues), before) self.assertEqual(self.fake.writes(), []) def test_recover_skips_command_not_claimed_anymore(self): cid = self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.hub.queue.remove(cid) self.hub.commands[cid]["status"] = "cancelled" self.journal.started(cid) self.runner.loop() self.assertNotIn(cid, self.hub.results) self.assertEqual(self.kinds(), [("started", cid), ("acked", cid)]) def test_recover_unacked_finished(self): cid = self.claim_in_flight("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.journal.started(cid) self.journal.finished(cid, "done", {"changed": ["duedate"]}, None, 2, 0) self.hub.enqueue("refresh", {"keys": ["AURORA-1"]}) self.runner.loop() self.assertEqual(self.hub.results[cid]["result"], {"changed": ["duedate"]}) self.assertEqual(self.hub.results[2]["status"], "done") self.assertEqual(self.kinds()[:3], [("started", 1), ("finished", 1), ("acked", 1)]) def test_stop_during_pause(self): self.runner.jira.last_call_at = time.monotonic() self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=5) self.runner.sleep = lambda s: setattr(self.runner, "stop", True) self.runner.loop() self.assertEqual(self.journal.records(), []) self.assertEqual(self.hub.results, {}) self.assertEqual(self.hub.commands[1]["status"], "claimed") def test_stop_during_execution_finishes_and_reports(self): self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.hub.enqueue("refresh", {"keys": ["AURORA-1"]}) self.runner.load_settings() run = self.runner.executor.run def run_and_signal(cmd): self.runner.stop = True # сигнал пришёл во время выполнения return run(cmd) self.runner.executor.run = run_and_signal self.runner.loop() self.assertEqual(self.hub.results[1]["status"], "done") self.assertNotIn(2, self.hub.results) # вторая команда не начата self.assertEqual(self.kinds(), [("started", 1), ("finished", 1), ("acked", 1)]) def test_stop_during_execution_hub_down_single_attempt(self): self.hub.fail_results = 5 self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) self.runner.load_settings() run = self.runner.executor.run self.runner.executor.run = lambda cmd: (setattr(self.runner, "stop", True), run(cmd))[1] self.runner.loop() self.assertEqual(self.sleeps, []) self.assertEqual([r["command_id"] for r in self.journal.unacked_finished()], [1]) def test_unexpected_exception_becomes_failed(self): self.runner.load_settings() def boom(cmd): raise KeyError("summary") self.runner.executor.run = boom self.hub.enqueue("ensure_issue", {"temp_key": "new:1", "issue_type": "task"}) self.runner.loop() self.assertEqual(self.hub.results[1]["status"], "failed") self.assertIn("KeyError", self.hub.results[1]["error"]) self.assertEqual(self.journal.unfinished_started(), []) self.assertEqual(self.journal.unacked_finished(), []) def test_recover_waits_when_hub_down(self): cid = self.claim_in_flight("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-20"}) self.journal.started(cid) real = self.runner.hub.command calls = [] def flaky(i): calls.append(1) if len(calls) == 1: raise ConnectionError("нет связи") return real(i) self.runner.hub.command = flaky self.runner.loop() self.assertEqual(self.sleeps, [10]) self.assertEqual(self.hub.results[cid]["status"], "done") def test_settings_reload_on_version_change(self): self.runner.load_settings() self.runner.settings["settings_version"] = 0 self.runner.loop() self.assertEqual(self.runner.settings["settings_version"], 1) def test_settings_reloaded_before_command(self): self.runner.load_settings() self.hub.settings["settings_version"] = 2 self.hub.settings["pacing"] = dict(self.hub.settings["pacing"], ensure_fields=0) self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}) seen = [] real = self.runner.execute self.runner.execute = lambda cmd: (seen.append(self.runner.settings["settings_version"]), real(cmd)) self.runner.loop() self.assertEqual(seen, [2]) def test_hub_down_at_start_keeps_polling(self): calls = [] def hub_settings(): calls.append(1) if len(calls) < 3: raise ConnectionError("нет связи") return dict(SETTINGS) self.runner.hub.settings = hub_settings self.runner.loop() self.assertEqual(len(calls), 3) self.assertEqual(self.sleeps, [0, 0]) if __name__ == "__main__": unittest.main()