diff --git a/README.md b/README.md index 1098d0b..ad1826e 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ python3 agent.py prune | Тип | Что делает | |---|---| -| `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт; возвращает `key` | +| `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт с типом из `issue_types` хаба (`task`, `epic`, `bug`; нет настройки типа — `failed` «нет настройки issue_types.<тип>» без вызова Jira); возвращает `key` | | `ensure_fields` | читает задачу и пишет только отличающиеся поля | | `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт | | `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных | diff --git a/src/agent.py b/src/agent.py index df5e457..37ea8d8 100644 --- a/src/agent.py +++ b/src/agent.py @@ -572,8 +572,14 @@ class Executor: return {self.s.get("priority_value_key") or "name": mapped} def issue_type_ref(self, kind: str) -> dict: - it = (self.s.get("issue_types") or {}).get(kind) or {} - return {"id": str(it["id"])} if it.get("id") else {"name": it.get("name")} + """Тип задачи Jira из issue_types.: {id}, если id задан, иначе {name}; нет ни того, ни другого — + ошибка конфигурации, а не {"name": None}, на которое Jira ответит 400 без понятной причины (спека agent v11).""" + it = (self.s.get("issue_types") or {}).get(kind) + if isinstance(it, dict) and it.get("id"): + return {"id": str(it["id"])} + if isinstance(it, dict) and it.get("name"): + return {"name": it["name"]} + raise ConfigGap("нет настройки issue_types.%s" % kind) def epic_field_id(self) -> str: ep = self.s.get("epic_link") or {} @@ -593,6 +599,7 @@ class Executor: # --- ensure_issue ---------------------------------------------------------- def do_ensure_issue(self, p: dict, cmd: dict) -> dict: + issuetype = self.issue_type_ref(p["issue_type"]) # до поиска дубля: без настройки — ни одного вызова Jira summary = (p["summary"] or "").strip() jql = "project = %s AND reporter = currentUser() AND created >= -7d ORDER BY created DESC" % self.s["project_key"] start = 0 @@ -605,8 +612,7 @@ class Executor: start += len(issues) if not issues or start >= int(found.get("total") or 0): break - fields = {"project": {"key": self.s["project_key"]}, "issuetype": self.issue_type_ref(p["issue_type"]), - "summary": p["summary"]} + fields = {"project": {"key": self.s["project_key"]}, "issuetype": issuetype, "summary": p["summary"]} if p.get("description") is not None: fields[self.fid("description")] = p["description"] if p.get("priority"): diff --git a/tests/_common.py b/tests/_common.py index 41fc089..d5f4838 100644 --- a/tests/_common.py +++ b/tests/_common.py @@ -14,7 +14,8 @@ ZERO_PACING = {"ensure_issue": 0, "ensure_fields": 0, "ensure_comment": 0, "ensu SETTINGS = { "base_jql": "project = AURORA AND labels = AWPCA", "project_key": "AURORA", - "issue_types": {"task": {"name": "Задача", "id": "10100"}, "epic": {"name": "Фича", "id": "12300"}}, + "issue_types": {"task": {"name": "Задача", "id": "10100"}, "epic": {"name": "Фича", "id": "12300"}, + "bug": {"name": "Ошибка", "id": None}}, "epic_name_field_id": "customfield_15304", "field_ids": {"description": "description", "priority": "priority", "duedate": "duedate", "labels": "labels", "planned_start": "customfield_10801", "planned_end": "customfield_10802"}, diff --git a/tests/test_executor_fields.py b/tests/test_executor_fields.py index 7aa71f1..a904a3f 100644 --- a/tests/test_executor_fields.py +++ b/tests/test_executor_fields.py @@ -48,6 +48,34 @@ class ExecutorFieldsTest(unittest.TestCase): out = self.run_cmd("ensure_issue", temp_key="new:3", issue_type="task", summary="x", priority="Наивысший") self.assertEqual(self.fake.issues[out["result"]["key"]]["priority"], "Критичный") + def test_ensure_issue_bug_by_name(self): + self.assertEqual(self.ex.issue_type_ref("bug"), {"name": "Ошибка"}) # id нет — по имени + self.assertEqual(self.ex.issue_type_ref("task"), {"id": "10100"}) # id есть — по id + out = self.run_cmd("ensure_issue", temp_key="new:1", issue_type="bug", summary="ОСВ 60 не распознана", + labels=["AWPCA"]) + self.assertEqual(out["status"], "done") + self.assertTrue(out["result"]["created"]) + self.assertEqual(self.fake.issues[out["result"]["key"]]["issuetype"], "Ошибка") + + def test_ensure_issue_type_not_configured_no_jira_calls(self): + self.ex.s = dict(SETTINGS, issue_types={"task": {"name": "Задача", "id": "10100"}}) + out = self.run_cmd("ensure_issue", temp_key="new:1", issue_type="bug", summary="x") + self.assertEqual(out["status"], "failed") + self.assertEqual(out["error"], "нет настройки issue_types.bug") + self.assertNotIn("retryable", out) + self.assertEqual(self.fake.requests, []) # ни поиска дубля, ни создания + + def test_issue_type_entry_without_name_and_id(self): + for entry in ({}, {"name": None, "id": None}, {"name": "", "id": ""}, None, "Ошибка"): + self.ex.s = dict(SETTINGS, issue_types={"bug": entry}) + with self.assertRaises(agent.ConfigGap) as cm: + self.ex.issue_type_ref("bug") + self.assertEqual(str(cm.exception), "нет настройки issue_types.bug") + self.ex.s = dict(SETTINGS) + del self.ex.s["issue_types"] + with self.assertRaises(agent.ConfigGap): + self.ex.issue_type_ref("task") + def test_ensure_fields_writes_only_diff(self): self.fake.add("AURORA-1", duedate="2026-10-20", labels=["AWPCA", "fact-open"], priority="Средний") out = self.run_cmd("ensure_fields", key="AURORA-1", duedate="2026-10-23", priority="Средний",