fix(agent): refresh не падает на комментариях без id и на 403/404 при дочитывании (M-1, M-2 #30, #31)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
da5d0b1d6d
commit
cd8213b71c
@ -238,6 +238,8 @@ python3 agent.py prune
|
||||
С v10 у каждой задачи состояния есть `comments` — все комментарии задачи (автор — отображаемое имя, иначе логин; время
|
||||
и текст — как в Jira). Они приходят тем же `search`; если Jira отдала не все, агент дочитывает их через
|
||||
`/issue/{key}/comment`.
|
||||
С v11 комментарий без `id` пропускается (строка в логе с ключом задачи), а если Jira не дала дочитать комментарии
|
||||
задачи (403 или 404), задача уходит без `comments` — хаб оставляет прежние; `refresh` продолжается.
|
||||
5. **Сравнение текстов без различия переводов строк.** Описание и тело комментария сравниваются после замены `\r\n` на
|
||||
`\n` (Jira хранит правленное в UI с `\r\n`); отправляется текст команды как есть. Прочие различия (пробелы,
|
||||
завершающий перевод строки) — изменение.
|
||||
|
||||
38
src/agent.py
38
src/agent.py
@ -900,22 +900,36 @@ class Executor:
|
||||
return {"id": str(c.get("id")), "author": a.get("displayName") or a.get("name") or None,
|
||||
"created": c.get("created"), "updated": c.get("updated"), "body": c.get("body") or ""}
|
||||
|
||||
def comments_of(self, raw: dict) -> List[dict]:
|
||||
def comments_of(self, raw: dict) -> Optional[List[dict]]:
|
||||
"""Комментарии задачи из ответа search. Если Jira отдала не все (len < total) — дочитать постранично
|
||||
через /issue/{key}/comment и взять полный список вместо частичного."""
|
||||
через /issue/{key}/comment и взять полный список вместо частичного. Комментарий без id отбрасывается (M-1 #30);
|
||||
403/404 при дочитывании — None: задача уходит без comments, хаб оставит прежние (M-2 #30)."""
|
||||
key = raw["key"]
|
||||
block = (raw.get("fields") or {}).get("comment") or {}
|
||||
got = list(block.get("comments") or [])
|
||||
total = block.get("total")
|
||||
if total is not None and len(got) < int(total):
|
||||
got, start = [], 0
|
||||
while True:
|
||||
page = self.jira.get(API + "/issue/%s/comment" % raw["key"], startAt=start, maxResults=100)
|
||||
part = page.get("comments") or []
|
||||
got += part
|
||||
start += len(part)
|
||||
if not part or page.get("total") is None or start >= int(page["total"]):
|
||||
break
|
||||
return [self.comment_state(c) for c in got]
|
||||
try:
|
||||
while True:
|
||||
page = self.jira.get(API + "/issue/%s/comment" % key, startAt=start, maxResults=100)
|
||||
part = page.get("comments") or []
|
||||
got += part
|
||||
start += len(part)
|
||||
if not part or page.get("total") is None or start >= int(page["total"]):
|
||||
break
|
||||
except HttpError as e:
|
||||
if e.code not in (403, 404):
|
||||
raise
|
||||
log("refresh: %s — комментарии не дочитаны (HTTP %d), задача уходит без comments" % (key, e.code))
|
||||
return None
|
||||
out = []
|
||||
for c in got:
|
||||
if c.get("id") is None or str(c.get("id")) == "":
|
||||
log("refresh: %s — комментарий без id пропущен" % key)
|
||||
continue
|
||||
out.append(self.comment_state(c))
|
||||
return out
|
||||
|
||||
def sprint_names(self, keys: List[str]) -> Dict[str, str]:
|
||||
# Источник спринта — Agile по активным и будущим спринтам доски (R-6): id поля спринта в настройках хаба нет,
|
||||
@ -969,7 +983,9 @@ class Executor:
|
||||
for r in everything:
|
||||
st = self.to_state(r)
|
||||
st["sprint"] = names.get(r["key"])
|
||||
st["comments"] = self.comments_of(r)
|
||||
comments = self.comments_of(r)
|
||||
if comments is not None: # None — нет доступа к комментариям: поле не передаём (M-2 #30)
|
||||
st["comments"] = comments
|
||||
issues.append(st)
|
||||
for l in self.links_from(r):
|
||||
sig = (l["source_key"], l["target_key"], l["link_type"])
|
||||
|
||||
@ -38,6 +38,7 @@ class FakeJira:
|
||||
self.requests = [] # [(метод, путь)]
|
||||
self.queries = [] # параметры запросов (q) — для проверки JQL
|
||||
self.drop_next_post_issue = False # выполнить создание задачи и оборвать соединение без ответа
|
||||
self.comment_denied = {} # ключ → код ответа на GET /issue/{key}/comment (403 — нет прав, 404 — нет задачи)
|
||||
self.search_comment_cap = None # сколько комментариев задачи отдаёт search (None — все), как обрезка Jira
|
||||
self.lock = threading.Lock()
|
||||
fake = self
|
||||
@ -234,6 +235,8 @@ class FakeJira:
|
||||
m = re.match(r"^/rest/api/2/issue/([^/]+)/comment$", p)
|
||||
if m:
|
||||
key = m.group(1)
|
||||
if method == "GET" and key in self.comment_denied:
|
||||
return self.comment_denied[key], {"errorMessages": ["Нет прав на просмотр комментариев"]}
|
||||
self.issues[key]
|
||||
lst = self.comments.setdefault(key, [])
|
||||
if method == "GET":
|
||||
|
||||
@ -126,6 +126,56 @@ class RefreshTest(unittest.TestCase):
|
||||
[{"id": "5", "author": None, "created": None, "updated": None, "body": "x"}])
|
||||
self.assertEqual(self.fake.requests, []) # без дочитывания
|
||||
|
||||
def test_comment_without_id_dropped(self):
|
||||
self.fake.add("AURORA-1")
|
||||
self.fake.add_comment("AURORA-1", "нормальный")
|
||||
self.fake.add_comment("AURORA-1", "второй")["id"] = None
|
||||
self.fake.add_comment("AURORA-1", "третий")["id"] = ""
|
||||
logs = []
|
||||
orig = agent.log
|
||||
agent.log = logs.append
|
||||
try:
|
||||
out = self.ex.run({"id": 10, "type": "refresh", "params": {"keys": ["AURORA-1"]}})
|
||||
finally:
|
||||
agent.log = orig
|
||||
self.assertEqual(out["status"], "done")
|
||||
c = self.hub.states[0]["issues"][0]["comments"]
|
||||
self.assertEqual([x["body"] for x in c], ["нормальный"])
|
||||
self.assertEqual(len([l for l in logs if "AURORA-1" in l and "комментарий без id" in l]), 2, logs)
|
||||
|
||||
def test_comments_read_denied_keeps_refresh(self):
|
||||
for key in ("AURORA-1", "AURORA-2", "AURORA-3"):
|
||||
self.fake.add(key)
|
||||
for n in range(3):
|
||||
self.fake.add_comment(key, "%s-%d" % (key, n))
|
||||
self.fake.search_comment_cap = 1 # search отдаёт 1 из 3 — нужно дочитывание
|
||||
self.fake.comment_denied = {"AURORA-1": 403, "AURORA-2": 404}
|
||||
logs = []
|
||||
orig = agent.log
|
||||
agent.log = logs.append
|
||||
try:
|
||||
out = self.ex.run({"id": 11, "type": "refresh", "params": {"keys": ["AURORA-1", "AURORA-2", "AURORA-3"]}})
|
||||
finally:
|
||||
agent.log = orig
|
||||
self.assertEqual(out["status"], "done")
|
||||
by_key = {i["key"]: i for i in self.hub.states[0]["issues"]}
|
||||
self.assertNotIn("comments", by_key["AURORA-1"])
|
||||
self.assertNotIn("comments", by_key["AURORA-2"])
|
||||
self.assertEqual([x["body"] for x in by_key["AURORA-3"]["comments"]], ["AURORA-3-0", "AURORA-3-1", "AURORA-3-2"])
|
||||
self.assertTrue(any("AURORA-1" in l and "HTTP 403" in l for l in logs), logs)
|
||||
self.assertTrue(any("AURORA-2" in l and "HTTP 404" in l for l in logs), logs)
|
||||
|
||||
def test_comments_read_5xx_still_fails_refresh(self):
|
||||
self.fake.add("AURORA-1")
|
||||
for n in range(3):
|
||||
self.fake.add_comment("AURORA-1", "к%d" % n)
|
||||
self.fake.search_comment_cap = 1
|
||||
self.fake.comment_denied = {"AURORA-1": 500}
|
||||
out = self.ex.run({"id": 12, "type": "refresh", "params": {"keys": ["AURORA-1"]}})
|
||||
self.assertEqual(out["status"], "failed")
|
||||
self.assertTrue(out["retryable"]) # 5xx — по-прежнему повтор через проверку (R-10)
|
||||
self.assertEqual(self.hub.states, [])
|
||||
|
||||
def test_refresh_empty_jql_result(self):
|
||||
out = self.ex.run({"id": 8, "type": "refresh", "params": {"jql": "project = AURORA AND labels = AWPCA"}})
|
||||
self.assertEqual(out["status"], "done")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user