Plugin reply shape, preferences UI, cited-record status, and the lesson kind #166

Merged
bvandeusen merged 7 commits from dev into main 2026-09-18 16:33:57 -04:00
Showing only changes of commit 94ecb633a0 - Show all commits
+10 -6
View File
@@ -53,6 +53,10 @@ def _sessions(results: list, counter: list[int], seen_sql: list | None = None):
the ORDER of `results` pins the order of the queries — which is what makes the ORDER of `results` pins the order of the queries — which is what makes
the query-count assertion meaningful rather than incidental. the query-count assertion meaningful rather than incidental.
Counts rows are 4-tuples — (milestone_id, status, count, max(updated_at)) —
because the counts query also carries the touched-at that `last_touched_at`
is computed from. A 3-tuple unpacks as a ValueError, not as a wrong answer.
`seen_sql` collects the rendered statements. The ordering these surfaces `seen_sql` collects the rendered statements. The ordering these surfaces
have to agree on lives in an ORDER BY, which no amount of feeding rows to a have to agree on lives in an ORDER BY, which no amount of feeding rows to a
mock can exercise — a stand-in hands back whatever order the test chose. mock can exercise — a stand-in hands back whatever order the test chose.
@@ -116,7 +120,7 @@ async def test_a_milestone_row_names_its_next_open_step():
"""The listing that says "8 of 9" now says WHICH one, in the same read.""" """The listing that says "8 of 9" now says WHICH one, in the same read."""
rows = await _summaries( rows = await _summaries(
[_milestone_row(409)], [_milestone_row(409)],
[(409, "done", 8), (409, "todo", 1)], [(409, "done", 8, None), (409, "todo", 1, None)],
[(409, 4154, "Step 8 — a cited record carries its status", "todo")], [(409, 4154, "Step 8 — a cited record carries its status", "todo")],
) )
assert rows[5][0]["next_step"] == { assert rows[5][0]["next_step"] == {
@@ -134,7 +138,7 @@ async def test_a_finished_plan_names_no_next_step_rather_than_omitting_the_key()
test for its absence to learn the answer, and a reader who forgets is back test for its absence to learn the answer, and a reader who forgets is back
to guessing — which is the failure this step exists for. to guessing — which is the failure this step exists for.
""" """
rows = await _summaries([_milestone_row(416)], [(416, "done", 9)], []) rows = await _summaries([_milestone_row(416)], [(416, "done", 9, None)], [])
assert rows[5][0]["next_step"] is None assert rows[5][0]["next_step"] is None
assert "next_step" in rows[5][0] assert "next_step" in rows[5][0]
@@ -149,7 +153,7 @@ async def test_the_earliest_open_step_wins_not_the_earliest_step():
""" """
rows = await _summaries( rows = await _summaries(
[_milestone_row(409)], [_milestone_row(409)],
[(409, "done", 2), (409, "todo", 2)], [(409, "done", 2, None), (409, "todo", 2, None)],
# The query filters to open steps, so the closed ones never appear — # The query filters to open steps, so the closed ones never appear —
# this asserts the ORDER of what does: earliest open, not last written. # this asserts the ORDER of what does: earliest open, not last written.
[(409, 4015, "Step 6", "in_progress"), (409, 4154, "Step 8", "todo")], [(409, 4015, "Step 6", "in_progress"), (409, 4154, "Step 8", "todo")],
@@ -169,7 +173,7 @@ async def test_the_batch_does_not_fan_out_per_milestone():
counter = [0] counter = [0]
await _summaries( await _summaries(
[_milestone_row(i) for i in range(40)], [_milestone_row(i) for i in range(40)],
[(i, "todo", 1) for i in range(40)], [(i, "todo", 1, None) for i in range(40)],
[(i, 1000 + i, f"S{i}", "todo") for i in range(40)], [(i, 1000 + i, f"S{i}", "todo") for i in range(40)],
counter=counter, counter=counter,
) )
@@ -190,7 +194,7 @@ async def test_both_step_queries_take_the_same_visibility_clause():
seen.append(uid) seen.append(uid)
return true() return true()
results = [[_milestone_row(409)], [(409, "todo", 1)], [(409, 1, "S", "todo")]] results = [[_milestone_row(409)], [(409, "todo", 1, None)], [(409, 1, "S", "todo")]]
with patch.object(ms, "async_session", _sessions(results, [0])), \ with patch.object(ms, "async_session", _sessions(results, [0])), \
patch.object(ms.access_svc, "readable_notes_clause", _clause): patch.object(ms.access_svc, "readable_notes_clause", _clause):
await ms.get_project_milestone_summaries(7, [5]) await ms.get_project_milestone_summaries(7, [5])
@@ -222,7 +226,7 @@ async def test_the_listing_and_placement_agree_on_what_open_means():
listing_sql: list = [] listing_sql: list = []
rows = await _summaries( rows = await _summaries(
[_milestone_row(409)], [_milestone_row(409)],
[(409, "done", 2), (409, "todo", 2)], [(409, "done", 2, None), (409, "todo", 2, None)],
[(409, s.id, s.title, s.status) for s in steps if s.status in ms.OPEN_STEP_STATUSES], [(409, s.id, s.title, s.status) for s in steps if s.status in ms.OPEN_STEP_STATUSES],
seen_sql=listing_sql, seen_sql=listing_sql,
) )