feat: ядро моста Jira — хаб (bridge-core, #1) #2

Merged
aep merged 37 commits from feat/1-bridge-core into main 2026-10-12 20:47:31 +03:00
2 changed files with 69 additions and 7 deletions
Showing only changes of commit acbc19c7eb - Show all commits

View File

@ -1,9 +1,10 @@
"""Разовый импорт CSV-пакета admin/jira-sync: служебные колонки → issue_note; links.csv — сверочный набор
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует."""
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует.
Пустая ячейка CSV не меняет существующее поле заметки (правки, сделанные в хабе, сохраняются)."""
import csv
from pathlib import Path
from . import mirror, settings
from . import db, mirror, settings
def _rows(path: Path) -> list[dict]:
@ -18,8 +19,20 @@ def _clean(value) -> str | None:
return value or None
def _note(conn, key, fields: dict) -> None:
"""Записать только непустые поля; если их нет — заметку не трогаем."""
fields = {k: v for k, v in fields.items() if v is not None}
if fields:
mirror.set_note(conn, key, fields, "import")
def import_package(conn, folder) -> dict:
folder = Path(folder)
with db.transaction(conn):
return _import(conn, folder)
def _import(conn, folder: Path) -> dict:
notes = 0
csv_keys = []
@ -27,10 +40,10 @@ def import_package(conn, folder) -> dict:
key = _clean(r.get("Код"))
if not key:
continue
mirror.set_note(conn, key, {
_note(conn, key, {
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
"done_text": _clean(r.get("Что сделано по факту (внутр.)")), "rest_text": _clean(r.get("Что осталось (внутр.)")),
"owner_note": _clean(r.get("Комментарий актуализации"))}, "import")
"owner_note": _clean(r.get("Комментарий актуализации"))})
notes += 1
csv_keys.append(key)
@ -41,17 +54,17 @@ def import_package(conn, folder) -> dict:
if real:
new_keys[f"new:{i}"] = real
csv_keys.append(real)
mirror.set_note(conn, key, {
_note(conn, key, {
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
"rest_text": _clean(r.get("Основание (внутр.)")),
"owner_note": None if real else _clean(r.get("Тема"))}, "import")
"owner_note": None if real else _clean(r.get("Тема"))})
notes += 1
for r in _rows(folder / "epics.csv"):
key = _clean(r.get("Ключ"))
if not key:
continue
mirror.set_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))}, "import")
_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))})
notes += 1
csv_keys.append(key)

View File

@ -50,3 +50,52 @@ def test_import_notes_and_report(tmp_path):
assert rep["missing_links"] == [{"source": "AURORA-1533", "target": "AURORA-1416", "link_type": "ASUPR_Блокер_Начала"}]
assert importer.import_package(conn, tmp_path)["notes"] == 6
assert conn.execute("SELECT COUNT(*) AS n FROM issue_note").fetchone()["n"] == 6
def write_tasks(folder, comment):
write_csv(folder / "tasks.csv",
["Код", "Тема", "Вид работы", "Статус по факту", "Комментарий актуализации",
"Что сделано по факту (внутр.)", "Что осталось (внутр.)", "Спринт"],
[["AURORA-1533", "Тест ОСВ", "тестирование", "Ожидание разработки", comment, "", "Прогнать", "Спринт 2"]])
def test_reimport_keeps_hub_edit_when_cell_empty(tmp_path):
write_tasks(tmp_path, "Актуализация 10.10.2026")
conn = db.connect(":memory:")
settings.ensure_defaults(conn)
importer.import_package(conn, tmp_path)
mirror.set_note(conn, "AURORA-1533", {"owner_note": "правка в хабе"}, "hub")
write_tasks(tmp_path, "") # пустая ячейка в CSV
importer.import_package(conn, tmp_path)
n = mirror.get_note(conn, "AURORA-1533")
assert n["owner_note"] == "правка в хабе"
assert n["rest_text"] == "Прогнать"
def test_reimport_updates_nonempty_cell(tmp_path):
write_tasks(tmp_path, "Первая версия")
conn = db.connect(":memory:")
settings.ensure_defaults(conn)
importer.import_package(conn, tmp_path)
write_tasks(tmp_path, "Вторая версия")
importer.import_package(conn, tmp_path)
assert mirror.get_note(conn, "AURORA-1533")["owner_note"] == "Вторая версия"
def test_import_does_not_touch_issue_tables(tmp_path):
make_package(tmp_path)
conn = db.connect(":memory:")
settings.ensure_defaults(conn)
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}],
[{"source_key": "AURORA-1533", "target_key": "AURORA-1416", "link_type": "x"}])
count = lambda t: conn.execute(f"SELECT COUNT(*) AS n FROM {t}").fetchone()["n"]
before = (count("issue"), count("issue_link"))
importer.import_package(conn, tmp_path)
assert (count("issue"), count("issue_link")) == before
def test_missing_csv_files_give_empty_report(tmp_path):
conn = db.connect(":memory:")
settings.ensure_defaults(conn)
rep = importer.import_package(conn, tmp_path)
assert rep == {"notes": 0, "expected_links": 0, "missing_keys": [], "missing_links": []}