"""История правок вики (wiki-history, #140): приём версий `POST /agent/wiki/versions`, время `history.utc`.""" import json import pytest from sdlc_hub.core.client import HubError from sdlc_hub.modules.wiki import history from test_wiki_sync import CODES, TREE, api, first_sync, page # noqa: F401 — api — фикстура SOSNOV = {"login": "sosnov-dvi@rshbeco.ru", "name": "Дмитрий Соснов"} MOLCH = {"login": "molchanov-iv@rshbeco.ru", "name": "Илья Молчанов"} def version(page_code, code, created_at, html="

текст

", author=SOSNOV, attachments=None, links=None): return {"page_code": page_code, "code": code, "created_at": created_at, "author": author, "html": html, "attachments": attachments or [], "links": links or []} def stage_rows(api, cmd_id): return api.rows("SELECT code, page_code, payload FROM wiki_version_stage WHERE command_id = ? ORDER BY code", cmd_id) def test_versions_staged(api): # перевод и форма payload cmd = api.claim() out = api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [ version("DOC-3", "DOCH-1", "2026-10-27T23:48:02+03:00", "

статус новый

")]}) assert out == {"accepted": 1} (row,) = stage_rows(api, cmd["id"]) p = json.loads(row["payload"]) assert p == {"page_code": "DOC-3", "code": "DOCH-1", "created_at": "2026-10-27T20:48:02+00:00", "author_login": "sosnov-dvi@rshbeco.ru", "author_name": "Дмитрий Соснов", "markdown": "статус **новый**\n"} def test_versions_resend_same_batch(api): # Review Focus 2: повтор пачки — одна строка на код cmd = api.claim() batch = [version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z"), version("DOC-3", "DOCH-2", "2026-10-27T11:00:00Z")] for _ in range(2): api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": batch}) assert [r["code"] for r in stage_rows(api, cmd["id"])] == ["DOCH-1", "DOCH-2"] @pytest.mark.parametrize("raw,stored", [("2026-10-27T23:48:02.123456+03:00", "2026-10-27T20:48:02+00:00"), ("2026-10-27T20:48:02Z", "2026-10-27T20:48:02+00:00"), ("2026-10-27T23:48:02+0300", "2026-10-27T20:48:02+00:00")]) def test_versions_created_at_forms(api, raw, stored): # Review Focus 4 → created_at в payload == stored cmd = api.claim() api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [version("DOC-3", "DOCH-1", raw)]}) (row,) = stage_rows(api, cmd["id"]) assert json.loads(row["payload"])["created_at"] == stored def test_versions_created_at_without_offset_422(api): cmd = api.claim() with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [ version("DOC-3", "DOCH-1", "2026-10-27T23:48:02")]}) assert e.value.status == 422 and e.value.error == "bad_versions" and e.value.detail == "DOCH-1: created_at" def test_versions_author_nulls(api): cmd = api.claim() api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [ version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z", author={"login": None, "name": None})]}) (row,) = stage_rows(api, cmd["id"]) p = json.loads(row["payload"]) assert p["author_login"] is None and p["author_name"] is None @pytest.mark.parametrize("field,value", [("page_code", ""), ("page_code", 5), ("html", None), ("author", "Соснов"), ("author", {"login": 1, "name": "x"}), ("attachments", [{"ref": 1}]), ("links", "x")]) def test_versions_bad_field(api, field, value): # 422 bad_versions, detail == f"DOCH-1: {field}" cmd = api.claim() bad = version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z") | {field: value} with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [bad]}) assert e.value.status == 422 and e.value.error == "bad_versions" and e.value.detail == f"DOCH-1: {field}" assert api.rows("SELECT COUNT(*) AS n FROM wiki_version_stage")[0]["n"] == 0 def test_versions_bad_batch(api): # [] и 51 версия → 422 bad_versions; command_id "1" → 422 bad_request; # версия-не объект → detail "версия 1: не объект"; без code → "версия 1: code" cmd = api.claim() with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": []}) assert e.value.status == 422 and e.value.error == "bad_versions" many = [version("DOC-3", f"DOCH-{i}", "2026-10-27T10:00:00Z") for i in range(51)] with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": many}) assert e.value.status == 422 and e.value.error == "bad_versions" with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": "1", "versions": [version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z")]}) assert e.value.status == 422 and e.value.error == "bad_request" with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": ["x"]}) assert e.value.status == 422 and e.value.detail == "версия 1: не объект" no_code = {k: v for k, v in version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z").items() if k != "code"} with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [no_code]}) assert e.value.status == 422 and e.value.detail == "версия 1: code" def test_versions_not_claimed(api): # команда queued → 409 not_claimed; несуществующая → 404 (row,) = api.rows("SELECT id FROM command WHERE type = 'wiki_sync'") with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": row["id"], "versions": [version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z")]}) assert e.value.status == 409 and e.value.error == "not_claimed" with pytest.raises(HubError) as e: api.wiki.post("/agent/wiki/versions", {"command_id": 999999, "versions": [version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z")]}) assert e.value.status == 404 def test_versions_wrong_agent(api): # токеном агента jira (api.jira) → 401 cmd = api.claim() with pytest.raises(HubError) as e: api.jira.post("/agent/wiki/versions", {"command_id": cmd["id"], "versions": [version("DOC-3", "DOCH-1", "2026-10-27T10:00:00Z")]}) assert e.value.status == 401 @pytest.mark.parametrize("raw,expected", [ ("2026-10-27T23:48:02.123456+03:00", "2026-10-27T20:48:02+00:00"), ("2026-10-27T20:48:02Z", "2026-10-27T20:48:02+00:00"), ("2026-10-27T23:48:02+0300", "2026-10-27T20:48:02+00:00"), ("2026-10-27T23:48:02", None), # без смещения ("не дата", None), (None, None), (1234, None), ]) def test_history_utc(raw, expected): assert history.utc(raw) == expected