Агент: оценка story_points в ensure_fields (#49)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 18:57:12 +04:00
parent 514e98e819
commit 255784c858
2 changed files with 55 additions and 0 deletions

View File

@ -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})

View File

@ -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()