From 4bedd23c2487b3f0fc548ff1dd2b1ffb535ac408 Mon Sep 17 00:00:00 2001 From: hermes Date: Mon, 9 Nov 2026 19:10:16 +1000 Subject: [PATCH] fix(advice): the no-token guard must cover all three shapes of "no token" The guard matches on the absence of a token. `{"$exists": False}` covers a reading with no token field, but NOT one whose token field is present and empty. Verified against the live database, before the fix: token present but empty -> write matched: 0 <-- missed it `{"$in": [None, ""]}` covers all three - a missing field, an empty field, and a null one - because `$in` with null also matches a missing field. no token field -> write matched: 1 token present but empty -> write matched: 1 token is null -> write matched: 1 183 tests (up from 182). --- src/flask/mongo/repository.py | 5 ++++- tests/test_brew_advice.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/flask/mongo/repository.py b/src/flask/mongo/repository.py index e223f35..f781ada 100644 --- a/src/flask/mongo/repository.py +++ b/src/flask/mongo/repository.py @@ -278,7 +278,10 @@ class BeerRepository: # Falling back to the gravity here would re-open the hole # this guard exists to close - a comment-only correction # leaves the gravity unchanged. - query["advice_token"] = {"$exists": False} + # `$in` with null also matches a MISSING field, so this + # covers a reading with no token AND one whose token field + # is present but empty. + query["advice_token"] = {"$in": [None, ""]} elif expected_sg is not None: # A direct call with a gravity to check against. query["sg"] = str(expected_sg) diff --git a/tests/test_brew_advice.py b/tests/test_brew_advice.py index 0a74a15..3879299 100644 --- a/tests/test_brew_advice.py +++ b/tests/test_brew_advice.py @@ -826,6 +826,33 @@ def test_a_pre_token_reading_is_still_guarded_against_a_stale_write(repo): assert after["comment"] == "corrected comment" +def test_the_advice_is_written_for_a_reading_with_no_token_field(repo): + """All three shapes of "no token" must work. + + A reading read from the database has no token at all; one written by + an older version could have the field present but empty, or null. + The guard matches on the absence of a token, so it has to cover all + three - otherwise the advice is silently never written. + """ + for index, extra in enumerate(({}, {"advice_token": ""}, {"advice_token": None})): + reading_id = f"noToken{index:04d}" + repo.readings.insert_one({ + "_id": reading_id, + "beer_run_id": str(index), + "beer_type": "Ale", + "sg": "1050", + "date": "2026-01-01", + "final_reading": "False", + "comment": "c", + **extra, + }) + + written = repo.set_reading_advice(reading_id, "THE ADVICE", True, + expected_sg=None, + expected_token="") + assert written == 1, f"the advice was not written for {extra!r}" + + def test_a_reply_cut_off_at_the_token_ceiling_is_marked(repo): """A reply that stopped at the token limit is incomplete.