feat(hub): проверка комментариев в POST /agent/state (422 bad_state), комментарии в MCP issue (#37)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
88a6f72b13
commit
a03550f079
@ -163,8 +163,13 @@ def h_agent_result(hub, conn, m, q, body):
|
||||
|
||||
|
||||
def h_agent_state(hub, conn, m, q, body):
|
||||
issues = body.get("issues") or []
|
||||
try:
|
||||
mirror.check_comments(issues) # до записи: неверная пачка не применяется целиком
|
||||
except ValueError as e:
|
||||
raise ApiError(422, "bad_state", str(e))
|
||||
with db.transaction(conn):
|
||||
out = mirror.apply_state(conn, body.get("issues") or [], body.get("links") or [], body.get("neighbor_keys") or [],
|
||||
out = mirror.apply_state(conn, issues, body.get("links") or [], body.get("neighbor_keys") or [],
|
||||
authoritative=queue.is_full_refresh(conn, body.get("command_id")))
|
||||
if body.get("command_id"):
|
||||
queue.record_state(conn, body["command_id"], out["issues"], out["links"])
|
||||
|
||||
@ -80,8 +80,14 @@ def fmt_issue(d) -> str:
|
||||
note = d.get("note") or {}
|
||||
notes = [f" {k}: {note[k]}" for k in ("fact_status", "work_kind", "done_text", "rest_text", "owner_note") if note.get(k)]
|
||||
cmds = [f" #{c['id']} {c['type']} {c['status']} {c.get('finished_at') or ''} {c.get('error') or ''}".rstrip() for c in d.get("commands", [])]
|
||||
return "\n".join(head + ["связи:"] + (links or [" —"]) + ["заметки:"] + (notes or [" —"]) + ["команды:"] + (cmds or [" —"])
|
||||
+ ["описание:", d.get("description") or "—"])
|
||||
comments = d.get("comments") or []
|
||||
if comments:
|
||||
shown = [f" {_v(c.get('author'))} {_v(c.get('created'))}: {_cut(c.get('body'), 300)}" for c in comments[:5]]
|
||||
cmts = [f"комментарии ({len(comments)}):"] + shown + ([f" ещё {len(comments) - 5}"] if len(comments) > 5 else [])
|
||||
else:
|
||||
cmts = ["комментариев нет"]
|
||||
return "\n".join(head + ["связи:"] + (links or [" —"]) + ["заметки:"] + (notes or [" —"]) + cmts
|
||||
+ ["команды:"] + (cmds or [" —"]) + ["описание:", d.get("description") or "—"])
|
||||
|
||||
|
||||
def fmt_batch(b) -> str:
|
||||
|
||||
@ -72,7 +72,7 @@ def test_agent_flow(hub):
|
||||
a.post(f"/agent/commands/{tail['id']}/result", {"status": "done", "jira_calls": 1})
|
||||
assert c.get("/batches/%d" % b["id"])["status"] == "done"
|
||||
issue = c.get("/issues/A-1")
|
||||
assert issue["links_out"] == [{"target_key": "B-1", "link_type": "ASUPR_Связанные"}]
|
||||
assert issue["links_out"] == [{"target_key": "B-1", "link_type": "ASUPR_Связанные", "summary": "Сосед", "status": None}]
|
||||
assert c.get("/issues/B-1")["neighbor"] is True
|
||||
runs = c.get("/sync-runs", limit=5)["runs"]
|
||||
assert runs[0]["issues_count"] == 2 and runs[0]["finished_at"]
|
||||
@ -572,3 +572,29 @@ def test_tick_creates_auto_refresh_once(hub):
|
||||
assert [b["title"] for b in batches] == ["Автообновление"] and batches[0]["author"] == "hub"
|
||||
hub["hub"].tick()
|
||||
assert len(hub["client"].get("/batches", limit=5)["batches"]) == 1
|
||||
|
||||
|
||||
def test_agent_state_comments_validated_whole_batch(hub):
|
||||
a, c = hub["agent"], hub["client"]
|
||||
a.post("/agent/state", {"issues": [{"key": "X-1", "summary": "Икс", "labels": []}], "links": []})
|
||||
with pytest.raises(HubError) as e:
|
||||
a.post("/agent/state", {"issues": [{"key": "X-1", "summary": "Икс новый", "labels": []},
|
||||
{"key": "Y-1", "summary": "Игрек", "labels": [],
|
||||
"comments": [{"id": "7", "body": 42}]}], "links": []})
|
||||
assert e.value.status == 422 and e.value.error == "bad_state"
|
||||
assert "Y-1" in e.value.detail and "body" in e.value.detail
|
||||
assert c.get("/issues/X-1")["summary"] == "Икс" # пачка не применена целиком
|
||||
with pytest.raises(HubError) as e:
|
||||
c.get("/issues/Y-1")
|
||||
assert e.value.status == 404
|
||||
|
||||
|
||||
def test_issue_detail_with_comments(hub):
|
||||
hub["agent"].post("/agent/state", {"issues": [
|
||||
{"key": "A-1", "summary": "Схема", "labels": [], "epic_key": "E-1", "comments": [
|
||||
{"id": "1", "author": "Петров Пётр", "created": "2026-10-14T12:00:00.000+0300", "updated": None, "body": "старый"},
|
||||
{"id": "2", "author": None, "created": "2026-10-15T12:00:00.000+0300", "updated": None, "body": "новый"}]},
|
||||
{"key": "E-1", "summary": "Эпик", "labels": []}], "links": []})
|
||||
got = hub["client"].get("/issues/A-1")
|
||||
assert [x["body"] for x in got["comments"]] == ["новый", "старый"]
|
||||
assert got["epic_summary"] == "Эпик"
|
||||
|
||||
@ -236,3 +236,25 @@ def test_agent_status_right_after_hub_start(live):
|
||||
live["agent"].post("/agent/claim", {"agent_version": 9, "launcher_version": 2})
|
||||
text = mcp_server.agent_status()
|
||||
assert "запускатель: v2" in text and "неизвестно" not in text
|
||||
|
||||
|
||||
def _detail(**over):
|
||||
d = {"key": "A-1", "summary": "Т", "labels": [], "links_out": [], "links_in": [], "note": None, "commands": [],
|
||||
"comments": [], "description": None}
|
||||
d.update(over)
|
||||
return d
|
||||
|
||||
|
||||
def test_fmt_issue_comments():
|
||||
assert "комментариев нет" in mcp_server.fmt_issue(_detail())
|
||||
comments = [{"comment_id": str(n), "author": f"Автор {n}", "created": f"2026-10-1{n}T12:00:00.000+0300",
|
||||
"updated": None, "body": ("д" * 500 if n == 7 else f"текст {n}\nстрока")} for n in range(7, 0, -1)]
|
||||
text = mcp_server.fmt_issue(_detail(comments=comments))
|
||||
assert "комментарии (7):" in text
|
||||
assert "д" * 299 + "…" in text and "д" * 300 not in text # самый новый — первым, обрезан до 300
|
||||
assert "Автор 3" in text and "Автор 2" not in text # 5 последних: 7…3
|
||||
assert "ещё 2" in text
|
||||
assert text.index("заметки:") < text.index("комментарии (7):") < text.index("команды:")
|
||||
no_author = mcp_server.fmt_issue(_detail(comments=[{"comment_id": "1", "author": None, "created": None,
|
||||
"updated": None, "body": ""}]))
|
||||
assert "комментарии (1):" in no_author and "ещё" not in no_author
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user