Files
6krrt/tests/test_sql_time_windows.py
adlee-was-taken 6b0c3e3e85 test: tidy the julianday window fix
Match the SQL indent of the two edited lines in metrics.py to their
neighbours (one space too deep, which made the diff noisier than the change),
drop test_src_has_no_unexpected_hits from the tripwire (an identical copy of
test_no_iso_t_datetime_comparison misfiled under the positive controls), and
replace an em dash in a comment with ASCII.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa
2026-10-05 21:32:12 -04:00

137 lines
4.7 KiB
Python

"""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)