Task 3: author в PATCH, ответ POST по спеке, проверка editable-полей (#91)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
04e67d5139
commit
9534e1385a
@ -382,6 +382,8 @@ def _module_fields(body, skip, *, editable_only: bool) -> dict[str, tuple[object
|
||||
known = registry.project_fields()
|
||||
out: dict[str, tuple[object, dict]] = {}
|
||||
for key, value in body.items():
|
||||
if key == "author": # автор пакетов, не поле проекта: читает _author
|
||||
continue
|
||||
if key in skip:
|
||||
if editable_only:
|
||||
raise ApiError(422, "bad_project", f"{key} после создания не меняется")
|
||||
@ -402,13 +404,13 @@ def _module_fields(body, skip, *, editable_only: bool) -> dict[str, tuple[object
|
||||
def h_projects_post(hub, conn, m, q, body):
|
||||
"""Новый проект: проект, поля и пакеты модулей (on_project_created; у jira — «Новый проект <метка>») — в одной
|
||||
транзакции; ошибка любого шага не оставляет ни проекта, ни пакета (hub-api, «Маршруты проектов»).
|
||||
Ответ: project, batch_ids (все пакеты) и batch_id — первый из них (у jira это её пакет, R-2)."""
|
||||
Ответ: project и batch_id — пакет первого модуля, вернувшего пакет."""
|
||||
author = _author(body)
|
||||
try:
|
||||
label, name = projects.validate(body.get("label"), body.get("name"))
|
||||
except projects.ProjectError as e:
|
||||
raise ApiError(422, "bad_project", str(e))
|
||||
by_module = _module_fields(body, {"label", "name", "author"}, editable_only=False)
|
||||
by_module = _module_fields(body, {"label", "name"}, editable_only=False)
|
||||
batch_ids = []
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
@ -425,7 +427,7 @@ def h_projects_post(hub, conn, m, q, body):
|
||||
raise ApiError(422, "bad_project", str(e))
|
||||
except CommandError as e:
|
||||
raise ApiError(422, "bad_batch", str(e))
|
||||
return 201, {"project": row, "batch_id": batch_ids[0] if batch_ids else None, "batch_ids": batch_ids}
|
||||
return 201, {"project": row, "batch_id": batch_ids[0] if batch_ids else None}
|
||||
|
||||
|
||||
def h_project_patch(hub, conn, m, q, body):
|
||||
@ -433,7 +435,7 @@ def h_project_patch(hub, conn, m, q, body):
|
||||
label = unquote(m.group(1))
|
||||
project = not_found(projects.get(conn, label), f"project {label}")
|
||||
author = _author(body)
|
||||
by_module = _module_fields(body, {"label", "name", "created_at", "author"}, editable_only=True)
|
||||
by_module = _module_fields(body, {"label", "name", "created_at"}, editable_only=True)
|
||||
batch_ids = []
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
|
||||
@ -62,7 +62,7 @@ def _migrate_projects(conn) -> None:
|
||||
projects.migrate_v3(conn)
|
||||
|
||||
|
||||
def _migrate_jira_project(conn) -> None:
|
||||
def _project_fields_v7(conn) -> None:
|
||||
from ..modules.jira import projects
|
||||
projects.migrate_v6(conn)
|
||||
|
||||
@ -129,7 +129,7 @@ MIGRATIONS = {
|
||||
_fill_comment_utc],
|
||||
5: [], # v5 → v6: issue_attachment и attachment_file создаются схемой (CREATE TABLE IF NOT EXISTS), данных переносить не нужно
|
||||
6: [_tokens_v7, _agent_status_v7,
|
||||
_migrate_jira_project], # v6 → v7: токены и статус по агентам модулей (wiki-module, решение 15)
|
||||
_project_fields_v7], # v6 → v7: токены и статус по агентам модулей (wiki-module, решение 15)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -116,7 +116,8 @@ def create_project(label: str, name: str, fields: dict, module: str) -> str:
|
||||
исключения клиента: вызывающий инструмент обёрнут в guard."""
|
||||
out = client().post("/projects", {"label": label, "name": name, "author": AUTHOR, **fields})
|
||||
p = out["project"]
|
||||
return (f"проект {p['label']} «{p['name']}» заведён, пакет обновления #{out['batch_id']}\n"
|
||||
batch = f", пакет обновления #{out['batch_id']}" if out.get("batch_id") is not None else ""
|
||||
return (f"проект {p['label']} «{p['name']}» заведён{batch}\n"
|
||||
"Положите в корень репозитория проекта .mcp.json (слейте с имеющимся) и переподключите MCP: "
|
||||
"/mcp → reconnect, одобрите сервер проекта.\n"
|
||||
+ json.dumps(mcp_json(p["label"], module), ensure_ascii=False, indent=2))
|
||||
|
||||
@ -60,6 +60,8 @@ def check(mods: list[Module], core_routes, core_settings: dict) -> None:
|
||||
if f.name in fields:
|
||||
raise RuntimeError(f"поле проекта {f.name}: {fields[f.name]} и {m.name}")
|
||||
fields[f.name] = m.name
|
||||
if f.editable and m.on_project_updated is None:
|
||||
raise RuntimeError(f"поле проекта {f.name}: модуль {m.name} объявил его изменяемым без on_project_updated")
|
||||
for r in m.routes:
|
||||
if (r[0], r[1]) in routes:
|
||||
raise RuntimeError(f"маршрут {r[0]} {r[1]}: {routes[(r[0], r[1])]} и модуль {m.name}")
|
||||
|
||||
@ -44,9 +44,60 @@ def test_post_unknown_field(hub_server):
|
||||
assert c.get("/projects")["projects"] == []
|
||||
|
||||
|
||||
def test_post_returns_batch_ids(hub_server):
|
||||
def test_post_response_shape(hub_server):
|
||||
out = hub_server["client"].post("/projects", {"label": "X", "name": "X", "board_id": 7})
|
||||
assert out["batch_ids"] == [out["batch_id"]] and out["project"]["board_id"] == 7
|
||||
assert set(out) == {"project", "batch_id"} and out["project"]["board_id"] == 7
|
||||
assert hub_server["client"].get(f"/batches/{out['batch_id']}")["title"] == "Новый проект X"
|
||||
|
||||
|
||||
def test_patch_author_ok(hub_server, add_project):
|
||||
conn = hub_server["hub"].conn()
|
||||
try:
|
||||
add_project(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
out = hub_server["client"].patch("/projects/AWPCA", {"author": "viewer"})
|
||||
assert out["batch_ids"] == []
|
||||
|
||||
|
||||
def test_patch_unknown_field(hub_server, add_project):
|
||||
conn = hub_server["hub"].conn()
|
||||
try:
|
||||
add_project(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
with pytest.raises(HubError) as e:
|
||||
hub_server["client"].patch("/projects/AWPCA", {"nope": 1})
|
||||
assert e.value.status == 422 and e.value.detail == "nope: неизвестное поле"
|
||||
|
||||
|
||||
def test_editable_field_needs_updater():
|
||||
m = Module(name="a", project_fields=(ProjectField("root", _ok, editable=True),))
|
||||
with pytest.raises(RuntimeError) as e:
|
||||
registry.check([m], [], {})
|
||||
assert "root" in str(e.value) and "on_project_updated" in str(e.value)
|
||||
|
||||
|
||||
def test_migrate_v6_idempotent(tmp_path):
|
||||
from jira_hub.modules.jira import projects as jira_projects
|
||||
make_v6_db(tmp_path / "hub.sqlite")
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
try:
|
||||
before = [tuple(r) for r in conn.execute("SELECT * FROM jira_project ORDER BY label")]
|
||||
jira_projects.migrate_v6(conn)
|
||||
assert [tuple(r) for r in conn.execute("SELECT * FROM jira_project ORDER BY label")] == before
|
||||
assert conn.execute("PRAGMA foreign_key_check").fetchall() == []
|
||||
assert [r["name"] for r in conn.execute("SELECT name FROM project ORDER BY label")] == ["АРМ КА", "sfm_kedr"]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_create_project_text_without_batch(monkeypatch):
|
||||
class C:
|
||||
def post(self, path, body):
|
||||
return {"project": {"label": "P", "name": "p"}, "batch_id": None}
|
||||
monkeypatch.setattr(core_mcp, "client", lambda: C())
|
||||
assert "#None" not in core_mcp.create_project("P", "p", {}, "wiki") and "пакет" not in core_mcp.create_project("P", "p", {}, "wiki").splitlines()[0]
|
||||
|
||||
|
||||
def test_patch_board_forbidden(hub_server, add_project):
|
||||
@ -152,7 +203,7 @@ def test_module_fields_post_and_patch(two_modules):
|
||||
api, ApiError, conn, re = two_modules
|
||||
code, out = api.h_projects_post(None, conn, None, {}, {"label": "P", "name": "p", "board_id": 3, "root": "doc-1"})
|
||||
assert code == 201 and out["project"]["root"] == "DOC-1" and out["project"]["board_id"] == 3
|
||||
assert out["batch_ids"] == [out["batch_id"]] # пакет есть только у jira
|
||||
assert set(out) == {"project", "batch_id"} and out["batch_id"] is not None # пакет только у jira
|
||||
code, out = api.h_project_patch(None, conn, re.match(r"(.*)", "P"), {}, {"root": "doc-2"})
|
||||
assert code == 200 and out["project"]["root"] == "DOC-2" and len(out["batch_ids"]) == 1
|
||||
with pytest.raises(ApiError) as e:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user