API: состояние агента и синхронизация вики неверной формы — 422, а не 500 (#129, #42 T2.1, #94 T8.1)

mirror.check_issues проверяет форму issues до записи; /wiki/sync отвергает project не строку.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-26 02:32:39 +04:00
parent 4a6855d70b
commit f1dae5a811
5 changed files with 35 additions and 0 deletions

View File

@ -24,6 +24,7 @@ def h_agent_state(hub, conn, m, q, body):
issues = body.get("issues") or []
boards = body.get("boards")
try:
mirror.check_issues(issues)
mirror.check_comments(issues) # до записи: неверная пачка не применяется целиком
mirror.check_attachments(issues)
mirror.check_agile_fields(issues)

View File

@ -32,6 +32,15 @@ def check_comments(issues) -> None:
if c.get(f) is not None and not isinstance(c[f], str):
raise ValueError(f"{where}.{f} — строка или null")
def check_issues(issues) -> None:
"""Форма списка задач состояния: список объектов с непустым строковым key. Нарушение — ValueError."""
if not isinstance(issues, list):
raise ValueError("issues — список задач")
for n, it in enumerate(issues):
if not isinstance(it, dict) or not isinstance(it.get("key"), str) or not it["key"]:
raise ValueError(f"issues[{n}] — объект задачи с key")
def check_attachments(issues) -> None:
"""Форма поля attachments у задач состояния (агент v14+); поле необязательно. Нарушение — ValueError с ключом и полем."""
for it in issues:

View File

@ -94,6 +94,8 @@ def h_search(hub, conn, m, q, body):
def h_sync(hub, conn, m, q, body):
if body.get("project") is not None and not isinstance(body["project"], str):
raise ApiError(422, "bad_request", "project — строка")
q = dict(q, project=body.get("project"))
author = body.get("author") or "api"
if not isinstance(author, str):

View File

@ -1299,3 +1299,17 @@ def test_rank_before_trimmed_on_claim(hub):
{"type": "ensure_rank", "params": {"key": "X-1", "before": " X-2 "}}]})
cmd = hub["agent"].post("/agent/claim", {})["command"]
assert cmd["type"] == "ensure_rank" and cmd["params"]["before"] == "X-2"
@pytest.mark.parametrize("issues, detail", [
(["X-1"], "issues[0] — объект задачи с key"),
([{"summary": "без ключа"}], "issues[0] — объект задачи с key"),
([{"key": 5}], "issues[0] — объект задачи с key"),
([{"key": ""}], "issues[0] — объект задачи с key"),
({"key": "X-1"}, "issues — список задач"),
("X-1", "issues — список задач"),
])
def test_agent_state_bad_issue_shape_422(hub, issues, detail):
with pytest.raises(HubError) as e:
hub["agent"].post("/agent/state", {"issues": issues, "links": []})
assert e.value.status == 422 and e.value.error == "bad_state" and detail in e.value.detail

View File

@ -228,3 +228,12 @@ def test_page_attachment_failed_error_visible(api):
c.close()
att = api.client.get("/wiki/pages/DOC-3", project="AWPCA")["attachments"][0]
assert att["state"] == "failed" and att["error"] == "Eva: 404"
@pytest.mark.parametrize("project", [5, ["AWPCA"], {"a": 1}, True])
def test_sync_project_not_string_422(api, project):
first_sync(api)
before = api.rows("SELECT id FROM batch")
e = err(api.client.post, "/wiki/sync", {"project": project})
assert e.status == 422 and e.error == "bad_request" and e.detail == "project — строка"
assert api.rows("SELECT id FROM batch") == before # пакета не создано