fix(advice): guard the advice write on the comment as well as the token
The editing token catches every edit the application makes, because upsert_reading writes a fresh token on every write. A document edited outside the application - by a script, or by hand - keeps its empty token, so a stale thread's write would still match on the gravity alone and could put advice about the old comment back. The advice is generated from the comment as well as the gravity, so the comment is now a second, independent guard on the write. A stale write can only land if BOTH the gravity and the comment are unchanged. The stored comment and the one the view sees can differ by trailing whitespace - two of the live readings are padded - so the match is a stripped, anchored regex rather than an exact one. Also: the log message said the reading 'no longer holds the gravity it was written for', but the guard is the token and the comment. It now says what actually happened. 194 tests pass.
This commit is contained in:
@@ -1121,6 +1121,47 @@ def test_stale_advice_cannot_overwrite_a_comment_only_correction(repo):
|
||||
assert doc["comment"] == "corrected comment"
|
||||
|
||||
|
||||
def test_stale_advice_cannot_overwrite_a_comment_edited_outside_the_app(repo):
|
||||
"""The comment is a guard in its own right, not just the token.
|
||||
|
||||
The token covers every edit the application makes. A document
|
||||
edited outside it - by a script, or by hand - keeps its empty
|
||||
token, so a stale thread's write would still match on the gravity
|
||||
alone. Matching the comment as well closes that.
|
||||
"""
|
||||
repo.readings.insert_one({
|
||||
"_id": "outsideedit01",
|
||||
"beer_run_id": "1",
|
||||
"date": "2026-01-01",
|
||||
"sg": "1040",
|
||||
"comment": "original comment",
|
||||
"final_reading": "False",
|
||||
# No advice_token - a legacy reading.
|
||||
})
|
||||
|
||||
# The advice is written for the original comment.
|
||||
assert repo.set_reading_advice(
|
||||
"outsideedit01", "FRESH ADVICE", ok=True,
|
||||
expected_token="", expected_comment="original comment") == 1
|
||||
|
||||
# Somebody edits the comment directly in the database, leaving the
|
||||
# token empty - so the token cannot tell the generations apart.
|
||||
repo.readings.update_one(
|
||||
{"_id": "outsideedit01"},
|
||||
{"$set": {"comment": "edited outside the app"}},
|
||||
)
|
||||
|
||||
# The slow thread lands. It must be refused, even though the token
|
||||
# still matches.
|
||||
assert repo.set_reading_advice(
|
||||
"outsideedit01", "STALE ADVICE", ok=True,
|
||||
expected_token="", expected_comment="original comment") == 0
|
||||
|
||||
doc = repo.readings.find_one({"_id": "outsideedit01"})
|
||||
assert doc["advice"] == "FRESH ADVICE"
|
||||
assert doc["comment"] == "edited outside the app"
|
||||
|
||||
|
||||
def test_a_failed_advice_is_marked_as_such(client, repo):
|
||||
"""A generation failure reads differently from real advice."""
|
||||
repo.users.insert_one({"username": "beer", "password": "beer_pass"})
|
||||
|
||||
Reference in New Issue
Block a user