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` — все комментарии задачи (автор — отображаемое имя, иначе логин; время
|
С v10 у каждой задачи состояния есть `comments` — все комментарии задачи (автор — отображаемое имя, иначе логин; время
|
||||||
и текст — как в Jira). Они приходят тем же `search`; если Jira отдала не все, агент дочитывает их через
|
и текст — как в Jira). Они приходят тем же `search`; если Jira отдала не все, агент дочитывает их через
|
||||||
`/issue/{key}/comment`.
|
`/issue/{key}/comment`.
|
||||||
|
С v11 комментарий без `id` пропускается (строка в логе с ключом задачи), а если Jira не дала дочитать комментарии
|
||||||
|
задачи (403 или 404), задача уходит без `comments` — хаб оставляет прежние; `refresh` продолжается.
|
||||||
5. **Сравнение текстов без различия переводов строк.** Описание и тело комментария сравниваются после замены `\r\n` на
|
5. **Сравнение текстов без различия переводов строк.** Описание и тело комментария сравниваются после замены `\r\n` на
|
||||||
`\n` (Jira хранит правленное в UI с `\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,
|
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 ""}
|
"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) — дочитать постранично
|
"""Комментарии задачи из ответа 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 {}
|
block = (raw.get("fields") or {}).get("comment") or {}
|
||||||
got = list(block.get("comments") or [])
|
got = list(block.get("comments") or [])
|
||||||
total = block.get("total")
|
total = block.get("total")
|
||||||
if total is not None and len(got) < int(total):
|
if total is not None and len(got) < int(total):
|
||||||
got, start = [], 0
|
got, start = [], 0
|
||||||
while True:
|
try:
|
||||||
page = self.jira.get(API + "/issue/%s/comment" % raw["key"], startAt=start, maxResults=100)
|
while True:
|
||||||
part = page.get("comments") or []
|
page = self.jira.get(API + "/issue/%s/comment" % key, startAt=start, maxResults=100)
|
||||||
got += part
|
part = page.get("comments") or []
|
||||||
start += len(part)
|
got += part
|
||||||
if not part or page.get("total") is None or start >= int(page["total"]):
|
start += len(part)
|
||||||
break
|
if not part or page.get("total") is None or start >= int(page["total"]):
|
||||||
return [self.comment_state(c) for c in got]
|
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]:
|
def sprint_names(self, keys: List[str]) -> Dict[str, str]:
|
||||||
# Источник спринта — Agile по активным и будущим спринтам доски (R-6): id поля спринта в настройках хаба нет,
|
# Источник спринта — Agile по активным и будущим спринтам доски (R-6): id поля спринта в настройках хаба нет,
|
||||||
@ -969,7 +983,9 @@ class Executor:
|
|||||||
for r in everything:
|
for r in everything:
|
||||||
st = self.to_state(r)
|
st = self.to_state(r)
|
||||||
st["sprint"] = names.get(r["key"])
|
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)
|
issues.append(st)
|
||||||
for l in self.links_from(r):
|
for l in self.links_from(r):
|
||||||
sig = (l["source_key"], l["target_key"], l["link_type"])
|
sig = (l["source_key"], l["target_key"], l["link_type"])
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class FakeJira:
|
|||||||
self.requests = [] # [(метод, путь)]
|
self.requests = [] # [(метод, путь)]
|
||||||
self.queries = [] # параметры запросов (q) — для проверки JQL
|
self.queries = [] # параметры запросов (q) — для проверки JQL
|
||||||
self.drop_next_post_issue = False # выполнить создание задачи и оборвать соединение без ответа
|
self.drop_next_post_issue = False # выполнить создание задачи и оборвать соединение без ответа
|
||||||
|
self.comment_denied = {} # ключ → код ответа на GET /issue/{key}/comment (403 — нет прав, 404 — нет задачи)
|
||||||
self.search_comment_cap = None # сколько комментариев задачи отдаёт search (None — все), как обрезка Jira
|
self.search_comment_cap = None # сколько комментариев задачи отдаёт search (None — все), как обрезка Jira
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
fake = self
|
fake = self
|
||||||
@ -234,6 +235,8 @@ class FakeJira:
|
|||||||
m = re.match(r"^/rest/api/2/issue/([^/]+)/comment$", p)
|
m = re.match(r"^/rest/api/2/issue/([^/]+)/comment$", p)
|
||||||
if m:
|
if m:
|
||||||
key = m.group(1)
|
key = m.group(1)
|
||||||
|
if method == "GET" and key in self.comment_denied:
|
||||||
|
return self.comment_denied[key], {"errorMessages": ["Нет прав на просмотр комментариев"]}
|
||||||
self.issues[key]
|
self.issues[key]
|
||||||
lst = self.comments.setdefault(key, [])
|
lst = self.comments.setdefault(key, [])
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
|
|||||||
@ -126,6 +126,56 @@ class RefreshTest(unittest.TestCase):
|
|||||||
[{"id": "5", "author": None, "created": None, "updated": None, "body": "x"}])
|
[{"id": "5", "author": None, "created": None, "updated": None, "body": "x"}])
|
||||||
self.assertEqual(self.fake.requests, []) # без дочитывания
|
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):
|
def test_refresh_empty_jql_result(self):
|
||||||
out = self.ex.run({"id": 8, "type": "refresh", "params": {"jql": "project = AURORA AND labels = AWPCA"}})
|
out = self.ex.run({"id": 8, "type": "refresh", "params": {"jql": "project = AURORA AND labels = AWPCA"}})
|
||||||
self.assertEqual(out["status"], "done")
|
self.assertEqual(out["status"], "done")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user