CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 15s
Docstrings, section headers and assertion messages drop the record number, milestone or step that introduced the behaviour, and incident accounts are restated as the present-tense failure shape. A dead pointer to a design doc that is not in the repo is removed. Renamed to the property they check: test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note Fixture and example ids stay: "note 5" in a supersession chain, "rule 156" in an illustrative question, `#2275` as the hex-versus-citation case, `#3244` as the example of a bare number, fixed dates in date-filter tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
10 KiB
Python
243 lines
10 KiB
Python
"""The note staleness sweep.
|
|
|
|
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():
|
|
"""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 — 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
|