"""Tripwire: detect ISO-T vs datetime('now', ...) comparisons in src/*.py. SQLite ``datetime()`` returns a space-separated string (``2026-08-23 00:18:04``). Columns in this codebase are written by ``datetime.now(timezone.utc).isoformat()``, so they carry a ``T`` separator (``2026-08-23T00:20:04.577131+00:00``). Comparing them with ``>=`` / ``<=`` / ``>`` / ``<`` is a silent bug: ``T > ' '``, so once the dates match the time-of-day never participates and the window becomes "everything today". Use ``julianday()`` on both sides of the comparison instead. """ from __future__ import annotations import re from pathlib import Path import pytest SRC = Path(__file__).resolve().parent.parent / "src" _COLUMNS = ( "observed_at|ticked_at|created_at|opened_at|" "last_fired_at|resolved_at|updated_at" ) # Column on the left: observed_at >= datetime('now', ...) _RE_COL_OP_NOW = re.compile( rf"\b(?:{_COLUMNS})\s*(?:>=|<=|>|<)\s*" rf"datetime\(\s*'now'" ) # Column on the right: datetime('now', ...) <= observed_at _RE_NOW_OP_COL = re.compile( rf"datetime\(\s*'now'[^)]*\)\s*(?:>=|<=|>|<)\s*" rf"\b(?:{_COLUMNS})\b" ) _FAIL_MSG = ( "compare with julianday(col) >= julianday('now', ...); " "the column is ISO with a T, " "datetime() returns a space, " "and 'T' > ' ' makes the time of day never participate." ) def _iter_source_files() -> list[Path]: """Return all .py files under src/, sorted.""" return sorted(SRC.rglob("*.py")) # --------------------------------------------------------------------------- # Tripwire # --------------------------------------------------------------------------- def test_no_iso_t_datetime_comparison(): """Fail if any src/*.py file compares an ISO-T column against datetime('now', ...) with an operator.""" hits: list[tuple[Path, int, str]] = [] for fpath in _iter_source_files(): text = fpath.read_text(encoding="utf-8") for lineno, line in enumerate(text.splitlines(), 1): if _RE_COL_OP_NOW.search(line) or _RE_NOW_OP_COL.search(line): hits.append((fpath, lineno, line.strip())) if hits: parts = [_FAIL_MSG, ""] for fpath, lineno, line in hits: parts.append(f" {fpath}:{lineno} {line}") pytest.fail("\n".join(parts)) # --------------------------------------------------------------------------- # Positive controls: verify the regexes themselves are correct # --------------------------------------------------------------------------- class TestRegexPositiveControls: """Regexes must match the bad pattern and reject the safe pattern.""" # --- bad pattern (column op datetime) ----------------------------------- def test_re_col_op_now_matches_forbidden_pattern(self): assert _RE_COL_OP_NOW.search( "AND observed_at >= datetime('now', '-24 hours')" ) def test_re_now_op_col_matches_forbidden_pattern(self): assert _RE_NOW_OP_COL.search( "AND datetime('now', '-24 hours') <= observed_at" ) def test_re_col_op_now_matches_gt(self): assert _RE_COL_OP_NOW.search("WHERE observed_at > datetime('now')") def test_re_col_op_now_matches_lt(self): assert _RE_COL_OP_NOW.search("WHERE ticked_at < datetime('now')") def test_re_now_op_col_matches_ge(self): assert _RE_NOW_OP_COL.search( "datetime('now') >= opened_at" ) # --- safe pattern (julianday) ------------------------------------------- def test_re_col_op_now_rejects_julianday(self): assert not _RE_COL_OP_NOW.search( "AND julianday(observed_at) >= julianday('now', '-24 hours')" ) def test_re_now_op_col_rejects_julianday(self): assert not _RE_NOW_OP_COL.search( "AND julianday(observed_at) >= julianday('now', '-24 hours')" ) def test_both_regexes_reject_julianday_all_columns(self): """Spot-check a few column names with julianday wrappers.""" for col in ("observed_at", "created_at", "updated_at", "last_fired_at", "resolved_at", "ticked_at", "opened_at"): sql = ( f"AND julianday({col}) >= " f"julianday('now', '-24 hours')" ) assert not _RE_COL_OP_NOW.search(sql), f"col side failed for {col}" assert not _RE_NOW_OP_COL.search(sql), f"now side failed for {col}" # --- regression: docstring must not trip the scan ----------------------- def test_docstring_does_not_trip(self): """The docstring near dispatcher.py:3187 mentions both names without an operator between them.""" line = "``datetime('now', ...)``. ``observed_at`` is written by" assert not _RE_COL_OP_NOW.search(line) assert not _RE_NOW_OP_COL.search(line)