sdlc-hub/tests/test_project_fields.py
Andrey Epifancev a88bb1e6e8 refactor(sdlc-rename): пакет sdlc_hub, CLI sdlc-hub, каталог данных sdlc-hub
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:27:17 +04:00

222 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Поля проекта от модулей; доски — в модуле jira (wiki-module, решение 8, спеки modules/data-model/hub-api)."""
import pytest
from sdlc_hub.core import db, mcp as core_mcp, registry
from sdlc_hub.core.client import HubError
from sdlc_hub.core.modules import Module, ProjectField
from sdlc_hub.modules.jira import mirror
from v6db import make_v6_db
def _ok(v):
return v
def test_v6_boards_moved(tmp_path):
make_v6_db(tmp_path / "hub.sqlite")
conn = db.connect(tmp_path / "hub.sqlite")
try:
cols = {r["name"] for r in conn.execute("PRAGMA table_info(project)")}
assert cols == {"label", "name", "created_at"}
rows = conn.execute("SELECT label, board_id, board_type FROM jira_project ORDER BY label").fetchall()
assert [tuple(r) for r in rows] == [("AWPCA", 4216, "scrum"), ("sfm_kedr", None, None)]
finally:
conn.close()
def test_projects_get_shape(hub_server, add_project):
conn = hub_server["hub"].conn()
try:
add_project(conn)
mirror.apply_state(conn, [{"key": "A-1", "labels": ["AWPCA"]}], [])
finally:
conn.close()
got = hub_server["client"].get("/projects")["projects"]
assert got == [{"label": "AWPCA", "name": "АРМ КА", "created_at": got[0]["created_at"],
"board_id": 4216, "board_type": "scrum", "issues_count": 1, "wiki_root": None,
"wiki_root_title": None, "wiki_pages": None, "wiki_synced_at": None, "wiki_sync_error": None}]
def test_post_unknown_field(hub_server):
c = hub_server["client"]
with pytest.raises(HubError) as e:
c.post("/projects", {"label": "X", "name": "X", "board": 5})
assert e.value.status == 422 and e.value.error == "bad_project" and "board" in e.value.detail
assert c.get("/projects")["projects"] == []
def test_post_response_shape(hub_server):
out = hub_server["client"].post("/projects", {"label": "X", "name": "X", "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 sdlc_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):
conn = hub_server["hub"].conn()
try:
add_project(conn)
finally:
conn.close()
c = hub_server["client"]
with pytest.raises(HubError) as e:
c.patch("/projects/AWPCA", {"board_id": 1})
assert e.value.status == 422 and e.value.error == "bad_project"
assert "board_id после создания не меняется" in e.value.detail
assert c.get("/projects")["projects"][0]["board_id"] == 4216
def test_patch_unknown_project(hub_server):
c = hub_server["client"]
with pytest.raises(HubError) as e:
c.patch("/projects/NOPE", {})
assert e.value.status == 404
def test_patch_empty_ok(hub_server, add_project):
conn = hub_server["hub"].conn()
try:
add_project(conn)
finally:
conn.close()
c = hub_server["client"]
out = c.patch("/projects/AWPCA", {})
assert out["batch_ids"] == [] and out["project"]["label"] == "AWPCA"
def test_field_conflict():
a = Module(name="a", project_fields=(ProjectField("root", _ok),))
b = Module(name="b", project_fields=(ProjectField("root", _ok),))
with pytest.raises(RuntimeError) as e:
registry.check([a, b], [], {})
text = str(e.value)
assert "root" in text and "a" in text and "b" in text
@pytest.mark.parametrize("name", ["label", "name", "created_at", "author"])
def test_field_clashes_with_core(name):
with pytest.raises(RuntimeError) as e:
registry.check([Module(name="a", project_fields=(ProjectField(name, _ok),))], [], {})
assert name in str(e.value)
def test_mcp_json_per_module():
first = core_mcp.mcp_json("AWPCA")["mcpServers"]
assert list(first) == ["jira-hub"] and first["jira-hub"]["args"][-3:] == ["mcp", "--project", "AWPCA"]
other = core_mcp.mcp_json("AWPCA", "wiki")["mcpServers"]
assert list(other) == ["jira-hub-wiki"]
assert other["jira-hub-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
def test_fmt_projects_columns():
items = [{"label": "A", "name": "a", "x": 1}, {"label": "B", "name": "b", "x": None}]
text = core_mcp.fmt_projects(items, "B", [("икс", lambda p: core_mcp.value(p.get("x")))])
assert text == "метка | название | икс\nA | a | 1\nB | b | — (текущий)"
@pytest.fixture
def two_modules(monkeypatch, open_db):
"""Реестр [jira, stub]: stub объявляет изменяемое поле root, хранит его в setting и ставит пакеты-пустышки."""
import json
import re
from sdlc_hub.core import api, queue
from sdlc_hub.core.http import ApiError
from sdlc_hub.modules.jira import MODULE as JIRA
def check_root(v):
if not isinstance(v, str) or not v:
raise ValueError("ожидается непустая строка")
return v.upper()
def created(conn, project, fields, author):
if "root" in fields:
conn.execute("INSERT OR REPLACE INTO setting(key, value) VALUES (?, ?)",
(f"root:{project['label']}", json.dumps(fields["root"])))
def updated(conn, project, fields, author):
created(conn, project, fields, author)
return [queue.create_batch(conn, "stub", author, [{"type": "refresh", "params": {"keys": ["A-1"]}}])]
def view(conn, labels):
out = {}
for label in labels:
row = conn.execute("SELECT value FROM setting WHERE key = ?", (f"root:{label}",)).fetchone()
out[label] = {"root": json.loads(row["value"]) if row else None}
return out
stub = Module(name="stub", project_fields=(ProjectField("root", check_root, editable=True),),
project_view=view, on_project_created=created, on_project_updated=updated)
monkeypatch.setattr(registry, "_cache", [JIRA, stub])
conn = open_db()
return api, ApiError, conn, re
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 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:
api.h_project_patch(None, conn, re.match(r"(.*)", "P"), {}, {"root": ""})
assert e.value.status == 422 and e.value.detail.startswith("root")
assert api.project_rows(conn)[0]["root"] == "DOC-2"
def test_module_field_error_writes_nothing(two_modules):
api, ApiError, conn, re = two_modules
with pytest.raises(ApiError) as e:
api.h_projects_post(None, conn, None, {}, {"label": "P", "name": "p", "root": 5})
assert e.value.status == 422 and e.value.detail.startswith("root")
assert api.project_rows(conn) == []