fix(advice): the pre-token guard, and a stripper that cannot empty real advice
Two findings from the fifth review.
The pre-token fallback re-opened the hole
-----------------------------------------
A reading that has never been edited has no token. The guard fell back
to the gravity check for those, and a comment-only correction leaves the
gravity unchanged - so a late thread's write matched and overwrote the
fresh advice, for exactly the 47 existing readings.
Reproduced live before the fix:
pre-token reading token: None -> stale thread carries ""
fresh write modified: 1
stale write modified: 1 <<< stale advice overwrote the fresh one
final advice: 'STALE ADVICE'
The guard now matches on the ABSENCE of a token, which still tells the
generations apart: an edit writes a token, so a stale thread's "" stops
matching the moment the reading is corrected.
fresh write modified: 1 (must be 1)
stale write modified: 0 (must be 0)
The stripper could empty real advice
------------------------------------
"Actually the drop is fine.\\n\\nWait a couple of days and check again."
is advice, and judging it paragraph by paragraph emptied it - so the
brewer saw nothing at all, with no failure note.
The test is no longer a marker list, which real advice often does not
contain. A reply is all-working when it is LONG and has no instruction
in it: the model's working runs to thousands of characters, while the
advice is two to five sentences.
short reply with no marker -> kept
long reply, no instruction -> dropped
long reply with advice -> advice kept
single-paragraph working -> dropped
Also: the log message named the gravity, but the guard is the token.
182 tests (up from 178).
This commit is contained in:
@@ -649,15 +649,23 @@ def test_a_reply_that_is_all_working_stores_no_working():
|
||||
about their beer instead of any advice at all.
|
||||
"""
|
||||
working_only = (
|
||||
"Let me think about what this reading means.\n\n"
|
||||
"Let me think about what this reading means. The original gravity is "
|
||||
"1.055 and the current reading is 1.030, which is a drop of 25 points "
|
||||
"over nine days at 19 degrees, and I need to work out what that means "
|
||||
"for the expected attenuation.\n\n"
|
||||
"Perhaps the drop is smaller than expected, and it could be that the "
|
||||
"yeast is stressed.\n\n"
|
||||
"yeast is stressed by the temperature rather than actually done, "
|
||||
"because most ale strains slow down considerably below 18 degrees.\n\n"
|
||||
"Wait, I need to keep in mind that the hydrometer was read at a "
|
||||
"different temperature.\n\n"
|
||||
"different temperature than the sample, which would make the reading "
|
||||
"read lower than it really is, and I have no way of knowing from the "
|
||||
"data alone what the correction factor was.\n\n"
|
||||
"I need to keep in mind that the original gravity here was measured "
|
||||
"before the hops went in.\n\n"
|
||||
"So the question is what the expected attenuation should be, and I "
|
||||
"am not sure without more data."
|
||||
"before the hops went in, and the reading on brew day was 1.055, so "
|
||||
"the drop is twenty-five points against a potential of fifty-five.\n\n"
|
||||
"So the question is what the expected attenuation should be for this "
|
||||
"style at this temperature, and I am not sure without more data or a "
|
||||
"temperature-corrected reading to compare against."
|
||||
)
|
||||
|
||||
stripped = brew_advice._strip_reasoning(working_only)
|
||||
@@ -667,6 +675,39 @@ def test_a_reply_that_is_all_working_stores_no_working():
|
||||
)
|
||||
|
||||
|
||||
def test_a_short_reply_is_advice_even_without_a_marker_word():
|
||||
"""Real advice is often phrased without any marker phrase.
|
||||
|
||||
"Actually the drop is fine. Wait a couple of days and check again."
|
||||
is advice - and judging it paragraph by paragraph emptied it, so the
|
||||
brewer saw nothing at all.
|
||||
"""
|
||||
reply = ("Actually the drop is fine.\n\n"
|
||||
"Wait a couple of days and check again.")
|
||||
|
||||
assert brew_advice._strip_reasoning(reply) == reply
|
||||
|
||||
|
||||
def test_a_long_reply_with_no_instruction_is_dropped():
|
||||
"""Long, and nothing in it reads like an instruction - all working."""
|
||||
working = ("Let me think about this.\n\n" +
|
||||
"Perhaps the yeast is stressed, and it could be that the "
|
||||
"temperature is too low. " * 20)
|
||||
|
||||
assert brew_advice._strip_reasoning(working) == ""
|
||||
|
||||
|
||||
def test_a_long_reply_with_an_instruction_keeps_it():
|
||||
"""The working is trimmed from it, but the advice stays."""
|
||||
mixed = ("Let me think about this.\n\n" +
|
||||
"Perhaps the yeast is stressed. " * 15 +
|
||||
"\n\nWarm the fermenter to 20C and give the yeast a gentle "
|
||||
"swirl to rouse it.")
|
||||
|
||||
out = brew_advice._strip_reasoning(mixed)
|
||||
assert "Warm the fermenter" in out
|
||||
|
||||
|
||||
def test_a_paragraph_about_keeping_something_in_mind_is_not_advice():
|
||||
""""Keep" alone is not an advice marker.
|
||||
|
||||
@@ -745,6 +786,46 @@ def test_the_advice_is_written_for_a_reading_that_predates_the_token(repo):
|
||||
assert after["comment"] == "a comment"
|
||||
|
||||
|
||||
def test_a_pre_token_reading_is_still_guarded_against_a_stale_write(repo):
|
||||
"""A reading that has never been edited has no token at all.
|
||||
|
||||
The guard matches on the ABSENCE of a token, so an edit still tells
|
||||
the generations apart. Falling back to the gravity here would
|
||||
re-open the hole, because a comment-only correction leaves the
|
||||
gravity unchanged.
|
||||
"""
|
||||
repo.readings.insert_one({
|
||||
"_id": "preToken0000000000000010",
|
||||
"beer_run_id": "1",
|
||||
"beer_type": "Ale",
|
||||
"sg": "1050",
|
||||
"date": "2026-11-01",
|
||||
"final_reading": "False",
|
||||
"comment": "first comment",
|
||||
})
|
||||
|
||||
# The advice thread starts on this generation - no token.
|
||||
assert repo.set_reading_advice("preToken0000000000000010", "FRESH ADVICE",
|
||||
True, expected_sg=None,
|
||||
expected_token="") == 1
|
||||
|
||||
# The brewer corrects the comment. The edit writes a token.
|
||||
repo.upsert_reading(beer_run_id="1", beer_type="Ale", sg=1050,
|
||||
reading_date="2026-11-01", final_reading=False,
|
||||
comment="corrected comment")
|
||||
|
||||
# The late thread for the OLD generation lands. It carries no token,
|
||||
# and the reading now HAS one - so it must be refused.
|
||||
stale = repo.set_reading_advice("preToken0000000000000010", "STALE ADVICE",
|
||||
True, expected_sg=None,
|
||||
expected_token="")
|
||||
assert stale == 0, "the stale advice overwrote the correction"
|
||||
|
||||
after = repo.readings.find_one({"beer_run_id": "1"})
|
||||
assert after.get("advice") is None
|
||||
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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user