From ccf519c9e74e41424dc708353b12e90496af3b87 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 22:03:59 +0400 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20ensure=5Flin?= =?UTF-8?q?k=20=D1=81=20=D0=BA=D0=BE=D0=BD=D1=84=D0=BB=D0=B8=D0=BA=D1=82?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B8=20ensure=5Fissue=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D1=83=20=D0=B8=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_confirmed для ensure_link: связь есть и нет конфликтов (другой тип между теми же задачами или обратное направление любого типа). check_confirmed для ensure_issue: необязательный reporter, сужает поиск по теме до задач этого автора. Тесты: конфликты, обратное направление при обоих ключах в зеркале, давность 7 дней, фильтр по reporter, выбор самой новой из двух. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- src/jira_hub/commands.py | 21 +++++++++--- tests/test_commands.py | 71 +++++++++++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/jira_hub/commands.py b/src/jira_hub/commands.py index 8dfcdd5..913f558 100644 --- a/src/jira_hub/commands.py +++ b/src/jira_hub/commands.py @@ -106,14 +106,19 @@ def estimate(types, pacing: dict) -> int: return sum(int(pacing.get(t, 0)) for t in types) -def check_confirmed(conn, cmd, link_types: dict) -> tuple[bool, dict | None]: - """Виден ли желаемый результат в зеркале. ensure_comment и refresh по состоянию не подтверждаются.""" +def check_confirmed(conn, cmd, link_types: dict, reporter: str | None = None) -> tuple[bool, dict | None]: + """Виден ли желаемый результат в зеркале. ensure_comment и refresh по состоянию не подтверждаются. + reporter: если задан, ensure_issue подтверждается только задачей, заведённой этим пользователем.""" ctype, p = cmd["type"], cmd["params"] if ctype in ("ensure_comment", "refresh"): return False, None if ctype == "ensure_issue": - row = conn.execute("SELECT key FROM issue WHERE summary = ? AND substr(created, 1, 10) >= date('now', '-7 days') " - "ORDER BY created DESC LIMIT 1", (p["summary"],)).fetchone() + sql = "SELECT key FROM issue WHERE summary = ? AND substr(created, 1, 10) >= date('now', '-7 days')" + args = [p["summary"]] + if reporter is not None: + sql += " AND reporter = ?" + args.append(reporter) + row = conn.execute(sql + " ORDER BY created DESC LIMIT 1", args).fetchone() return (True, {"key": row["key"]}) if row else (False, None) issue = mirror.get_issue(conn, p.get("key") or p.get("source")) if issue is None: @@ -132,8 +137,14 @@ def check_confirmed(conn, cmd, link_types: dict) -> tuple[bool, dict | None]: final = target[-1] if isinstance(target, list) else target return issue["status"] == final, None if ctype == "ensure_link": + # связь есть И конфликтов нет: другой тип между теми же задачами или обратное направление любого типа jira_type = link_types.get(p["link_type"], p["link_type"]) - return any(l["target_key"] == p["target"] and l["link_type"] == jira_type for l in issue["links_out"]), None + all_types = set(link_types.values()) + present = any(l["target_key"] == p["target"] and l["link_type"] == jira_type for l in issue["links_out"]) + other_type = any(l["target_key"] == p["target"] and l["link_type"] in all_types - {jira_type} + for l in issue["links_out"]) + reverse = any(l["source_key"] == p["target"] and l["link_type"] in all_types for l in issue["links_in"]) + return present and not other_type and not reverse, None if ctype == "ensure_sprint": want = None if p["sprint"] == "backlog" else p["sprint"] return (issue.get("sprint") or None) == want, None diff --git a/tests/test_commands.py b/tests/test_commands.py index 263f03e..6518e5a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,6 +3,8 @@ import pytest from jira_hub import commands, db, mirror from jira_hub.commands import CommandError +NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')" + def test_validate_ok_and_tail_keys(): cmds = commands.validate([ @@ -48,10 +50,12 @@ def test_substitute_and_estimate(): def _mirror(): conn = db.connect(":memory:") - mirror.apply_state(conn, [{"key": "A-1", "summary": "Тема", "status": "Готово", "priority": "Высокий", - "duedate": "2026-10-23", "labels": ["AWPCA", "kind-dev"], "sprint": "Спринт 2", - "created": "2026-10-10T10:00:00.000+0300", "assignee": "ivanov"}], - [{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Связанные"}]) + mirror.apply_state(conn, [ + {"key": "A-1", "summary": "Тема", "status": "Готово", "priority": "Высокий", + "duedate": "2026-10-23", "labels": ["AWPCA", "kind-dev"], "sprint": "Спринт 2", + "created": "2026-10-10T10:00:00.000+0300", "assignee": "ivanov", "reporter": "petrov"}, + {"key": "A-2", "summary": "Вторая", "status": "Разработка", "created": "2026-10-10T11:00:00.000+0300"}, + ], [{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Связанные"}]) return conn @@ -79,10 +83,61 @@ def test_check_confirmed(cmd, expected): assert ok is expected -def test_check_confirmed_issue_by_summary(monkeypatch): +def test_ensure_link_reverse_direction_with_both_keys_in_mirror(): + # A-1 -> A-2 есть, спрашиваем A-2 -> A-1 при обоих ключах в зеркале: обратная связь — конфликт conn = _mirror() - conn.execute("UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now') WHERE key = 'A-1'") - ok, result = commands.check_confirmed(conn, {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Тема"}}, LT) + ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-2", "target": "A-1", "link_type": "связанные"}}, LT) + assert ok is False + + +def test_ensure_link_conflict_other_type_blocks_confirmation(): + conn = _mirror() + conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-1', 'A-2', 'ASUPR_Блокер_Начала')") + ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT) + assert ok is False + + +def test_ensure_link_conflict_reverse_blocks_confirmation(): + conn = _mirror() + conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-2', 'A-1', 'ASUPR_Связанные')") + ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT) + assert ok is False + + +def _ensure_issue(summary): + return {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": summary}} + + +def test_check_confirmed_issue_by_summary(): + conn = _mirror() + conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'") + ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT) assert ok and result == {"key": "A-1"} - ok, _ = commands.check_confirmed(conn, {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Другая"}}, LT) + ok, _ = commands.check_confirmed(conn, _ensure_issue("Другая"), LT) assert not ok + + +def test_ensure_issue_older_than_7_days_not_confirmed(): + conn = _mirror() + conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'") + conn.execute(f"UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now', '-30 days') WHERE key = 'A-1'") + ok, _ = commands.check_confirmed(conn, _ensure_issue("Тема"), LT) + assert ok is False + + +def test_ensure_issue_reporter_filter(): + conn = _mirror() + conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'") + ok, _ = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="ivanov") + assert ok is False + ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov") + assert ok and result == {"key": "A-1"} + + +def test_ensure_issue_two_matches_takes_newest(): + conn = _mirror() + mirror.apply_state(conn, [{"key": "A-3", "summary": "Тема", "reporter": "petrov"}], []) + conn.execute(f"UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now', '-1 day') WHERE key = 'A-1'") + conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-3'") + ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov") + assert ok and result == {"key": "A-3"}