diff --git a/src/agent.py b/src/agent.py index ca5f1db..1fd9da8 100644 --- a/src/agent.py +++ b/src/agent.py @@ -1241,7 +1241,8 @@ class Executor: return got def refresh_history(self, p: dict, cmd: dict, fetched: List[dict]) -> Optional[dict]: - """Журнал после состояния: since — задачи этого refresh с updated не раньше since, jql — стартовая загрузка. + """Журнал после состояния: jql — стартовая загрузка (первой, журнал целиком), since — задачи этого refresh с + updated не раньше since, ещё не прочитанные стартовой. Ответ не 2xx Jira или хаба — текст в error (since, отправка) или backfill_error (jql), refresh остаётся done; обрыв связи — как во всём refresh.""" h = p.get("history") @@ -1263,10 +1264,16 @@ class Executor: for raw in raws: if raw["key"] in read: continue - read.add(raw["key"]) items.extend(self.changelog_items(raw["key"], self.full_changelog(raw, out["incomplete"]), since, field_id_of)) + read.add(raw["key"]) # только прочитанную: упавшую на ней стартовую часть дочитает since + if h.get("jql"): # первой: журнал задач стартовой выборки — целиком (I-1) + try: + collect(self.search_changelog(h["jql"]), None) + except HttpError as e: + out["backfill_error"] = "HTTP %d %s" % (e.code, e.text[:500]) + log("refresh: стартовая загрузка журнала не прочитана: %s" % out["backfill_error"]) if h.get("since"): since = _jira_time(h["since"]) try: @@ -1275,8 +1282,9 @@ class Executor: changed = [] for r in fetched: upd = _jira_time(((r.get("fields") or {}).get("updated"))) - if upd is not None and upd >= since and r["key"] not in changed: + if upd is not None and upd >= since and r["key"] not in read: changed.append(r["key"]) + changed = list(dict.fromkeys(changed)) for i in range(0, len(changed), 100): collect(self.search_changelog("key in (%s)" % ",".join(changed[i:i + 100])), since) except HttpError as e: @@ -1285,12 +1293,6 @@ class Executor: except ValueError as e: out["error"] = str(e) log("refresh: %s" % e) - if h.get("jql"): - try: - collect(self.search_changelog(h["jql"]), None) - except HttpError as e: - out["backfill_error"] = "HTTP %d %s" % (e.code, e.text[:500]) - log("refresh: стартовая загрузка журнала не прочитана: %s" % out["backfill_error"]) sent_keys = set() for i in range(0, len(items), self.HISTORY_BATCH): chunk = items[i:i + self.HISTORY_BATCH] diff --git a/tests/test_history.py b/tests/test_history.py index f5b0920..42955e1 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -174,6 +174,37 @@ class RefreshHistoryTest(Base): self.assertEqual(sorted(c["key"] for c in self.sent()), ["X", "Y"]) self.assertEqual(out["result"]["history"]["changes"], 2) + def test_since_and_jql_keeps_old_history(self): # I-1: задача в обеих выборках — журнал целиком + jql = "(%s) AND sprint in openSprints() AND statusCategory != Done" % BASE + self.fake.add("X", updated=CHANGED) + self.fake.add_history("X", "2020-01-01T00:00:00.000+0300", [_st("status", "a", "b")]) + self.fake.add_history("X", "2026-10-22T12:10:00.000+0300", [_st("status", "b", "c")]) + self.fake.jql_results[jql] = ["X"] + out = self.refresh({"since": SINCE, "jql": jql}) + self.assertEqual(sorted(c["created"][:4] for c in self.sent()), ["2020", "2026"]) + self.assertEqual(out["result"]["history"]["changes"], 2) + self.assertEqual(len(self.changelog_queries()), 1) # X уже прочитана стартовой — второго search нет + + def test_backfill_failure_on_issue_leaves_it_to_since(self): # I-1: упавшая на задаче стартовая часть её не «съедает» + jql = "(%s) AND sprint in openSprints() AND statusCategory != Done" % BASE + self.fake.add("X", updated=CHANGED) + self.fake.add_history("X", "2020-01-01T00:00:00.000+0300", [_st("status", "a", "b")]) + self.fake.add_history("X", "2026-10-22T12:10:00.000+0300", [_st("status", "b", "c")]) + self.fake.jql_results[jql] = ["X"] + self.fake.changelog_cap = 1 # search обрезает — дочитывание GET /issue отдаёт обе записи + self.fake.issue_changelog_cap = 2 + orig, failed = self.fake.route, [] + + def route(method, path, q, body): + if path == "/rest/api/2/issue/X" and q.get("expand") == "changelog" and not failed: + failed.append(1) + return 500, {"errorMessages": ["сбой"]} + return orig(method, path, q, body) + self.fake.route = route + out = self.refresh({"since": SINCE, "jql": jql}) + self.assertTrue(out["result"]["history"]["backfill_error"].startswith("HTTP 500")) + self.assertEqual([c["created"][:4] for c in self.sent()], ["2026"]) + def test_neighbor_changed_included(self): # Review Focus 5 self.fake.add("X") self.fake.add("N", in_base=False, updated=CHANGED)