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([ {"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Т"}}, {"type": "ensure_link", "params": {"source": "new:60", "target": "A-1", "link_type": "связанные"}}, {"type": "refresh", "params": {"keys": ["A-1", "new:60"]}}, ]) assert [c["type"] for c in cmds] == ["ensure_issue", "ensure_link", "refresh"] assert commands.referenced_keys(cmds[1]) == ["new:60", "A-1"] assert commands.temp_keys_used(cmds[1]) == ["new:60"] assert commands.temp_keys_used(cmds[0]) == [] assert commands.temp_keys_used(cmds[2]) == ["new:60"] @pytest.mark.parametrize("bad, msg", [ ([], "без команд"), ([{"type": "ensure_all"}], "неизвестный тип"), ([{"type": "ensure_fields", "params": {"key": "A-1"}}], "без полей"), ([{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23", "foo": 1}}], "лишние параметры"), ([{"type": "ensure_status", "params": {"key": "A-1"}}], "нет target"), ([{"type": "refresh", "params": {}}], "без keys и jql"), ([{"type": "ensure_link", "params": {"source": "new:3", "target": "A-1", "link_type": "связанные"}}], "не объявлен"), ([{"type": "ensure_issue", "params": {"temp_key": "A-1", "issue_type": "task", "summary": "x"}}], "new:"), ([{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "x"}}], "task или epic"), ([{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "x"}}, {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "y"}}], "уже объявлен"), ]) def test_validate_errors(bad, msg): with pytest.raises(CommandError, match=msg): commands.validate(bad) def test_substitute_and_estimate(): p = commands.substitute("ensure_link", {"source": "new:60", "target": "A-1", "link_type": "связанные"}, {"new:60": "A-9"}) assert p["source"] == "A-9" p = commands.substitute("refresh", {"keys": ["new:60", "A-1"]}, {"new:60": "A-9"}) assert p["keys"] == ["A-9", "A-1"] p = commands.substitute("ensure_issue", {"temp_key": "new:60", "issue_type": "task", "summary": "x", "epic_key": "new:2"}, {"new:2": "A-7", "new:60": "A-9"}) assert p["temp_key"] == "new:60" and p["epic_key"] == "A-7" assert commands.estimate(["ensure_fields", "ensure_comment", "refresh"], {"ensure_fields": 120, "ensure_comment": 30, "refresh": 2}) == 152 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", "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 LT = {"связанные": "ASUPR_Связанные", "блокирует начало": "ASUPR_Блокер_Начала"} @pytest.mark.parametrize("cmd, expected", [ ({"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23", "priority": "Высокий"}}, True), ({"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-24"}}, False), ({"type": "ensure_fields", "params": {"key": "A-1", "labels_add": ["kind-dev"], "labels_remove": ["fact-done"]}}, True), ({"type": "ensure_fields", "params": {"key": "A-1", "labels_remove": ["kind-dev"]}}, False), ({"type": "ensure_status", "params": {"key": "A-1", "target": ["Разработка", "Готово"]}}, True), ({"type": "ensure_status", "params": {"key": "A-1", "target": "Разработка"}}, False), ({"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, True), ({"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "блокирует начало"}}, False), ({"type": "ensure_link", "params": {"source": "A-2", "target": "A-1", "link_type": "связанные"}}, False), ({"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}, True), ({"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "backlog"}}, False), ({"type": "ensure_comment", "params": {"key": "A-1", "marker": "m", "body": "b"}}, False), ({"type": "refresh", "params": {"keys": ["A-1"]}}, False), ({"type": "ensure_fields", "params": {"key": "A-404", "duedate": "2026-10-23"}}, False), ]) def test_check_confirmed(cmd, expected): ok, _ = commands.check_confirmed(_mirror(), cmd, LT) assert ok is expected def test_ensure_link_reverse_direction_with_both_keys_in_mirror(): # A-1 -> A-2 есть, спрашиваем A-2 -> A-1 при обоих ключах в зеркале: обратная связь — конфликт conn = _mirror() 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, reporter="petrov") assert ok and result == {"key": "A-1"} ok, _ = commands.check_confirmed(conn, _ensure_issue("Другая"), LT, reporter="petrov") assert not ok def test_ensure_issue_without_reporter_never_confirmed(): """R-11: пока автор неизвестен, совпадение темы не подтверждает ensure_issue.""" conn = _mirror() conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'") assert commands.check_confirmed(conn, _ensure_issue("Тема"), LT) == (False, None) assert commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter=None) == (False, None) 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, reporter="petrov") 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"} def test_validate_rejects_duplicate_issue_summary(): cmds = [{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Отчёт"}}, {"type": "ensure_issue", "params": {"temp_key": "new:2", "issue_type": "task", "summary": " Отчёт "}}, {"type": "ensure_issue", "params": {"temp_key": "new:3", "issue_type": "task", "summary": "отчёт"}}] with pytest.raises(commands.CommandError) as e: commands.validate(cmds) assert "повторяется тема ensure_issue: «Отчёт»" in str(e.value) commands.validate([cmds[0], cmds[2]]) # регистр различается — пакет допустим