feat(agent): тип задачи из issue_types с ошибкой конфигурации, тип bug (#31)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-16 01:44:30 +04:00
parent cd8213b71c
commit 90dc429434
4 changed files with 41 additions and 6 deletions

View File

@ -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` со списком доступных |

View File

@ -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.<kind>: {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"):

View File

@ -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"},

View File

@ -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="Средний",