fix(agent): полная пауза при времени журнала в будущем, claim без оценки из журнала (#15)

M-6, M-7 (agent#12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-14 12:40:46 +04:00
parent 0532b2476c
commit 3736fb3948
3 changed files with 21 additions and 8 deletions

View File

@ -940,7 +940,9 @@ class Runner:
def restore_pace(self) -> None: def restore_pace(self) -> None:
"""После перезапуска время последнего вызова Jira неизвестно (живёт в памяти). Берём самое позднее время записи """После перезапуска время последнего вызова Jira неизвестно (живёт в памяти). Берём самое позднее время записи
журнала: finished пишется сразу после последнего вызова команды, оценка не раньше настоящего вызова (M-9).""" журнала: finished пишется сразу после последнего вызова команды, оценка не раньше настоящего вызова (M-9).
Время в будущем (часы ушли назад) — считаем «сейчас»: пауза полная (M-6 #12). Оценка служит только паузе:
last_call_iso (уходит хабу в claim) ставит лишь настоящий вызов Jira (M-7 #12)."""
if self.jira.last_call_at is not None: if self.jira.last_call_at is not None:
return return
at = self.journal.last_at() at = self.journal.last_at()
@ -948,11 +950,11 @@ class Runner:
return return
ago = (datetime.now(timezone.utc) - at).total_seconds() ago = (datetime.now(timezone.utc) - at).total_seconds()
if ago < 0: if ago < 0:
log("журнал: время последней записи в будущем (%s) — темп не восстанавливаю" % at.isoformat()) log("журнал: время последней записи в будущем (%s) — пауза перед первой командой полная" % at.isoformat())
return ago = 0
self.jira.last_call_at = time.monotonic() - ago self.jira.last_call_at = time.monotonic() - ago
self.jira.last_call_iso = at.astimezone(timezone.utc).replace(microsecond=0).isoformat() log("темп: последний вызов Jira не позже %s (по журналу)"
log("темп: последний вызов Jira не позже %s (по журналу)" % self.jira.last_call_iso) % at.astimezone(timezone.utc).replace(microsecond=0).isoformat())
def send_result(self, rec: dict) -> bool: def send_result(self, rec: dict) -> bool:
"""Отчёт хабу; повторяется каждые 10 с, пока хаб не подтвердит. 404/409/422 — хаб отверг (команда отменена """Отчёт хабу; повторяется каждые 10 с, пока хаб не подтвердит. 404/409/422 — хаб отверг (команда отменена

View File

@ -16,6 +16,7 @@ class FakeHub:
self.states = [] self.states = []
self.known = [] self.known = []
self.claims = 0 self.claims = 0
self.claim_bodies = []
self.fail_results = 0 self.fail_results = 0
self.reject_results = {} self.reject_results = {}
self.in_flight = None self.in_flight = None
@ -68,6 +69,7 @@ class FakeHub:
def route(self, method, path, body): def route(self, method, path, body):
if path == "/agent/claim": if path == "/agent/claim":
self.claim_bodies.append(body or {})
self.claims += 1 self.claims += 1
if self.in_flight and self.commands[self.in_flight]["status"] == "claimed": if self.in_flight and self.commands[self.in_flight]["status"] == "claimed":
return 200, {"command": None, "settings_version": self.settings["settings_version"]} return 200, {"command": None, "settings_version": self.settings["settings_version"]}

View File

@ -60,14 +60,23 @@ class RunnerTest(unittest.TestCase):
self.runner.loop() self.runner.loop()
self.assertAlmostEqual(sum(self.sleeps), 20, delta=1.5) self.assertAlmostEqual(sum(self.sleeps), 20, delta=1.5)
def test_future_journal_time_ignored(self): def test_future_journal_time_means_full_pause(self):
at = (datetime.now(timezone.utc) + timedelta(hours=1)).replace(microsecond=0).isoformat() at = (datetime.now(timezone.utc) + timedelta(hours=2)).replace(microsecond=0).isoformat()
self.journal.append({"kind": "acked", "command_id": 99, "at": at}) self.journal.append({"kind": "acked", "command_id": 99, "at": at})
self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=30) self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=30)
self.runner.loop() self.runner.loop()
self.assertEqual(self.sleeps, []) # время из будущего — как пустой журнал self.assertAlmostEqual(sum(self.sleeps), 30, delta=1.5) # часы ушли назад — пауза полная (M-6 #12)
self.assertEqual(self.hub.results[1]["status"], "done") self.assertEqual(self.hub.results[1]["status"], "done")
def test_claim_after_restart_sends_no_journal_estimate(self):
at = (datetime.now(timezone.utc) - timedelta(seconds=10)).replace(microsecond=0).isoformat()
self.journal.append({"kind": "acked", "command_id": 99, "at": at})
self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"})
self.runner.loop()
self.assertIsNone(self.hub.claim_bodies[0].get("last_jira_call_at")) # оценка журнала — только для паузы (M-7 #12)
self.assertIsNotNone(self.hub.claim_bodies[-1].get("last_jira_call_at")) # после вызова Jira — настоящее время
self.assertIsNotNone(self.runner.jira.last_call_at)
def test_restore_pace_keeps_existing_last_call(self): def test_restore_pace_keeps_existing_last_call(self):
at = (datetime.now(timezone.utc) - timedelta(seconds=100)).replace(microsecond=0).isoformat() at = (datetime.now(timezone.utc) - timedelta(seconds=100)).replace(microsecond=0).isoformat()
self.journal.append({"kind": "acked", "command_id": 99, "at": at}) self.journal.append({"kind": "acked", "command_id": 99, "at": at})