Журнал: стартовая часть первой, задача в обеих выборках не теряет историю (#39, I-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-22 10:47:11 +04:00
parent 05e3f2c114
commit faed8b9c7a
2 changed files with 42 additions and 9 deletions

View File

@ -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]

View File

@ -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)