""" Tests for BATON-ARCH-014: Доработать ADR-002 и ADR-004 по замечаниям ревью. Acceptance criteria: 1. docs/adr/ADR-002-offline-pattern.md существует в docs/adr/. 2. ADR-002: заголовок содержит «ADR-002». 3. ADR-002: дата 2026-03-20 присутствует. 4. ADR-002: статус «Accepted». 5. ADR-002: секция «Open Questions» присутствует. 6. ADR-002: Open Questions содержит вопрос о #1001 и BackgroundSync 78.75%. 7. ADR-002: Open Questions содержит ACTION item с отсылкой на #1049. 8. ADR-004: пункт о 429 содержит «exponential backoff» и ссылку на «#1046». 9. ARCHITECTURE.md: фраза «ADR-файлы хранятся в `docs/adr/`.» стоит после заголовка «## Ссылки на ADR» и перед таблицей. 10. ARCHITECTURE.md: таблица ADR содержит строку с ADR-002. """ from __future__ import annotations import re from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent ADR_DIR = PROJECT_ROOT / "docs" / "adr" ADR_002 = ADR_DIR / "ADR-002-offline-pattern.md" ADR_004 = ADR_DIR / "ADR-004-telegram-strategy.md" ARCHITECTURE_MD = PROJECT_ROOT / "ARCHITECTURE.md" # --------------------------------------------------------------------------- # Criterion 1 — ADR-002 file existence # --------------------------------------------------------------------------- def test_adr_002_offline_pattern_file_exists() -> None: """docs/adr/ADR-002-offline-pattern.md должен существовать.""" assert ADR_002.is_file(), ( f"Файл ADR-002-offline-pattern.md не найден в {ADR_DIR}" ) # --------------------------------------------------------------------------- # Criterion 2 — ADR-002 header # --------------------------------------------------------------------------- def test_adr_002_header_contains_adr_002() -> None: """Заголовок ADR-002 должен содержать идентификатор «ADR-002».""" content = ADR_002.read_text(encoding="utf-8") assert "ADR-002" in content, ( "ADR-002-offline-pattern.md не содержит идентификатор «ADR-002» в заголовке" ) # --------------------------------------------------------------------------- # Criterion 3 — ADR-002 date # --------------------------------------------------------------------------- def test_adr_002_contains_date_2026_03_20() -> None: """ADR-002 должен содержать дату «2026-03-20».""" content = ADR_002.read_text(encoding="utf-8") assert "2026-03-20" in content, ( "ADR-002-offline-pattern.md не содержит дату 2026-03-20" ) # --------------------------------------------------------------------------- # Criterion 4 — ADR-002 status # --------------------------------------------------------------------------- def test_adr_002_status_is_accepted() -> None: """ADR-002 должен иметь статус «Accepted».""" content = ADR_002.read_text(encoding="utf-8") assert "Accepted" in content, ( "ADR-002-offline-pattern.md не содержит статус «Accepted»" ) # --------------------------------------------------------------------------- # Criterion 5 — ADR-002 Open Questions section # --------------------------------------------------------------------------- def test_adr_002_has_open_questions_section() -> None: """ADR-002 должен содержать секцию «Open Questions».""" content = ADR_002.read_text(encoding="utf-8") assert "Open Questions" in content, ( "ADR-002-offline-pattern.md не содержит секцию «Open Questions»" ) # --------------------------------------------------------------------------- # Criterion 6 — Open Questions: #1001 and BackgroundSync 78.75% # --------------------------------------------------------------------------- def test_adr_002_open_questions_references_decision_1001() -> None: """Open Questions ADR-002 должен ссылаться на решение #1001.""" content = ADR_002.read_text(encoding="utf-8") assert "#1001" in content, ( "ADR-002-offline-pattern.md Open Questions не содержит ссылку на решение #1001" ) def test_adr_002_open_questions_mentions_backgroundsync_coverage() -> None: """Open Questions ADR-002 должен упоминать покрытие BackgroundSync 78.75%.""" content = ADR_002.read_text(encoding="utf-8") assert "78.75" in content, ( "ADR-002-offline-pattern.md Open Questions не содержит покрытие «78.75%» " "(BackgroundSync, caniuse март 2026)" ) # --------------------------------------------------------------------------- # Criterion 7 — Open Questions: ACTION item with #1049 # --------------------------------------------------------------------------- def test_adr_002_open_questions_has_action_item() -> None: """Open Questions ADR-002 должен содержать явный ACTION item (конвенция #1049).""" content = ADR_002.read_text(encoding="utf-8") # Конвенция #1049: строки Open Questions с устаревшими решениями должны содержать ACTION: assert re.search(r"ACTION:", content), ( "ADR-002-offline-pattern.md Open Questions не содержит маркер «ACTION:» " "— нарушение конвенции #1049" ) def test_adr_002_action_item_references_decision_1049() -> None: """ACTION item в ADR-002 должен ссылаться на конвенцию #1049.""" content = ADR_002.read_text(encoding="utf-8") assert "#1049" in content, ( "ADR-002-offline-pattern.md не содержит ссылку на конвенцию #1049 в ACTION item" ) # --------------------------------------------------------------------------- # Criterion 8 — ADR-004: exponential backoff + #1046 # --------------------------------------------------------------------------- def test_adr_004_retry_after_mentions_exponential_backoff() -> None: """Пункт о 429 в ADR-004 должен упоминать «exponential backoff».""" content = ADR_004.read_text(encoding="utf-8") # Проверяем, что "exponential backoff" присутствует в контексте retry_after retry_section = re.search( r"retry_after[^\n]*", content, re.IGNORECASE ) assert retry_section is not None, ( "ADR-004 не содержит строки с упоминанием retry_after" ) # Ищем exponential backoff в пределах абзаца о 429 para_429 = re.search( r"(?:429|retry_after)[^\n]*(?:\n[^\n]+)*", content, re.IGNORECASE, ) assert para_429 is not None assert "exponential backoff" in para_429.group(0).lower(), ( "Пункт о retry_after/429 в ADR-004 не содержит «exponential backoff» — " "требуется дополнение согласно решению #1046" ) def test_adr_004_exponential_backoff_references_decision_1046() -> None: """Упоминание exponential backoff в ADR-004 должно ссылаться на решение #1046.""" content = ADR_004.read_text(encoding="utf-8") assert "#1046" in content, ( "ADR-004 не содержит ссылку на решение #1046 рядом с exponential backoff" ) # --------------------------------------------------------------------------- # Criterion 9 — ARCHITECTURE.md: intro sentence in ADR section # --------------------------------------------------------------------------- def test_architecture_md_adr_section_has_intro_sentence() -> None: """Секция «Ссылки на ADR» в ARCHITECTURE.md должна начинаться с вводной фразы о пути docs/adr/.""" content = ARCHITECTURE_MD.read_text(encoding="utf-8") # Ищем вводную фразу непосредственно после заголовка pattern = re.compile( r"##\s+Ссылки на ADR\s*\n+(?:[^\n]*\n)*?.*ADR-файлы хранятся в `docs/adr/`", re.MULTILINE, ) assert pattern.search(content), ( "ARCHITECTURE.md: секция «Ссылки на ADR» не содержит вводную фразу " "«ADR-файлы хранятся в `docs/adr/`.»" ) # --------------------------------------------------------------------------- # Criterion 10 — ARCHITECTURE.md: ADR-002 row in table # --------------------------------------------------------------------------- def test_architecture_md_adr_table_contains_adr_002_row() -> None: """Таблица ADR в ARCHITECTURE.md должна содержать строку для ADR-002.""" content = ARCHITECTURE_MD.read_text(encoding="utf-8") # Ищем строку таблицы с ADR-002 и ссылкой на файл assert re.search( r"\|\s*\[ADR-002\]\(docs/adr/ADR-002-offline-pattern\.md\)", content ), ( "ARCHITECTURE.md не содержит строки таблицы с " "[ADR-002](docs/adr/ADR-002-offline-pattern.md)" ) def test_architecture_md_adr_002_row_has_accepted_status() -> None: """Строка ADR-002 в таблице ARCHITECTURE.md должна иметь статус Accepted.""" content = ARCHITECTURE_MD.read_text(encoding="utf-8") row_match = re.search( r"\|\s*\[ADR-002\].*?\|\s*([^|]+)\|\s*(Accepted|Superseded|Draft)\s*\|", content, ) assert row_match, ( "Строка ADR-002 в таблице ARCHITECTURE.md не найдена или не содержит поля статуса" ) assert "Accepted" in row_match.group(0), ( "Статус строки ADR-002 в ARCHITECTURE.md должен быть «Accepted»" )