diff --git a/tests/test_app_js.py b/tests/test_app_js.py index 3de90b82..8eeb54fc 100644 --- a/tests/test_app_js.py +++ b/tests/test_app_js.py @@ -285,6 +285,33 @@ def test_system_turn_dedups_against_history_by_event_id() -> None: "replayHistory (and the live handler) must record system-turn ids for the dedup set." ) + # Pin the wiring on BOTH read paths, scoped to its method — a refactor that + # keeps the Set but drops the live-handler consultation (or the + # replayHistory-side record) silently re-opens the double-render while the + # file-global checks above still pass. + live_start = body.index('case "system_turn":') + # End at the NEXT switch case, not the first ``break;`` — the dedup-skip + # path breaks before the ``.add(``, so a ``break;``-bounded slice would + # drop the record half and false-fail the ``.add(`` assertion below. + live_end = body.index('\n case "', live_start + 1) + live_block = body[live_start:live_end] + assert re.search(r"_renderedSystemEventIds[\s\S]*?\.\s*has\(", live_block), ( + "the live system_turn handler must CONSULT the dedup set (skip an id " + "already painted from /history), not merely reference the Set elsewhere." + ) + assert re.search(r"_renderedSystemEventIds[\s\S]*?\.\s*add\(", live_block), ( + "the live system_turn handler must RECORD the id it renders so a later " + "/history re-render (clear_ui) doesn't repaint it." + ) + + replay_start = _pane_method_offset(body, "replayHistory") + replay_end = _pane_method_offset(body, "_attachRetryToLastAssistant") + replay_block = body[replay_start:replay_end] + assert re.search(r"_renderedSystemEventIds[\s\S]*?\.\s*add\(", replay_block), ( + "replayHistory must record each replayed system row's event_id so the " + "live system_turn handler can dedup against it." + ) + def test_retry_walk_skips_operator_context_cards() -> None: """Interactive twin of the coord retry-skip guard. diff --git a/tests/test_coordinator_page.py b/tests/test_coordinator_page.py index 74511b20..9a8729bd 100644 --- a/tests/test_coordinator_page.py +++ b/tests/test_coordinator_page.py @@ -401,6 +401,38 @@ def test_coord_dedups_system_turn_against_history_by_event_id(): "false-skip after clear_ui / replay_truncated." ) + # The seam must be wired on BOTH read paths, not merely present somewhere + # in the file — a refactor that keeps the Set but drops the live-handler + # consultation (or the history-side record) silently re-opens the + # double-render. Scope each assertion to its block so the wiring, not the + # bare symbol, is pinned. (A dedupe-neutered factory — guard short-circuited + # to ``false`` — still contains ``renderedSystemEventIds.has(`` and so + # passes the file-global checks above; these slice checks catch it.) + sys_case = body.index('case "system_turn":') + # End at the NEXT switch case, not the first ``break;`` — the dedup-skip + # ``...has(sysEid)) break;`` is itself a break that precedes the ``.add(``, + # so a ``break;``-bounded slice would drop the record half. + sys_case_end = body.index('\n case "', sys_case + 1) + live_block = body[sys_case:sys_case_end] + assert "renderedSystemEventIds.has(" in live_block, ( + "the live system_turn handler must CONSULT the dedup set (skip an id " + "already painted from /history) — not just reference the Set elsewhere." + ) + assert "renderedSystemEventIds.add(" in live_block, ( + "the live system_turn handler must RECORD the id it renders so a later " + "/history re-render (clear_ui) doesn't repaint it." + ) + + # The history render path must seed the set from each replayed system row's + # event_id, so a subsequent live replay of the same id is skipped. + assert 'role === "system"' in body + sys_replay = body.index('role === "system"', body.index("refetchHistory")) + replay_window = body[sys_replay : sys_replay + 600] + assert "renderedSystemEventIds.add(" in replay_window, ( + "the history render's system-role branch must record each replayed " + "turn's event_id so the live system_turn handler can dedup against it." + ) + def test_coord_retry_walk_skips_operator_context_cards(): """Retry must NOT regenerate a stale assistant turn when the last DOM row is