fix(agent): поиск дубля с пагинацией, ошибки настроек (R-11), HTTPException как обрыв, маркер комментария, тесты ревью

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
Andrey Epifancev 2026-10-12 00:35:06 +04:00
parent 6bcc6f611f
commit 2e6f47a514
4 changed files with 139 additions and 1 deletions

View File

@ -86,7 +86,7 @@ class FakeJira:
def issue_json(self, key):
f = self.issues[key]
fields = {"summary": f["summary"], "description": f["description"], "status": {"name": f["status"]},
"priority": {"name": f["priority"]}, "assignee": self._user(f["assignee"]), "reporter": self._user(f["reporter"]),
"priority": {"name": f["priority"], "id": int(f["priority"]) if str(f["priority"]).isdigit() else f["priority"]}, "assignee": self._user(f["assignee"]), "reporter": self._user(f["reporter"]),
"duedate": f["duedate"], "labels": list(f["labels"]), "issuetype": {"name": f["issuetype"]},
"created": f["created"], "updated": f["updated"], "issuelinks": self.links_of(key)}
fields.update({c: f.get(c) for c in CUSTOM})

View File

@ -89,6 +89,37 @@ class ClientsTest(unittest.TestCase):
self.jira.get("/x")
self.assertEqual(self.jira.calls, 1)
def test_incomplete_read_is_connection_error(self):
import http.client
from unittest import mock
with mock.patch("urllib.request.urlopen", side_effect=http.client.IncompleteRead(b"ab", 10)):
with self.assertRaises(ConnectionError):
self.jira.get("/x")
def test_incomplete_read_inside_response_body(self):
import socket
import threading
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(1)
def serve():
c, _ = srv.accept()
c.recv(4096)
c.sendall(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 100\r\n\r\n{\"a\":")
c.close()
t = threading.Thread(target=serve, daemon=True)
t.start()
cfg = jira_cfg(self.fake)
cfg["jira"]["base_url"] = "http://127.0.0.1:%d" % srv.getsockname()[1]
try:
with self.assertRaises(ConnectionError):
agent.JiraClient(cfg).get("/x")
finally:
t.join(2)
srv.close()
def test_bearer_header(self):
cfg = jira_cfg(self.fake)
cfg["jira"]["auth"] = {"type": "bearer", "token": "tok123"}

View File

@ -104,6 +104,99 @@ class ExecutorFieldsTest(unittest.TestCase):
def test_unknown_type(self):
self.assertEqual(self.run_cmd("ensure_magic", key="x")["status"], "failed")
# --- ensure_issue: поиск дубля -----------------------------------------------
def test_ensure_issue_finds_on_later_page(self):
self.fake.add("AURORA-500", reporter="me", summary="Нужная задача")
for i in range(250):
self.fake.add("AURORA-%d" % (600 + i), reporter="me", summary="Другая %d" % i)
out = self.run_cmd("ensure_issue", temp_key="n:1", issue_type="task", summary="Нужная задача")
self.assertEqual(out["result"], {"key": "AURORA-500", "existed": True})
searches = [q for q in self.fake.queries if "jql" in q]
self.assertEqual(len(searches), 2) # вторая страница
self.assertIn("reporter = currentUser()", searches[0]["jql"])
self.assertIn("created >= -7d", searches[0]["jql"])
self.assertIn("ORDER BY created DESC", searches[0]["jql"])
self.assertEqual(searches[0]["fields"], "summary")
self.assertEqual(self.fake.writes(), [])
def test_ensure_issue_substring_is_not_duplicate_and_strip(self):
self.fake.add("AURORA-500", reporter="me", summary="Нужная задача (старая)")
self.fake.add("AURORA-501", reporter="me", summary=" Точная тема \n")
out = self.run_cmd("ensure_issue", temp_key="n:1", issue_type="task", summary="Нужная задача")
self.assertTrue(out["result"]["created"])
out = self.run_cmd("ensure_issue", temp_key="n:2", issue_type="task", summary="Точная тема")
self.assertEqual(out["result"], {"key": "AURORA-501", "existed": True})
def test_http_error_becomes_failed(self):
self.fake.fail_next.append((400, {}))
out = self.run_cmd("ensure_issue", temp_key="n:1", issue_type="task", summary="x")
self.assertEqual(out["status"], "failed")
self.assertIn("HTTP 400", out["error"])
self.fake.add("AURORA-1")
self.fake.fail_next.append((400, {}))
out = self.run_cmd("ensure_comment", key="AURORA-1", marker="М", body="текст")
self.assertEqual(out["status"], "failed")
# --- настройки (R-11) --------------------------------------------------------
def with_settings(self, **over):
s = dict(SETTINGS)
s.update(over)
self.ex = agent.Executor(self.jira, s)
self.ex.sleep = no_sleep
def test_epic_key_without_field_id_fails(self):
self.fake.add("AURORA-1")
self.with_settings(epic_link={"mode": "field"})
for ctype, params in (("ensure_issue", dict(temp_key="n", issue_type="task", summary="Э", epic_key="AURORA-9")),
("ensure_fields", dict(key="AURORA-1", epic_key="AURORA-9"))):
out = self.run_cmd(ctype, **params)
self.assertEqual(out["status"], "failed")
self.assertIn("epic_link.field_id", out["error"])
self.assertEqual(self.fake.writes(), [])
def test_epic_key_with_link_mode_fails_same_in_both(self):
self.fake.add("AURORA-1")
self.with_settings(epic_link={"mode": "link", "field_id": "customfield_15302"})
a = self.run_cmd("ensure_issue", temp_key="n", issue_type="task", summary="Э2", epic_key="AURORA-9")
b = self.run_cmd("ensure_fields", key="AURORA-1", epic_key="AURORA-9")
self.assertEqual((a["status"], b["status"]), ("failed", "failed"))
self.assertIn("epic_link.mode", a["error"])
self.assertIn("epic_link.mode", b["error"])
def test_planned_without_field_id_fails(self):
self.fake.add("AURORA-1")
ids = dict(SETTINGS["field_ids"])
del ids["planned_start"]
self.with_settings(field_ids=ids)
a = self.run_cmd("ensure_issue", temp_key="n", issue_type="task", summary="П", planned_start="2026-10-20")
b = self.run_cmd("ensure_fields", key="AURORA-1", planned_start="2026-10-20")
for out in (a, b):
self.assertEqual(out["status"], "failed")
self.assertIn("field_ids.planned_start", out["error"])
self.assertEqual(self.fake.writes(), [])
# --- комментарии -------------------------------------------------------------
def test_comment_marker_only_at_start(self):
self.fake.add("AURORA-1")
self.fake.comments["AURORA-1"] = [{"id": "1", "body": "Обсуждали, см. Актуализация 10.10.2026 в письме"}]
out = self.run_cmd("ensure_comment", key="AURORA-1", marker="Актуализация 10.10.2026", body="новое")
self.assertTrue(out["result"]["created"]) # маркер в середине чужого комментария — не наш
again = self.run_cmd("ensure_comment", key="AURORA-1", marker="Актуализация 10.10.2026", body="новое")
self.assertEqual(again["result"]["changed"], False)
def test_comment_found_on_later_page(self):
self.fake.add("AURORA-1")
self.fake.comments["AURORA-1"] = [{"id": str(i), "body": "чужой %d" % i} for i in range(1, 121)]
self.fake.comments["AURORA-1"].append({"id": "999", "body": "*Маркер.* старое"})
out = self.run_cmd("ensure_comment", key="AURORA-1", marker="Маркер", body="новое")
self.assertEqual(out["result"], {"comment_id": "999", "changed": True})
def test_priority_compare_by_id_as_string(self):
self.fake.add("AURORA-1", priority="3")
self.with_settings(priority_value_key="id", priority_map={})
out = self.run_cmd("ensure_fields", key="AURORA-1", priority="3")
self.assertEqual(out["result"], {"changed": []})
if __name__ == "__main__":
unittest.main()

View File

@ -117,6 +117,20 @@ class RefreshTest(unittest.TestCase):
out = self.ex.run({"id": 16, "type": "refresh", "params": {"keys": keys}})
self.assertEqual(out["result"]["missing"], ["GONE-%d" % i for i in range(1, 51)])
def test_missing_not_duplicated_when_also_a_neighbor(self):
self.fake.add("AURORA-1")
self.fake.links.append({"id": 1, "type": "ASUPR_Связанные", "source": "AURORA-1", "target": "GONE-1"})
out = self.ex.run({"id": 17, "type": "refresh", "params": {"keys": ["AURORA-1", "GONE-1"]}})
self.assertEqual(out["status"], "done")
self.assertEqual(out["result"]["missing"], ["GONE-1"])
def test_input_keys_deduplicated(self):
self.fake.add("AURORA-1")
out = self.ex.run({"id": 18, "type": "refresh", "params": {"keys": ["AURORA-1", "AURORA-1", "", "AURORA-1"]}})
self.assertEqual(out["result"], {"issues": 1, "neighbors": 0, "pages": 1})
self.assertEqual(len(self.hub.states[0]["issues"]), 1)
self.assertEqual(self.hub.states[0]["agent_version"], agent.AGENT_VERSION)
def test_deleted_neighbor(self):
self.fake.add("AURORA-1")
self.fake.add("AURORA-2", in_base=False)