Хаб: доска проекта в ensure_sprint, boards в выдаче refresh, типы досок из отчёта агента (#61)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6d5c40b037
commit
e2e63dab23
@ -264,3 +264,9 @@
|
||||
26. 2026-10-19 — **`project` в `retry` / `POST /batches` не строка — 422 `bad_retry` / `bad_batch`; в `retry_command`
|
||||
сначала сверка `project` с проектом исходного, затем `create_batch` с проверками проекта.** Обоснование: «команда
|
||||
#N не из проекта P» точнее ошибки проверок. Цена ошибки: текст 422.
|
||||
27. 2026-10-19 — **типы досок из результата `refresh` пишутся только при `status = done`; результат без ключа `boards`
|
||||
или с мусорными значениями типы не трогает.** Обоснование: у проваленной команды результат неполон, а агент v11 ключа
|
||||
не присылает. Цена ошибки: одно условие в `report_result`.
|
||||
28. 2026-10-19 — **метки временного ключа для `ensure_sprint` берутся только из его `ensure_issue` в пакете, без
|
||||
обращения к зеркалу.** Обоснование: `new:<n>` в зеркале — не задача Jira; метки пакета (с дописанной меткой проекта)
|
||||
единственный надёжный источник. Цена ошибки: ветка в `_assign_boards`.
|
||||
|
||||
@ -180,7 +180,7 @@ def h_agent_settings(hub, conn, m, q, body):
|
||||
def h_agent_command(hub, conn, m, q, body):
|
||||
"""Команда по id для восстановления из журнала агента: параметры — как при выдаче (с size у ensure_attachment)."""
|
||||
cmd = _not_found(queue.get_command(conn, _int(m)), "command")
|
||||
return 200, {**cmd, "params": queue.deliver_params(cmd, hub.files_dir)}
|
||||
return 200, {**cmd, "params": queue.deliver_params(conn, cmd, hub.files_dir)}
|
||||
|
||||
|
||||
def h_agent_result(hub, conn, m, q, body):
|
||||
|
||||
@ -67,6 +67,32 @@ def _check_setup(conn, cmds, files_dir) -> None:
|
||||
if '"' in p["filename"] or any(ord(ch) < 32 or ord(ch) == 127 for ch in p["filename"]):
|
||||
# кавычка, CR, LF ломают заголовок части multipart/form-data у агента (R-2)
|
||||
raise C.CommandError(f"команда {i}: filename без кавычек и управляющих символов (CR, LF и др.)")
|
||||
_assign_boards(conn, cmds)
|
||||
|
||||
|
||||
def _assign_boards(conn, cmds) -> None:
|
||||
"""Доска проекта задачи в params ensure_sprint (design #7): метки временного ключа — из его ensure_issue в пакете,
|
||||
иначе из зеркала; 422 — задача вне проектов, разные доски, нет доски, канбан, тип ещё не известен."""
|
||||
temp_labels = {c["params"]["temp_key"]: c["params"].get("labels") or [] for c in cmds if c["type"] == "ensure_issue"}
|
||||
found = projects.list_projects(conn)
|
||||
for i, c in enumerate(cmds, 1):
|
||||
if c["type"] != "ensure_sprint":
|
||||
continue
|
||||
key = c["params"]["key"]
|
||||
labels = temp_labels[key] if key in temp_labels else (mirror.labels_of(conn, key) or [])
|
||||
own = [p for p in found if p["label"] in labels]
|
||||
if not own:
|
||||
raise C.CommandError(f"команда {i}: {key} не входит ни в один проект — доску для спринта не определить")
|
||||
if len({p["board_id"] for p in own}) > 1:
|
||||
raise C.CommandError(f"команда {i}: {key} в проектах {', '.join(p['label'] for p in own)} с разными досками")
|
||||
p = own[0]
|
||||
if p["board_id"] is None:
|
||||
raise C.CommandError(f"команда {i}: у проекта {p['label']} нет доски Jira")
|
||||
if p["board_type"] == "kanban":
|
||||
raise C.CommandError(f"команда {i}: у проекта {p['label']} канбан-доска, спринтов нет")
|
||||
if p["board_type"] is None:
|
||||
raise C.CommandError(f"команда {i}: тип доски проекта {p['label']} ещё не известен — дождитесь обновления из Jira")
|
||||
c["params"] = {**c["params"], "board_id": p["board_id"]}
|
||||
|
||||
|
||||
def _check_project(conn, cmds, project) -> list[dict]:
|
||||
@ -230,9 +256,12 @@ def _redeliver(conn, now) -> dict | None:
|
||||
log.warning("команда %s: агент не отчитался после %s выдач — failed", cmd["id"], MAX_DELIVERIES)
|
||||
|
||||
|
||||
def deliver_params(cmd, files_dir) -> dict:
|
||||
def deliver_params(conn, cmd, files_dir) -> dict:
|
||||
"""Параметры команды для агента: ensure_attachment получает size — текущий размер файла в хранилище или None,
|
||||
если файла уже нет (решение 5). В базе params остаются без size: повтор команды проходит проверку как новая."""
|
||||
если файла уже нет (решение 5); refresh — boards, доски проектов (design #7). В базе params остаются без них:
|
||||
повтор команды проходит проверку как новая."""
|
||||
if cmd["type"] == "refresh":
|
||||
return {**cmd["params"], "boards": projects.boards(conn)}
|
||||
if cmd["type"] != "ensure_attachment":
|
||||
return cmd["params"]
|
||||
size = files.size(files_dir, cmd["params"].get("file")) if files_dir is not None else None
|
||||
@ -281,7 +310,7 @@ def claim(conn, agent_version=None, settings_version=None, last_jira_call_at=Non
|
||||
backup(to_backup)
|
||||
except Exception: # сбой копии не должен держать очередь: команда уже взята
|
||||
log.warning("резервная копия %s не создана, команда %s выдаётся без неё", to_backup, cmd["id"], exc_info=True)
|
||||
return {"id": cmd["id"], "type": cmd["type"], "params": deliver_params(cmd, files_dir), "pause_seconds": pause,
|
||||
return {"id": cmd["id"], "type": cmd["type"], "params": deliver_params(conn, cmd, files_dir), "pause_seconds": pause,
|
||||
"batch_id": cmd["batch_id"]}
|
||||
|
||||
|
||||
@ -333,6 +362,8 @@ def report_result(conn, command_id, status, result=None, error=None, jira_calls=
|
||||
if cmd["type"] == "refresh":
|
||||
conn.execute("UPDATE sync_run SET finished_at = ?, jira_calls = ?, rate_limited = ? WHERE command_id = ?",
|
||||
(now, int(jira_calls or 0), int(rate_limited or 0), command_id))
|
||||
if status == "done" and isinstance(result, dict) and "boards" in result:
|
||||
projects.record_board_types(conn, result["boards"]) # без ключа (агент v11) типы не трогаем
|
||||
if status == "done" and is_full_refresh(conn, command_id):
|
||||
# полный прогон не обновил задачу — в базовую JQL она больше не входит: сосед. Пустой прогон (0 задач) —
|
||||
# почти всегда сбой выборки, а не «все задачи ушли»: соседей не трогаем (M-12)
|
||||
@ -400,7 +431,8 @@ def retry_command(conn, command_id, author, files_dir=None, project=None) -> dic
|
||||
origin = conn.execute("SELECT project FROM batch WHERE id = ?", (cmd["batch_id"],)).fetchone()["project"]
|
||||
if project is not None and project != origin:
|
||||
raise C.CommandError(f"команда #{command_id} не из проекта {project}")
|
||||
return create_batch(conn, f"Повтор команды {command_id}", author, [{"type": cmd["type"], "params": cmd["params"]}],
|
||||
params = {k: v for k, v in cmd["params"].items() if not (cmd["type"] == "ensure_sprint" and k == "board_id")}
|
||||
return create_batch(conn, f"Повтор команды {command_id}", author, [{"type": cmd["type"], "params": params}],
|
||||
files_dir=files_dir, project=origin)
|
||||
|
||||
|
||||
|
||||
@ -886,3 +886,25 @@ def test_project_batches_api(hub, add_project): # #61, T
|
||||
assert len(c.get("/batches", limit=50)["batches"]) == before
|
||||
r = c.post(f"/commands/{cid}/retry", {})
|
||||
assert c.get(f"/batches/{r['id']}")["project"] == "AWPCA"
|
||||
|
||||
|
||||
def test_agent_command_refresh_has_boards(hub, add_project): # #61, Task 6
|
||||
_add_project(hub, add_project)
|
||||
b = hub["client"].post("/batches", {"title": "r", "commands": [{"type": "refresh", "params": {"keys": ["A-1"]}}]})
|
||||
cid = hub["client"].get(f"/batches/{b['id']}")["commands"][0]["id"]
|
||||
got = hub["agent"].get(f"/agent/commands/{cid}")
|
||||
assert got["params"] == {"keys": ["A-1"], "boards": [{"id": 4216, "type": "scrum"}]}
|
||||
assert "boards" not in hub["client"].get(f"/batches/{b['id']}")["commands"][0]["params"]
|
||||
|
||||
|
||||
def test_board_type_round_trip(hub): # сценарий «Тип доски определён»
|
||||
c, a = hub["client"], hub["agent"]
|
||||
c.post("/projects", {"label": "STOCKS_DRRK", "name": "Остатки ДРРК", "board_id": 5120})
|
||||
cmd = a.post("/agent/claim", {"agent_version": 12, "settings_version": 1})["command"] # refresh от создания проекта
|
||||
assert cmd["type"] == "refresh" and {"id": 5120, "type": None} in cmd["params"]["boards"]
|
||||
a.post(f"/agent/commands/{cmd['id']}/result", {"status": "done", "result": {"boards": {"5120": "kanban"}}})
|
||||
assert [p["board_type"] for p in c.get("/projects")["projects"] if p["label"] == "STOCKS_DRRK"] == ["kanban"]
|
||||
a.post("/agent/state", {"issues": [{"key": "A-5", "summary": "С", "labels": ["STOCKS_DRRK"]}], "links": []})
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post("/batches", {"title": "s", "commands": [{"type": "ensure_sprint", "params": {"key": "A-5", "sprint": "С2"}}]})
|
||||
assert e.value.status == 422 and "канбан-доска" in e.value.detail
|
||||
|
||||
@ -7,11 +7,12 @@ from jira_hub.client import HubClient, HubError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def live(tmp_path):
|
||||
def live(tmp_path, add_project):
|
||||
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
|
||||
h.claim_wait = 0
|
||||
conn = h.conn()
|
||||
settings.set_(conn, "link_types", {"связанные": "ASUPR_Связанные"})
|
||||
add_project(conn)
|
||||
tok = settings.tokens(conn)
|
||||
conn.close()
|
||||
srv = api.make_server(h, "127.0.0.1", 0)
|
||||
@ -28,7 +29,8 @@ def test_create_link_sprint_flow(live):
|
||||
agent, client = live
|
||||
client.put("/issues/new:60/note", {"fact_status": "Ожидание разработки", "work_kind": "разработка", "updated_by": "import"})
|
||||
b = client.post("/batches", {"title": "новая задача в спринт", "author": "claude", "commands": [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая", "duedate": "2026-10-23"}},
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая", "duedate": "2026-10-23",
|
||||
"labels": ["AWPCA"]}},
|
||||
{"type": "ensure_link", "params": {"source": "new:60", "target": "A-1", "link_type": "связанные"}},
|
||||
{"type": "ensure_sprint", "params": {"key": "new:60", "sprint": "Спринт 2"}},
|
||||
]})
|
||||
@ -67,7 +69,7 @@ def test_create_link_sprint_flow(live):
|
||||
else:
|
||||
agent.post(f"/agent/commands/{cmd['id']}/result", {"status": "done", "jira_calls": 1})
|
||||
assert [t for t, _ in seen] == ["ensure_issue", "ensure_link", "ensure_sprint", "refresh"]
|
||||
assert seen[1][1]["source"] == "A-90" and seen[2][1]["key"] == "A-90" and seen[3][1]["keys"] == ["A-90", "A-1"]
|
||||
assert seen[1][1]["source"] == "A-90" and seen[2][1]["key"] == "A-90" and seen[2][1]["board_id"] == 4216 and seen[3][1]["keys"] == ["A-90", "A-1"]
|
||||
view = client.get(f"/batches/{bid}")
|
||||
assert view["status"] == "done" and view["remaining_seconds"] == 0
|
||||
assert all(c["status"] == "done" for c in view["commands"])
|
||||
|
||||
@ -96,12 +96,14 @@ def test_backup_failure_does_not_block_claim(conn, caplog):
|
||||
assert "не создана" in caplog.text
|
||||
|
||||
|
||||
def test_temp_key_substitution_and_skip(conn):
|
||||
def test_temp_key_substitution_and_skip(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.set_note(conn, "new:60", {"fact_status": "В работе"}, "import")
|
||||
queue.create_batch(conn, "создать", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая"}},
|
||||
{"type": "ensure_link", "params": {"source": "new:60", "target": "A-1", "link_type": "связанные"}},
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:61", "issue_type": "task", "summary": "Вторая"}},
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:61", "issue_type": "task", "summary": "Вторая",
|
||||
"labels": ["AWPCA"]}},
|
||||
{"type": "ensure_sprint", "params": {"key": "new:61", "sprint": "Спринт 2"}},
|
||||
])
|
||||
c1 = queue.claim(conn)
|
||||
@ -144,7 +146,9 @@ def test_report_after_force_cancel_keeps_cancelled(conn):
|
||||
assert queue.report_result(conn, c["id"], "failed", error="e")["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_reconcile_confirms_by_state(conn):
|
||||
def test_reconcile_confirms_by_state(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "labels": ["AWPCA"]}], [])
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}])
|
||||
c = queue.claim(conn)
|
||||
b = queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
@ -242,11 +246,13 @@ def test_reconcile_does_not_confirm_ensure_issue_without_reporter(conn):
|
||||
assert mirror.get_note(conn, "new:1") is not None and mirror.get_note(conn, "A-7") is None
|
||||
|
||||
|
||||
def test_reconcile_does_not_overwrite_finished_command(conn, monkeypatch):
|
||||
def test_reconcile_does_not_overwrite_finished_command(conn, monkeypatch, add_project):
|
||||
"""M-1: отчёт агента/отмена между check_confirmed и UPDATE не перезаписываются."""
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "labels": ["AWPCA"]}], [])
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}])
|
||||
c = queue.claim(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "sprint": "Спринт 2", "labels": []}], [])
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "sprint": "Спринт 2", "labels": ["AWPCA"]}], [])
|
||||
real = queue.C.check_confirmed
|
||||
|
||||
def racing(*a, **kw):
|
||||
@ -608,8 +614,9 @@ def test_attachment_params_rejected(conn, store, params, msg):
|
||||
assert str(e.value) == msg
|
||||
|
||||
|
||||
def test_bug_with_attachment_one_batch(conn, store):
|
||||
def test_bug_with_attachment_one_batch(conn, store, add_project):
|
||||
"""Спека commands «Ошибка с вложением одним пакетом»: вложение выдаётся после ensure_issue с ключом и size."""
|
||||
add_project(conn)
|
||||
settings.set_(conn, "issue_types", {"bug": {"name": "Ошибка", "id": None}})
|
||||
b = queue.create_batch(conn, "ошибка", "claude", [
|
||||
bug(), {"type": "ensure_sprint", "params": {"key": "new:1", "sprint": "Спринт 2"}}, attach(key="new:1")],
|
||||
@ -636,7 +643,7 @@ def test_attachment_size_null_when_file_gone(conn, store):
|
||||
c = queue.claim(conn, files_dir=store)
|
||||
assert c["type"] == "ensure_attachment" and c["params"]["size"] is None
|
||||
assert queue.claim(conn)["params"]["size"] is None # без хранилища — тоже null, не исключение
|
||||
assert queue.deliver_params(queue.get_command(conn, c["id"]), store)["size"] is None
|
||||
assert queue.deliver_params(conn, queue.get_command(conn, c["id"]), store)["size"] is None
|
||||
|
||||
|
||||
def test_attachment_retry_passes_validation(conn, store):
|
||||
@ -767,3 +774,164 @@ def test_retry_inherits_project_checks(conn, add_project): # Review
|
||||
with pytest.raises(CommandError, match="AURORA-1 не в проекте STOCKS_DRRK"):
|
||||
queue.retry_command(conn, cid, "claude")
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM batch").fetchone()["n"] == before
|
||||
|
||||
|
||||
# --- доски: ensure_sprint.board_id, refresh.boards, типы досок (#61, Task 6) -----------------------------------------
|
||||
|
||||
def sprint(key, name="Спринт 2"):
|
||||
return {"type": "ensure_sprint", "params": {"key": key, "sprint": name}}
|
||||
|
||||
|
||||
def test_sprint_gets_project_board(conn, add_project): # сценарий «Спринт задачи проекта»
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1538", "labels": ["AWPCA"]}], [])
|
||||
b = queue.create_batch(conn, "x", "claude", [sprint("AURORA-1538")]) # и без project пакета
|
||||
assert queue.batch_view(conn, b["id"])["commands"][0]["params"] == {"key": "AURORA-1538", "sprint": "Спринт 2", "board_id": 4216}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("setup, key, msg", [
|
||||
(dict(board_id=5120, board_type="kanban"), "AURORA-1", "у проекта STOCKS_DRRK канбан-доска, спринтов нет"),
|
||||
(dict(board_id=5120, board_type=None), "AURORA-1", "тип доски проекта STOCKS_DRRK ещё не известен"),
|
||||
(dict(board_id=None, board_type=None), "AURORA-1", "у проекта STOCKS_DRRK нет доски Jira"),
|
||||
(dict(board_id=5120, board_type="scrum"), "AURORA-77", "AURORA-77 не входит ни в один проект"),
|
||||
(dict(board_id=5120, board_type="scrum"), "AURORA-404", "AURORA-404 не входит ни в один проект"), # нет в зеркале
|
||||
(dict(board_id=5120, board_type="scrum"), "AURORA-12", "с разными досками"),
|
||||
])
|
||||
def test_sprint_board_rejected(conn, add_project, setup, key, msg):
|
||||
add_project(conn) # AWPCA 4216 scrum
|
||||
add_project(conn, "STOCKS_DRRK", **setup)
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1", "labels": ["STOCKS_DRRK"]}, {"key": "AURORA-77", "labels": ["kind-dev"]},
|
||||
{"key": "AURORA-12", "labels": ["AWPCA", "STOCKS_DRRK"]}], [])
|
||||
with pytest.raises(CommandError, match=msg):
|
||||
queue.create_batch(conn, "x", "claude", [sprint(key)])
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM batch").fetchone()["n"] == 0
|
||||
|
||||
|
||||
def test_sprint_rejected_message_has_command_number(conn, add_project):
|
||||
add_project(conn, board_id=5120, board_type="kanban")
|
||||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
|
||||
with pytest.raises(CommandError) as e:
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1"), sprint("A-1")])
|
||||
assert str(e.value) == "команда 2: у проекта AWPCA канбан-доска, спринтов нет"
|
||||
|
||||
|
||||
def test_backlog_on_kanban_rejected(conn, add_project):
|
||||
add_project(conn, board_id=5120, board_type="kanban")
|
||||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
|
||||
with pytest.raises(CommandError, match="канбан-доска"):
|
||||
queue.create_batch(conn, "x", "claude", [sprint("A-1", "backlog")])
|
||||
|
||||
|
||||
def test_sprint_two_projects_same_board(conn, add_project):
|
||||
add_project(conn)
|
||||
add_project(conn, "C", board_id=4216)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA", "C"]}], [])
|
||||
b = queue.create_batch(conn, "x", "claude", [sprint("A-1")])
|
||||
assert queue.batch_view(conn, b["id"])["commands"][0]["params"]["board_id"] == 4216
|
||||
|
||||
|
||||
def test_sprint_for_new_issue_uses_batch_labels(conn, add_project):
|
||||
add_project(conn)
|
||||
add_project(conn, "C", board_id=5120)
|
||||
b = queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Н"}}, sprint("new:1")],
|
||||
project="AWPCA") # метка проекта дописана Task 5
|
||||
assert queue.batch_view(conn, b["id"])["commands"][1]["params"]["board_id"] == 4216
|
||||
b = queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:2", "issue_type": "task", "summary": "Н2", "labels": ["C"]}},
|
||||
sprint("new:2")]) # без проекта — метки самой команды
|
||||
assert queue.batch_view(conn, b["id"])["commands"][1]["params"]["board_id"] == 5120
|
||||
|
||||
|
||||
def test_sprint_for_new_issue_without_project_labels_rejected(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "new:1", "labels": ["AWPCA"]}], []) # зеркало не должно подменять метки пакета
|
||||
with pytest.raises(CommandError, match="не входит ни в один проект"):
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Н"}}, sprint("new:1")])
|
||||
|
||||
|
||||
def test_client_board_id_rejected(conn, add_project):
|
||||
add_project(conn)
|
||||
with pytest.raises(CommandError, match="лишние параметры board_id"):
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "С", "board_id": 1}}])
|
||||
|
||||
|
||||
def test_input_params_not_mutated(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
|
||||
cmds = [sprint("A-1")]
|
||||
queue.create_batch(conn, "x", "claude", cmds)
|
||||
assert cmds == [sprint("A-1")]
|
||||
|
||||
|
||||
def test_retry_sprint_recomputes_board(conn, add_project): # сценарий «Повтор ensure_sprint»
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1538", "labels": ["AWPCA"]}], [])
|
||||
b = queue.create_batch(conn, "x", "claude", [sprint("AURORA-1538")])
|
||||
cid = queue.batch_view(conn, b["id"])["commands"][0]["id"]
|
||||
conn.execute("UPDATE project SET board_id = 5120, board_type = 'scrum'")
|
||||
r = queue.retry_command(conn, cid, "claude")
|
||||
assert queue.batch_view(conn, r["id"])["commands"][0]["params"]["board_id"] == 5120
|
||||
|
||||
|
||||
def test_retry_premigration_sprint_without_board(conn, add_project): # Review Focus 5
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
|
||||
cur = conn.execute("INSERT INTO batch(title, author, created_at, estimate_seconds, project) VALUES ('старый', 'claude', ?, 1, NULL)",
|
||||
(db.now(),))
|
||||
cid = conn.execute("INSERT INTO command(batch_id, seq, type, params) VALUES (?, 1, 'ensure_sprint', ?)",
|
||||
(cur.lastrowid, '{"key": "A-1", "sprint": "Спринт 2"}')).lastrowid
|
||||
r = queue.retry_command(conn, cid, "claude")
|
||||
assert queue.batch_view(conn, r["id"])["commands"][0]["params"] == {"key": "A-1", "sprint": "Спринт 2", "board_id": 4216}
|
||||
|
||||
|
||||
def test_refresh_delivered_with_boards(conn, add_project): # сценарий «Тип доски определён», часть 1
|
||||
add_project(conn)
|
||||
add_project(conn, "STOCKS_DRRK", board_id=5120, board_type=None)
|
||||
add_project(conn, "C", board_id=4216)
|
||||
b = queue.create_batch(conn, "r", "api", [{"type": "refresh", "params": {"jql": "key = A-1"}}])
|
||||
got = queue.claim(conn)
|
||||
assert got["params"]["boards"] == [{"id": 4216, "type": "scrum"}, {"id": 5120, "type": None}]
|
||||
assert "boards" not in queue.batch_view(conn, b["id"])["commands"][0]["params"]
|
||||
assert queue.claim(conn)["params"]["boards"] == got["params"]["boards"] # довыдача — тоже с boards
|
||||
|
||||
|
||||
def test_refresh_boards_empty_list(conn, add_project):
|
||||
add_project(conn, board_id=None, board_type=None)
|
||||
queue.create_batch(conn, "r", "api", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
assert queue.claim(conn)["params"]["boards"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("result, expected", [
|
||||
({"boards": {"5120": "kanban"}}, "kanban"), # ключ — строка, project.board_id — int
|
||||
({"issues": 3}, None), # агент v11 / v12 без запроса типов: ключа нет
|
||||
({"boards": {"5120": None}}, None), # доска не найдена — тип не меняется
|
||||
({"boards": {"abc": "scrum"}}, None),
|
||||
({"boards": {"5120": "sprint"}}, None), # мусорный тип
|
||||
({"boards": ["5120", "kanban"]}, None), # boards не словарь
|
||||
({"boards": "kanban"}, None),
|
||||
(None, None),
|
||||
])
|
||||
def test_refresh_result_board_types(conn, add_project, result, expected):
|
||||
add_project(conn, "STOCKS_DRRK", board_id=5120, board_type=None)
|
||||
queue.create_batch(conn, "r", "api", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
c = queue.claim(conn)
|
||||
queue.report_result(conn, c["id"], "done", result=result)
|
||||
assert projects.get(conn, "STOCKS_DRRK")["board_type"] == expected
|
||||
|
||||
|
||||
def test_refresh_result_board_type_changes_scrum_to_kanban(conn, add_project): # доска в Jira стала канбаном
|
||||
add_project(conn, "STOCKS_DRRK", board_id=5120, board_type="scrum")
|
||||
queue.create_batch(conn, "r", "api", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
c = queue.claim(conn)
|
||||
queue.report_result(conn, c["id"], "done", result={"boards": {"5120": "kanban"}})
|
||||
assert projects.get(conn, "STOCKS_DRRK")["board_type"] == "kanban"
|
||||
|
||||
|
||||
def test_refresh_failed_result_keeps_board_types(conn, add_project):
|
||||
add_project(conn, "STOCKS_DRRK", board_id=5120, board_type=None)
|
||||
queue.create_batch(conn, "r", "api", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
c = queue.claim(conn)
|
||||
queue.report_result(conn, c["id"], "failed", result={"boards": {"5120": "kanban"}}, error="e")
|
||||
assert projects.get(conn, "STOCKS_DRRK")["board_type"] is None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user