From 255784c858a38b8ea05ea1900b430d699120f518 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 18:57:12 +0400 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B3=D0=B5=D0=BD=D1=82:=20=D0=BE=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=B0=20story=5Fpoints=20=D0=B2=20ensure=5Ff?= =?UTF-8?q?ields=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/agent.py | 21 +++++++++++++++++++++ tests/test_executor_fields.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/agent.py b/src/agent.py index 0c2ab5e..b9a4089 100644 --- a/src/agent.py +++ b/src/agent.py @@ -641,6 +641,15 @@ def _nl(value) -> str: return (value or "").replace("\r\n", "\n") +def _same_points(a, b) -> bool: + """Равны ли оценки story_points: None равно только None; числа — с допуском 1e-9 (3 == 3.0); bool — не число.""" + if a is None or b is None: + return a is None and b is None + if isinstance(a, bool) or isinstance(b, bool) or not isinstance(a, (int, float)) or not isinstance(b, (int, float)): + return False + return abs(a - b) < 1e-9 + + class ConfigGap(Exception): """В настройках хаба нет того, что нужно команде (R-11): команда падает с понятным текстом, а не молча теряет поле.""" @@ -846,6 +855,15 @@ class Executor: # --- ensure_fields --------------------------------------------------------- def do_ensure_fields(self, p: dict, cmd: dict) -> dict: key = p["key"] + sp_fid = None + if "story_points" in p: + # Настроенный id берётся без GET /field (как в refresh он не нужен); иначе — поиск в jira_fields. + sp_fid = (self.s.get("field_ids") or {}).get("story_points") or self.jira_fields()["story_points"] + if not sp_fid: + raise ConfigGap("поле оценки не найдено в Jira: задайте field_ids.story_points") + want_sp = p["story_points"] + if want_sp is not None and (isinstance(want_sp, bool) or not isinstance(want_sp, (int, float)) or not want_sp >= 0): + return self._out("failed", error="story_points — число ≥ 0 или null") cur = self.jira.get(API + "/issue/%s" % key)["fields"] fields, changed = {}, [] if "summary" in p and (cur.get("summary") or "") != (p["summary"] or ""): @@ -884,6 +902,9 @@ class Executor: if new != labels: fields[self.fid("labels")] = new changed.append("labels") + if sp_fid and not _same_points(cur.get(sp_fid), p["story_points"]): + fields[sp_fid] = p["story_points"] + changed.append("story_points") if not fields: return self._out("done", {"changed": []}) self.jira.put(API + "/issue/%s" % key, {"fields": fields}) diff --git a/tests/test_executor_fields.py b/tests/test_executor_fields.py index a904a3f..7b4846a 100644 --- a/tests/test_executor_fields.py +++ b/tests/test_executor_fields.py @@ -259,6 +259,40 @@ class ExecutorFieldsTest(unittest.TestCase): out = self.run_cmd("ensure_fields", key="AURORA-1", priority="3") self.assertEqual(out["result"], {"changed": []}) + # --- story_points (Task 4, #49) ---------------------------------------------- + def test_story_points_set_clear_idempotent(self): + self.fake.add("AURORA-1") + out = self.run_cmd("ensure_fields", key="AURORA-1", story_points=3) + self.assertEqual(out["result"], {"changed": ["story_points"]}) + self.assertEqual(self.fake.issues["AURORA-1"]["customfield_10106"], 3) + for v in (3, 3.0): + again = self.run_cmd("ensure_fields", key="AURORA-1", story_points=v) + self.assertEqual(again["result"], {"changed": []}) + self.assertEqual(len(self.fake.writes()), 1) + self.run_cmd("ensure_fields", key="AURORA-1", story_points=None) + self.assertIsNone(self.fake.issues["AURORA-1"]["customfield_10106"]) + + def test_story_points_no_field(self): # сценарий «Нет поля оценки» + self.fake.add("AURORA-1") + self.fake.fields = [] + out = self.run_cmd("ensure_fields", key="AURORA-1", story_points=3) + self.assertEqual((out["status"], out["error"]), ("failed", "поле оценки не найдено в Jira: задайте field_ids.story_points")) + self.assertEqual(self.fake.writes(), []) + + def test_story_points_override(self): + self.fake.add("AURORA-1") + self.ex.s["field_ids"] = dict(SETTINGS["field_ids"], story_points="customfield_20000") + self.run_cmd("ensure_fields", key="AURORA-1", story_points=5) + self.assertEqual(self.fake.issues["AURORA-1"]["customfield_20000"], 5) + self.assertNotIn(("GET", "/rest/api/2/field"), self.fake.requests) + + def test_story_points_bad_value(self): + self.fake.add("AURORA-1") + for v in ("3", -1, True): + out = self.run_cmd("ensure_fields", key="AURORA-1", story_points=v) + self.assertEqual((out["status"], out["error"]), ("failed", "story_points — число ≥ 0 или null"), v) + self.assertEqual(self.fake.writes(), []) + if __name__ == "__main__": unittest.main()