baton/tests/test_structure.py

138 lines
4.4 KiB
Python
Raw Normal View History

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
ADR_PREFIXES = ["ADR-001", "ADR-002", "ADR-003", "ADR-004"]
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})"
)