"""The note staleness sweep (milestone 317 step 3). The query is a sibling of the rules sweep, not a shared implementation — a rule scopes by rulebook ownership, a note by the note ACL, so only the BEHAVIOUR is common (services/verification). These pin the parts that would fail silently rather than loudly: the ordering, which rows are eligible, and the asymmetry of a failed check. """ from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from tests.helpers import compiled_sql, fake_note, make_mock_session async def _sweep_sql(**kwargs): """The statement the sweep builds, as SQL text. No database: the SHAPE of the query is what is under test, and the ordering clause in particular cannot be checked any other way without one. The await happens INSIDE the patch — a coroutine created in the block and awaited outside it would run against the real session. """ captured = {} session = make_mock_session() async def _execute(stmt): try: captured["sql"] = compiled_sql(stmt) except Exception: # Literal-rendering a datetime bind is dialect-dependent and can # raise. Only the tests asserting on a literal value (user_id, # project_id) need that form, and none of those build a cutoff. captured["sql"] = str(stmt) result = MagicMock() result.scalars.return_value.all.return_value = [] return result session.execute = _execute with patch("scribe.services.notes.async_session") as cls: cls.return_value = session from scribe.services.notes import notes_due_for_verification await notes_due_for_verification(user_id=7, **kwargs) return captured["sql"] def _where(sql: str) -> str: """Just the WHERE clause. `select(Note)` names every column, so searching the whole statement for a column name always finds the SELECT list — which is how one of these tests first failed for the wrong reason. Matched by regex rather than split on a literal, because the exact whitespace SQLAlchemy emits around WHERE is not something a test should depend on. """ import re m = re.search(r"\bWHERE\b(.*?)(?:\bORDER BY\b|$)", sql, re.S) assert m, "no WHERE clause — the sweep must never select the whole table" return m.group(1) # ── the ordering, which is the whole signal ────────────────────────────────── @pytest.mark.asyncio async def test_never_checked_sorts_first_not_last(): """THE thing most likely to be got wrong, and it would not error. Postgres sorts NULLs LAST on ASC by default, so the obvious `ORDER BY verified_at ASC` sinks every never-checked note below every checked one — exactly inverting the signal the sweep exists to carry. A note nobody has ever confirmed is a claim with no evidence behind it at all. """ sql = await _sweep_sql() assert "ORDER BY notes.verified_at ASC NULLS FIRST" in sql @pytest.mark.asyncio async def test_the_order_is_total(): """A tiebreak, so two notes verified in the same transaction do not swap places between calls and make a page boundary lie. Asserted on the END of the statement, not by searching it: `select(Note)` names every column, so the first `notes.id` in the text is the SELECT list, not the ORDER BY.""" sql = (await _sweep_sql()).rstrip() assert sql.endswith("ORDER BY notes.verified_at ASC NULLS FIRST, notes.id") # ── which rows are eligible ────────────────────────────────────────────────── @pytest.mark.asyncio async def test_only_notes_that_carry_a_check_appear(): """A note with no `verify_with` is not overdue — it is a decision, and listing it would dilute the result until nobody reads it.""" assert "notes.verify_with IS NOT NULL" in await _sweep_sql() @pytest.mark.asyncio async def test_trashed_notes_are_excluded(): assert "notes.deleted_at IS NULL" in await _sweep_sql() @pytest.mark.asyncio async def test_the_scope_is_browse_not_read(): """Decision note 2094: a sweep is a PASSIVE surface, so a record shared one-to-one must not arrive in one unasked. `note_shares` is the tell — its presence would mean the read scope leaked in.""" sql = await _sweep_sql() assert "notes.user_id = 7" in sql assert "project_shares" in sql assert "note_shares" not in sql @pytest.mark.asyncio async def test_a_task_carrying_a_check_is_NOT_filtered_out(): """Deliberate. The write path permits a check on nothing but a plain note, so such a row would be in an ILLEGAL state — and this is the one surface that could tell somebody. Hiding it to match the invariant would make the sweep agree with a database it had stopped describing.""" where = _where(await _sweep_sql()) assert "notes.status" not in where assert "notes.note_type" not in where # ── the filters ────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_never_only_narrows_to_the_unexamined(): assert "notes.verified_at IS NULL" in await _sweep_sql(never_only=True) @pytest.mark.asyncio async def test_an_age_window_still_includes_the_never_checked(): """They are the most overdue thing there is; a window that excluded them would answer the opposite of the question.""" sql = await _sweep_sql(older_than_days=30) assert "notes.verified_at IS NULL" in sql assert "notes.verified_at <" in sql @pytest.mark.asyncio async def test_never_only_wins_over_an_age_window(): """Both together is a caller contradicting themselves; the narrower one is the safe reading, and it must not emit a cutoff as well.""" sql = await _sweep_sql(never_only=True, older_than_days=30) assert "notes.verified_at <" not in sql @pytest.mark.asyncio async def test_a_project_filter_narrows(): assert "notes.project_id = 4" in await _sweep_sql(project_id=4) @pytest.mark.asyncio async def test_a_negative_window_raises_rather_than_meaning_everything(): """Silently answering a different question is the failure this guards.""" with pytest.raises(ValueError, match="older_than_days"): await _sweep_sql(older_than_days=-1) # ── the stamp ──────────────────────────────────────────────────────────────── async def _mark(note, still_true=True, writable=True): session = make_mock_session() result = MagicMock() result.scalars.return_value.first.return_value = note session.execute = AsyncMock(return_value=result) with patch("scribe.services.notes.async_session") as cls, \ patch("scribe.services.access.can_write_note", AsyncMock(return_value=writable)): cls.return_value = session from scribe.services.notes import mark_note_verified return await mark_note_verified(note_id=1, user_id=7, still_true=still_true), session @pytest.mark.asyncio async def test_a_passing_check_stamps_the_note(): note = fake_note(verify_with="curl the docs", verified_at=None) out, session = await _mark(note) assert out is note assert note.verified_at is not None session.commit.assert_awaited() @pytest.mark.asyncio async def test_a_failing_check_writes_nothing(): """The asymmetry IS the design. There is no "verified false" state, because a note whose check failed is not in a special condition — it is WRONG. Recording the failure as a flag would let it sit there being false with the sweep quietly satisfied that somebody had looked.""" note = fake_note(verify_with="curl the docs", verified_at=None) out, session = await _mark(note, still_true=False) assert out is note assert note.verified_at is None, "a failed check must leave the stamp alone" session.commit.assert_not_awaited() @pytest.mark.asyncio async def test_a_note_with_no_check_cannot_be_verified(): """Nothing to verify is a different answer from verified.""" note = fake_note(verify_with=None) out, _ = await _mark(note) assert out is None @pytest.mark.asyncio async def test_a_reader_cannot_stamp(): """Stamping is a mutation (rules 47/78) — an editor-share holder may make it, a viewer may not.""" note = fake_note(verify_with="curl the docs", verified_at=None) out, _ = await _mark(note, writable=False) assert out is None assert note.verified_at is None # ── the row ────────────────────────────────────────────────────────────────── def test_the_row_carries_the_check_in_full(): """The opposite call from a listing: the caller is about to go and run it, so the text IS the payload rather than the bloat.""" note = fake_note( id=9, title="Versioning", project_id=2, verify_with="curl the AMO docs", expires_when="AMO allows re-signing", verified_at=datetime.now(timezone.utc) - timedelta(days=74), ) from scribe.services.notes import verification_row row = verification_row(note) assert row["verify_with"] == "curl the AMO docs" assert row["expires_when"] == "AMO allows re-signing" assert row["days_since_verified"] == 74 def test_a_never_checked_row_says_never_not_none(): """None means "this is a decision, the question does not apply"; "never" means "it asserts a fact and nobody has confirmed it". The second is the one worth acting on, and collapsing them loses the sweep's point.""" from scribe.services.notes import verification_row row = verification_row(fake_note(verify_with="a check", verified_at=None)) assert row["last_verified"] == "never" assert row["days_since_verified"] is None