Доработки по финальному ревью v15: kanban без колонок, имя закрытого спринта, строгий фильтр (#49)
M-1: при ошибке чтения настроек kanban-доски kanban_backlog = null и запрос бэклога не делается. M-2: ensure_sprint_meta с rename_to, имя которого занято только закрытым спринтом, — failed «имя занято». M-3: sprint_state принимает только целый id > 0 и непустое строковое name, нестроковые даты и goal дают null (как check_boards хаба). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9d3958e337
commit
d71c2f5180
21
src/agent.py
21
src/agent.py
@ -1117,6 +1117,8 @@ class Executor:
|
||||
return self._out("failed", error=self.many_sprints(rename_to, to))
|
||||
if found and to:
|
||||
return self._out("failed", error="имя занято")
|
||||
if not to and any(str(sp.get("state")).lower() == "closed" for sp in self.sprints_named(board, rename_to)):
|
||||
return self._out("failed", error="имя занято") # M-2: имя только у закрытого спринта
|
||||
cur = found[0] if found else (to[0] if to else None)
|
||||
if cur is not None and str(cur.get("state")).lower() == "closed":
|
||||
return self._out("failed", error="спринт закрыт")
|
||||
@ -1630,18 +1632,25 @@ class Executor:
|
||||
|
||||
@staticmethod
|
||||
def sprint_state(sp: dict) -> Optional[dict]:
|
||||
"""Спринт для хаба; без id/name или с состоянием вне active/future/closed — None (R-72)."""
|
||||
"""Спринт для хаба; id не целое > 0, name не непустая строка или состояние вне active/future/closed — None
|
||||
(R-72, M-3: как check_boards хаба); нестроковые даты и goal — None."""
|
||||
state = sp.get("state")
|
||||
state = state.lower() if isinstance(state, str) else None
|
||||
if sp.get("id") is None or not sp.get("name") or state not in ("active", "future", "closed"):
|
||||
sid, name = sp.get("id"), sp.get("name")
|
||||
if (not isinstance(sid, int) or isinstance(sid, bool) or sid <= 0 or not isinstance(name, str) or not name
|
||||
or state not in ("active", "future", "closed")):
|
||||
return None
|
||||
return {"id": sp["id"], "name": sp["name"], "state": state, "start_date": sp.get("startDate"),
|
||||
"end_date": sp.get("endDate"), "complete_date": sp.get("completeDate"), "goal": sp.get("goal")}
|
||||
|
||||
def text(v: Any) -> Optional[str]:
|
||||
return v if isinstance(v, str) else None
|
||||
return {"id": sid, "name": name, "state": state, "start_date": text(sp.get("startDate")),
|
||||
"end_date": text(sp.get("endDate")), "complete_date": text(sp.get("completeDate")),
|
||||
"goal": text(sp.get("goal"))}
|
||||
|
||||
def board_state(self, board: Any, kind: str) -> Optional[dict]:
|
||||
"""Элемент boards первой пачки; ошибка чтения спринтов — доска не отправляется (R-65), колонок — columns null."""
|
||||
columns = self.board_columns(board)
|
||||
backlog = self.kanban_backlog(board) if kind == "kanban" else None
|
||||
backlog = self.kanban_backlog(board) if kind == "kanban" and columns is not None else None # M-1
|
||||
sprints: List[dict] = []
|
||||
if kind == "scrum":
|
||||
try:
|
||||
@ -1652,7 +1661,7 @@ class Executor:
|
||||
for sp in raw:
|
||||
st = self.sprint_state(sp)
|
||||
if st is None:
|
||||
log("refresh: доска %s — спринт %r пропущен: нет id/name или состояние не active/future/closed"
|
||||
log("refresh: доска %s — спринт %r пропущен: некорректные id/name или состояние не active/future/closed"
|
||||
% (board, sp.get("id")))
|
||||
else:
|
||||
sprints.append(st)
|
||||
|
||||
@ -202,6 +202,21 @@ class ExecutorSprintsTest(unittest.TestCase):
|
||||
out = self.run_cmd("ensure_sprint_state", sprint="AWPCA_Sprint_3_Corrections", state="closed", move_open_to="backlog", **B)
|
||||
self.assertEqual(out["result"], {"sprint_id": 3, "moved": ["AURORA-3001"]})
|
||||
|
||||
def test_meta_rename_to_closed_name_taken(self): # M-2
|
||||
self.fake.board_sprints[4216] = [{"id": 1, "name": "T", "state": "closed"}]
|
||||
out = self.run_cmd("ensure_sprint_meta", sprint="S", rename_to="T", **B)
|
||||
self.assertEqual((out["status"], out["error"]), ("failed", "имя занято"))
|
||||
self.fake.board_sprints[4216].append({"id": 9, "name": "S", "state": "future"})
|
||||
out = self.run_cmd("ensure_sprint_meta", sprint="S", rename_to="T", **B)
|
||||
self.assertEqual((out["status"], out["error"]), ("failed", "имя занято"))
|
||||
self.assertEqual(self.fake.writes(), [])
|
||||
|
||||
def test_meta_repeat_after_rename_with_closed_namesake(self): # M-2 не ломает R-69
|
||||
self.fake.board_sprints[4216] = [{"id": 1, "name": "T", "state": "closed"}, {"id": 9, "name": "T", "state": "future"}]
|
||||
out = self.run_cmd("ensure_sprint_meta", sprint="S", rename_to="T", **B)
|
||||
self.assertEqual(out["result"], {"sprint_id": 9, "created": False})
|
||||
self.assertEqual(self.fake.writes(), [])
|
||||
|
||||
# --- ensure_rank ---
|
||||
def test_rank_already(self): # сценарий «Ранг уже такой»
|
||||
self.fake.add("AURORA-1612", customfield_10105="0|i0001:")
|
||||
|
||||
@ -564,6 +564,14 @@ class RefreshTest(unittest.TestCase):
|
||||
self.assertEqual((b["kanban_backlog"], b["sprints"], b["columns"]), (want, [], COLS))
|
||||
self.assertNotIn("/rest/agile/1.0/board/5120/sprint", [r[1] for r in self.fake.requests])
|
||||
|
||||
def test_kanban_config_error_backlog_null(self): # M-1: columns null → kanban_backlog null, запроса бэклога нет
|
||||
self.fake.boards[5120] = "kanban"
|
||||
self.fake.fail_paths["/rest/agile/1.0/board/5120/configuration"] = [403]
|
||||
self.refresh(keys=[], boards=[{"id": 5120, "type": "kanban"}])
|
||||
b = self.hub.states[-1]["boards"][0]
|
||||
self.assertEqual((b["columns"], b["kanban_backlog"]), (None, None))
|
||||
self.assertNotIn("/rest/agile/1.0/board/5120/backlog", [r[1] for r in self.fake.requests])
|
||||
|
||||
def test_board_config_error_columns_null(self):
|
||||
self.fake.add("AURORA-1")
|
||||
self.fake.fail_paths["/rest/agile/1.0/board/4216/configuration"] = [403]
|
||||
@ -608,6 +616,19 @@ class RefreshTest(unittest.TestCase):
|
||||
self.assertEqual(self.hub.states[0]["boards"][0]["columns"], [{"name": "Одна", "statuses": ["Разработка", "Принято"]}])
|
||||
self.assertEqual([l for l in logs if "не найден" in l], [])
|
||||
|
||||
def test_board_bad_types_dropped(self): # M-3: id/name/даты/goal строже, как check_boards хаба
|
||||
bad = [{"id": "5", "name": "A", "state": "active"}, {"id": True, "name": "B", "state": "active"},
|
||||
{"id": 0, "name": "C", "state": "active"}, {"id": 6, "name": 7, "state": "active"},
|
||||
{"id": 8, "name": "Ok", "state": "future", "startDate": 123, "endDate": "2026-10-28T09:00:00.000+0300",
|
||||
"completeDate": [], "goal": 5}]
|
||||
self.fake.board_sprints[4216] = bad
|
||||
out, logs = self.logged(lambda: self.refresh(keys=[], boards=[{"id": 4216, "type": "scrum"}]))
|
||||
self.assertEqual(out["status"], "done", out)
|
||||
self.assertEqual(self.hub.states[0]["boards"][0]["sprints"],
|
||||
[{"id": 8, "name": "Ok", "state": "future", "start_date": None,
|
||||
"end_date": "2026-10-28T09:00:00.000+0300", "complete_date": None, "goal": None}])
|
||||
self.assertEqual(len([l for l in logs if "пропущен" in l]), 4, logs)
|
||||
|
||||
def test_column_without_string_name_skipped(self): # M-3
|
||||
self.fake.board_columns[4216] = [(None, ["10003"]), ("Ок", ["10003"])]
|
||||
out, logs = self.logged(lambda: self.refresh(keys=[], boards=[{"id": 4216, "type": "scrum"}]))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user