diff --git a/src/jira_hub/mirror.py b/src/jira_hub/mirror.py index cd97480..a8961ef 100644 --- a/src/jira_hub/mirror.py +++ b/src/jira_hub/mirror.py @@ -95,20 +95,24 @@ def set_note(conn, key, fields: dict, updated_by: str) -> dict: unknown = set(fields) - set(NOTE_FIELDS) if unknown: raise ValueError(f"неизвестные поля заметки: {', '.join(sorted(unknown))}") - cur = get_note(conn, key) or {"key": key} - cur.update(fields) - cur["updated_at"], cur["updated_by"] = db.now(), updated_by - cols = ["key"] + NOTE_FIELDS + ["updated_at", "updated_by"] - conn.execute(f"INSERT INTO issue_note({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))}) " - "ON CONFLICT(key) DO UPDATE SET " + ", ".join(f"{c} = excluded.{c}" for c in cols[1:]), - [cur.get(c) for c in cols]) + with db.transaction(conn): # чтение, слияние и запись — атомарно (соединение общее для потоков) + cur = get_note(conn, key) or {"key": key} + cur.update(fields) + cur["updated_at"], cur["updated_by"] = db.now(), updated_by + cols = ["key"] + NOTE_FIELDS + ["updated_at", "updated_by"] + conn.execute(f"INSERT INTO issue_note({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))}) " + "ON CONFLICT(key) DO UPDATE SET " + ", ".join(f"{c} = excluded.{c}" for c in cols[1:]), + [cur.get(c) for c in cols]) return get_note(conn, key) def rename_note(conn, old_key, new_key) -> None: """После создания задачи: заметка с временного ключа переезжает на настоящий (старая запись под новым ключом, если вдруг была, уступает место).""" - if get_note(conn, old_key) is None: + if old_key == new_key: return - conn.execute("DELETE FROM issue_note WHERE key = ?", (new_key,)) - conn.execute("UPDATE issue_note SET key = ? WHERE key = ?", (new_key, old_key)) + with db.transaction(conn): + if get_note(conn, old_key) is None: + return + conn.execute("DELETE FROM issue_note WHERE key = ?", (new_key,)) + conn.execute("UPDATE issue_note SET key = ? WHERE key = ?", (new_key, old_key)) diff --git a/tests/test_mirror.py b/tests/test_mirror.py index d281b5c..d825f62 100644 --- a/tests/test_mirror.py +++ b/tests/test_mirror.py @@ -69,3 +69,26 @@ def test_list_issues_filters(): assert [i["key"] for i in mirror.list_issues(conn, {"q": "A-2"})] == ["A-2"] assert mirror.list_issues(conn, {"assignee": "ivanov"})[0]["work_kind"] is None assert len(mirror.list_issues(conn, None, limit=1)) == 1 + + +def test_rename_note_same_key_keeps_note(): + conn = db.connect(":memory:") + mirror.apply_state(conn, [issue("A-1")], []) + mirror.set_note(conn, "A-1", {"fact_status": "В работе"}, "x") + mirror.rename_note(conn, "A-1", "A-1") + assert mirror.get_note(conn, "A-1")["fact_status"] == "В работе" + + +def test_rename_note_missing_is_noop(): + conn = db.connect(":memory:") + mirror.rename_note(conn, "new:404", "A-9") # заметки нет — не падает + assert mirror.get_note(conn, "new:404") is None + assert mirror.get_note(conn, "A-9") is None + + +def test_set_note_partial_keeps_other_fields(): + conn = db.connect(":memory:") + mirror.set_note(conn, "A-1", {"work_kind": "разработка", "done_text": "сделано"}, "x") + got = mirror.set_note(conn, "A-1", {"fact_status": "Готово"}, "y") + assert got["work_kind"] == "разработка" and got["done_text"] == "сделано" + assert got["fact_status"] == "Готово" and got["updated_by"] == "y"