Files
hermes-webui/tests/test_issue5839_lazy_worklog_render.py
T
nesquena-hermesandt 803b081506 Release: lazy-render collapsed worklog rows w/ post-process + disclosure preservation (#5860, #5839) (#5866)
* fix(#5839): post-process + restore disclosure on lazily-materialized worklog rows

Gate (live Chromium) found the deferred-render path skipped two things the eager
path does: (1) materialized Markdown rows bypassed postProcessRenderedMessages ->
lost syntax highlight / copy button / mermaid / katex / trees; (2) detail-disclosure
(open tool/thinking card + scroll) was lost across rebuilds because restore ran
before deferred rows existed. _materializeDeferredWorklogRows now rAF-schedules
_postProcessWithAnchorSuppression + re-applies stashed disclosure state; the rebuild
restore pass stashes the captured state on still-deferred groups. +2 regression tests.

* changelog: #5860 lazy-worklog frontend fix (#5839, #4633)

---------

Co-authored-by: t <a@b>
2026-07-09 19:25:11 -07:00

105 lines
5.0 KiB
Python

"""Regression coverage for #5839: lazy settled-worklog DOM (compact mode).
Reasoning-heavy turns can carry 80+ activity rows per scene. The settled
compact-worklog render used to build EVERY row's DOM even when the group is
collapsed, so a long history balloons the DOM and a later synchronous layout
(e.g. opening a dropdown) tips the tab into a multi-GB freeze (#5839).
The fix defers row construction for a COLLAPSED settled worklog until first
expand, recovering the rows from the owning message after an HTML-cache restore
(where the JS-property stash is dropped). These are source-assertion tests in
the same style as the other anchor-scene UI tests (no JS engine needed).
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _function_body(src: str, name: str) -> str:
marker = f"function {name}"
start = src.index(marker)
brace = src.index("{", start)
depth = 0
for idx in range(brace, len(src)):
if src[idx] == "{":
depth += 1
elif src[idx] == "}":
depth -= 1
if depth == 0:
return src[brace + 1 : idx]
raise AssertionError(f"{name} body not found")
def test_settled_collapsed_worklog_defers_row_dom():
body = _function_body(UI_JS, "_renderSettledAnchorSceneForMessage")
# A collapsed settled worklog must stash rows + mark deferred + NOT build rows.
assert "group.classList.contains('tool-call-group-collapsed')" in body
assert "group._deferredWorklogRows=rows" in body
assert "data-worklog-rows-deferred" in body
# The expanded/keep-open path still renders eagerly.
assert "_renderAnchorSceneRowsIntoWorklog(group,rows,{settled:true})" in body
def test_toggle_materializes_deferred_rows_on_expand():
body = _function_body(UI_JS, "_toggleActivityGroup")
assert "if(!collapsed) _materializeDeferredWorklogRows(group);" in body
def test_materialize_helper_recovers_rows_after_cache_restore():
body = _function_body(UI_JS, "_materializeDeferredWorklogRows")
# Idempotent: gated on the deferred marker, cleared before building.
assert "data-worklog-rows-deferred" in body
# Falls back to message-recovery when the JS stash was dropped (innerHTML).
assert "_deferredWorklogRowsFromGroup(group)" in body
assert "_renderAnchorSceneRowsIntoWorklog(group,rows,{settled:true})" in body
def test_row_recovery_maps_group_to_message_scene():
body = _function_body(UI_JS, "_deferredWorklogRowsFromGroup")
# Recovers rawIdx from the disclosure key and rebuilds from S.messages.
assert "anchor-scene:" in body
assert "S.messages" in body
assert "_anchor_activity_scene" in body
assert "_anchorSceneRowsForRendering(scene,{settled:true})" in body
def test_cache_restore_rehydrates_deferred_worklogs():
# The HTML-cache fast-path restore must re-stash deferred rows so the first
# post-restore expand works (JS properties don't survive innerHTML).
assert "_rehydrateDeferredWorklogsFromCache(inner);" in UI_JS
body = _function_body(UI_JS, "_rehydrateDeferredWorklogsFromCache")
assert '[data-worklog-rows-deferred="1"]' in body
assert "_deferredWorklogRowsFromGroup(group)" in body
# If a group can't be recovered, drop the deferred marker (no dead chip).
assert "removeAttribute('data-worklog-rows-deferred')" in body
def test_blank_turn_reveal_materializes_deferred_rows():
# The "blank turn" safety reveal force-expands a collapsed worklog; it must
# also materialize deferred rows or the revealed worklog would be empty.
assert UI_JS.count("_materializeDeferredWorklogRows(group)") >= 3
def test_materialize_postprocesses_and_restores_disclosure():
"""#5860 gate fix: lazily-materialized rows must get the same post-processing
(syntax highlight / copy button / mermaid / katex / trees) as the eager path,
and any captured detail-disclosure state must be re-applied — otherwise
expanded rows render un-enhanced and an open tool card resets closed."""
body = _function_body(UI_JS, "_materializeDeferredWorklogRows")
# post-processing scheduled after materialization (matches eager rebuild paths)
assert "_postProcessWithAnchorSuppression(group)" in body
assert "requestAnimationFrame" in body
# disclosure state stashed at defer/rebuild time is re-applied after rows exist
assert "_deferredWorklogDisclosure" in body
assert "_restoreWorklogDetailDisclosureState(group" in body
def test_rebuild_stashes_disclosure_on_deferred_groups():
"""#5860 gate fix: on a transcript rebuild the disclosure-restore pass can't
reach deferred groups (no rows yet), so the captured state is stashed on each
still-deferred group for _materializeDeferredWorklogRows to apply on expand."""
assert "group._deferredWorklogDisclosure=worklogDetailDisclosureState;" in UI_JS
# stash happens right after the main restore pass over the rebuilt transcript
assert 'querySelectorAll(\'[data-worklog-rows-deferred="1"]\')' in UI_JS