sdlc-hub/tests/test_commands.py
2026-10-13 22:48:57 +04:00

126 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:<n>"),
([{"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_ensure_issue_never_confirmed_by_state():
"""bridge-reliability, решение 4: потерянный отчёт 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)
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]]) # регистр различается — пакет допустим