From 2e6f47a51405536646b6b8206fce209bd1cee5c5 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 12 Oct 2026 00:35:06 +0400 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D0=B4=D1=83=D0=B1=D0=BB=D1=8F=20=D1=81=20=D0=BF=D0=B0=D0=B3?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D0=B5=D0=B9,=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=20(R-11),=20HTTPException=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D1=8B=D0=B2,=20=D0=BC=D0=B0=D1=80=D0=BA?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D1=8F,=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- tests/fake_jira.py | 2 +- tests/test_clients.py | 31 ++++++++++++ tests/test_executor_fields.py | 93 +++++++++++++++++++++++++++++++++++ tests/test_refresh.py | 14 ++++++ 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/tests/fake_jira.py b/tests/fake_jira.py index 05add91..bef6359 100644 --- a/tests/fake_jira.py +++ b/tests/fake_jira.py @@ -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}) diff --git a/tests/test_clients.py b/tests/test_clients.py index eb1ae8b..9467b80 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -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"} diff --git a/tests/test_executor_fields.py b/tests/test_executor_fields.py index 5dd26f5..8d29b78 100644 --- a/tests/test_executor_fields.py +++ b/tests/test_executor_fields.py @@ -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() diff --git a/tests/test_refresh.py b/tests/test_refresh.py index 2fe2075..b6c16b6 100644 --- a/tests/test_refresh.py +++ b/tests/test_refresh.py @@ -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)