2026-03-20 20:44:00 +02:00
|
|
|
"""
|
|
|
|
|
Tests for BATON-ARCH-001: Project structure verification.
|
|
|
|
|
|
|
|
|
|
Verifies that all required files and directories exist on disk,
|
|
|
|
|
and that all Python source files have valid syntax (equivalent to
|
|
|
|
|
running `python3 -m ast <file>`).
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import ast
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
# Project root: tests/ -> project root
|
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Required files (acceptance criteria)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
REQUIRED_FILES = [
|
|
|
|
|
"backend/__init__.py",
|
|
|
|
|
"backend/config.py",
|
|
|
|
|
"backend/models.py",
|
|
|
|
|
"backend/db.py",
|
|
|
|
|
"backend/telegram.py",
|
|
|
|
|
"backend/middleware.py",
|
|
|
|
|
"backend/main.py",
|
|
|
|
|
"requirements.txt",
|
|
|
|
|
"requirements-dev.txt",
|
|
|
|
|
".env.example",
|
|
|
|
|
"docs/tech_report.md",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# ADR files: matched by prefix because filenames include descriptive suffixes
|
2026-03-20 21:10:26 +02:00
|
|
|
ADR_PREFIXES = ["ADR-001", "ADR-003", "ADR-004"]
|
2026-03-20 20:44:00 +02:00
|
|
|
|
|
|
|
|
PYTHON_SOURCES = [
|
|
|
|
|
"backend/__init__.py",
|
|
|
|
|
"backend/config.py",
|
|
|
|
|
"backend/models.py",
|
|
|
|
|
"backend/db.py",
|
|
|
|
|
"backend/telegram.py",
|
|
|
|
|
"backend/middleware.py",
|
|
|
|
|
"backend/main.py",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# File existence
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("rel_path", REQUIRED_FILES)
|
|
|
|
|
def test_required_file_exists(rel_path: str) -> None:
|
|
|
|
|
"""Every file listed in the acceptance criteria must exist on disk."""
|
|
|
|
|
assert (PROJECT_ROOT / rel_path).is_file(), (
|
|
|
|
|
f"Required file missing: {rel_path}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("prefix", ADR_PREFIXES)
|
|
|
|
|
def test_adr_file_exists(prefix: str) -> None:
|
|
|
|
|
"""Each ADR document (ADR-001..004) must have a file in docs/adr/."""
|
|
|
|
|
adr_dir = PROJECT_ROOT / "docs" / "adr"
|
|
|
|
|
matches = list(adr_dir.glob(f"{prefix}*.md"))
|
|
|
|
|
assert len(matches) >= 1, (
|
|
|
|
|
f"ADR file with prefix '{prefix}' not found in {adr_dir}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Repository metadata
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_git_directory_exists() -> None:
|
|
|
|
|
""".git directory must exist — project must be a git repository."""
|
|
|
|
|
assert (PROJECT_ROOT / ".git").is_dir(), (
|
|
|
|
|
f".git directory not found at {PROJECT_ROOT}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gitignore_exists() -> None:
|
|
|
|
|
""".gitignore must be present in the project root."""
|
|
|
|
|
assert (PROJECT_ROOT / ".gitignore").is_file(), (
|
|
|
|
|
f".gitignore not found at {PROJECT_ROOT}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Python syntax validation (replaces: python3 -m ast <file>)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("rel_path", PYTHON_SOURCES)
|
|
|
|
|
def test_python_file_has_valid_syntax(rel_path: str) -> None:
|
|
|
|
|
"""Every backend Python file must parse without SyntaxError."""
|
|
|
|
|
path = PROJECT_ROOT / rel_path
|
|
|
|
|
assert path.is_file(), f"Python file not found: {rel_path}"
|
|
|
|
|
source = path.read_text(encoding="utf-8")
|
|
|
|
|
try:
|
|
|
|
|
ast.parse(source, filename=str(path))
|
|
|
|
|
except SyntaxError as exc:
|
|
|
|
|
pytest.fail(f"Syntax error in {rel_path}: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# BATON-ARCH-008: monkey-patch must live only in conftest.py
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_PATCH_MARKER = "_safe_aiosqlite_await"
|
|
|
|
|
|
|
|
|
|
_FILES_MUST_NOT_HAVE_PATCH = [
|
|
|
|
|
"tests/test_register.py",
|
|
|
|
|
"tests/test_signal.py",
|
|
|
|
|
"tests/test_webhook.py",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_monkeypatch_present_in_conftest() -> None:
|
|
|
|
|
"""conftest.py must contain the aiosqlite monkey-patch."""
|
|
|
|
|
conftest = (PROJECT_ROOT / "tests" / "conftest.py").read_text(encoding="utf-8")
|
|
|
|
|
assert _PATCH_MARKER in conftest, (
|
|
|
|
|
"conftest.py is missing the aiosqlite monkey-patch (_safe_aiosqlite_await)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("rel_path", _FILES_MUST_NOT_HAVE_PATCH)
|
|
|
|
|
def test_monkeypatch_absent_in_test_file(rel_path: str) -> None:
|
|
|
|
|
"""Test files other than conftest.py must NOT contain duplicate monkey-patch."""
|
|
|
|
|
source = (PROJECT_ROOT / rel_path).read_text(encoding="utf-8")
|
|
|
|
|
assert _PATCH_MARKER not in source, (
|
|
|
|
|
f"{rel_path} still contains a duplicate monkey-patch block ({_PATCH_MARKER!r})"
|
|
|
|
|
)
|