fix(advice): an editing token, so a comment-only correction is safe too

The gravity guard caught a correction that changed the gravity, but a
COMMENT-ONLY correction leaves the gravity unchanged - so the late
thread for the old advice still matched and overwrote the fresh one.

Reproduced against the live database:

    fresh advice matched: 1
    stale thread matched: 1     <-- should be 0
    FINAL advice: 'STALE ADVICE'

Every edit now writes a fresh token onto the reading, and the advice
thread carries the token it was given.  A late thread finds a token that
no longer matches, so its write is refused.  The gravity is still
checked as well.

Verified against the live database:

    token changed: True
    fresh advice matched: 1
    stale thread matched: 0     <-- must be 0
    FINAL advice: 'FRESH ADVICE'

174 tests (up from 173).
This commit is contained in:
2026-11-09 18:14:24 +10:00
parent 8da3601941
commit a78aabe3ea
4 changed files with 87 additions and 11 deletions
+40
View File
@@ -600,6 +600,46 @@ def test_stale_advice_cannot_overwrite_a_correction(repo):
)
def test_a_comment_only_correction_cannot_be_overwritten(repo):
"""A comment-only correction leaves the gravity unchanged.
So the gravity alone cannot tell the two generations apart - a late
thread for the old advice would still match and overwrite the fresh
one. The editing token is what tells them apart.
"""
# Written through upsert_reading, so it has an editing token.
reading_id = repo.upsert_reading(beer_run_id="1", beer_type="Ale", sg=1050,
reading_date="2026-01-01",
final_reading=False, comment="first comment")
doc = repo.readings.find_one({"beer_run_id": "1"})
token = doc.get("advice_token")
assert token, "the reading has no editing token"
# The advice thread starts on this generation.
# The brewer corrects the COMMENT only - the gravity is unchanged.
repo.upsert_reading(beer_run_id="1", beer_type="Ale", sg=1050,
reading_date="2026-01-01", final_reading=False,
comment="corrected comment")
assert repo.readings.find_one({"beer_run_id": "1"}).get("advice_token") != token, (
"the correction did not write a fresh token"
)
# The fresh advice lands.
fresh_token = repo.readings.find_one({"beer_run_id": "1"}).get("advice_token")
assert repo.set_reading_advice(reading_id, "FRESH ADVICE", True,
expected_sg=1050,
expected_token=fresh_token) == 1
# The late thread for the OLD generation finally lands.
stale = repo.set_reading_advice(reading_id, "STALE ADVICE", True,
expected_sg=1050, expected_token=token)
assert stale == 0, "the stale advice overwrote the correction"
after = repo.readings.find_one({"beer_run_id": "1"})
assert after["advice"] == "FRESH ADVICE"
assert after["comment"] == "corrected comment"
def test_a_reply_cut_off_at_the_token_ceiling_is_marked(repo):
"""A reply that stopped at the token limit is incomplete.