216 lines
9.0 KiB
Python
216 lines
9.0 KiB
Python
"""Static-analysis tests for #2403 — collapse old interim progress notes.
|
|
|
|
When more than INTERIM_COLLAPSE_THRESHOLD interim_assistant events arrive in
|
|
one turn, earlier rendered blocks are hidden behind a toggle so the viewport
|
|
stays focused on the latest progress note.
|
|
|
|
These tests pin the structural invariants without a live browser, using the
|
|
same static-analysis pattern as test_issue2713_streaming_segment_flush.py.
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
|
|
|
|
def read(rel):
|
|
return (REPO / rel).read_text(encoding="utf-8")
|
|
|
|
|
|
def _extract_interim_handler(src):
|
|
"""Return the full interim_assistant SSE handler body."""
|
|
start_pattern = "source.addEventListener('interim_assistant'"
|
|
start = src.index(start_pattern)
|
|
end_marker = "\n });"
|
|
pos = start
|
|
while True:
|
|
idx = src.index(end_marker, pos + 1)
|
|
if idx > start + len(start_pattern) + 20:
|
|
return src[start : idx + len(end_marker)]
|
|
pos = idx
|
|
|
|
|
|
def _visible_interim_flush_pos(fn):
|
|
attr_pos = fn.index("setAttribute('data-interim','1')")
|
|
candidates = [
|
|
"_flushPendingSegmentRender({force:true,skipAnchorProcessProse:true})",
|
|
"_flushPendingSegmentRender({force:true})",
|
|
]
|
|
positions = [fn.find(candidate, attr_pos) for candidate in candidates]
|
|
positions = [pos for pos in positions if pos != -1]
|
|
assert positions, "visible interim flush call not found after data-interim marker"
|
|
return min(positions)
|
|
|
|
|
|
class TestInterimCollapseHandlerStructure:
|
|
"""The interim_assistant handler must contain the collapse threshold and logic."""
|
|
|
|
def test_collapse_threshold_constant_present(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
assert "INTERIM_COLLAPSE_THRESHOLD" in fn, (
|
|
"interim_assistant handler must define INTERIM_COLLAPSE_THRESHOLD "
|
|
"to avoid scattered magic numbers"
|
|
)
|
|
|
|
def test_threshold_is_three(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
# Constant must be assigned to 3
|
|
assert re.search(r"INTERIM_COLLAPSE_THRESHOLD\s*=\s*3\b", fn), (
|
|
"INTERIM_COLLAPSE_THRESHOLD must be set to 3"
|
|
)
|
|
|
|
def test_visibleInterimSnippets_length_comparison(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
assert "visibleInterimSnippets.length" in fn, (
|
|
"collapse guard must compare visibleInterimSnippets.length"
|
|
)
|
|
assert "INTERIM_COLLAPSE_THRESHOLD" in fn, (
|
|
"collapse guard must reference INTERIM_COLLAPSE_THRESHOLD, not a magic number"
|
|
)
|
|
|
|
def test_interim_data_attribute_set(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
assert "data-interim" in fn, (
|
|
"interim_assistant handler must mark each segment with data-interim "
|
|
"so collapse logic can query them"
|
|
)
|
|
|
|
def test_interim_collapsed_class_applied(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
assert "interim-collapsed" in fn, (
|
|
"collapse logic must apply the interim-collapsed CSS class to hide old blocks"
|
|
)
|
|
|
|
def test_collapse_toggle_element_created(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
assert "interim-collapse-toggle" in fn, (
|
|
"collapse logic must create an .interim-collapse-toggle element"
|
|
)
|
|
|
|
def test_toggle_text_references_count(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
# Toggle label must be dynamic: "Show N earlier update(s)"
|
|
assert "earlier update" in fn, (
|
|
"collapse toggle text must reference 'earlier update' so the count is visible"
|
|
)
|
|
|
|
def test_attribute_set_before_flush(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
attr_pos = fn.index("setAttribute('data-interim','1')")
|
|
flush_pos = _visible_interim_flush_pos(fn)
|
|
assert attr_pos < flush_pos, (
|
|
"data-interim attribute must be set before _flushPendingSegmentRender "
|
|
"so the segment is marked before it is sealed"
|
|
)
|
|
|
|
def test_collapse_after_flush_before_reset(self):
|
|
src = read("static/messages.js")
|
|
fn = _extract_interim_handler(src)
|
|
flush_pos = _visible_interim_flush_pos(fn)
|
|
collapse_pos = fn.index("INTERIM_COLLAPSE_THRESHOLD")
|
|
reset_pos = fn.index("_resetAssistantSegment()", collapse_pos)
|
|
assert flush_pos < collapse_pos < reset_pos, (
|
|
"collapse logic must run after flush but before _resetAssistantSegment"
|
|
)
|
|
|
|
|
|
class TestInterimCollapseCSS:
|
|
"""CSS must define both .interim-collapsed and .interim-collapse-toggle."""
|
|
|
|
def test_interim_collapsed_rule_present(self):
|
|
css = read("static/style.css")
|
|
assert ".interim-collapsed" in css, (
|
|
"style.css must define .interim-collapsed to hide collapsed blocks"
|
|
)
|
|
|
|
def test_interim_collapsed_uses_display_none(self):
|
|
css = read("static/style.css")
|
|
m = re.search(r"\.interim-collapsed\s*\{[^}]*\}", css)
|
|
assert m, ".interim-collapsed rule not found in style.css"
|
|
rule = m.group(0)
|
|
assert "display" in rule and "none" in rule, (
|
|
".interim-collapsed must set display:none"
|
|
)
|
|
|
|
def test_collapse_toggle_rule_present(self):
|
|
css = read("static/style.css")
|
|
assert ".interim-collapse-toggle" in css, (
|
|
"style.css must define .interim-collapse-toggle"
|
|
)
|
|
|
|
def test_collapse_toggle_has_cursor_pointer(self):
|
|
css = read("static/style.css")
|
|
# Extract the first .interim-collapse-toggle rule block
|
|
m = re.search(r"\.interim-collapse-toggle\s*\{[^}]*\}", css)
|
|
assert m, ".interim-collapse-toggle rule not found"
|
|
rule = m.group(0)
|
|
assert "cursor" in rule and "pointer" in rule, (
|
|
".interim-collapse-toggle must set cursor:pointer"
|
|
)
|
|
|
|
def test_collapse_toggle_hover_rule_present(self):
|
|
css = read("static/style.css")
|
|
assert ".interim-collapse-toggle:hover" in css, (
|
|
"style.css must define a :hover rule for .interim-collapse-toggle"
|
|
)
|
|
|
|
|
|
class TestInterimCollapseSurvivesLiveTurnRestore:
|
|
"""Regression guard (#3574 deep-review, Codex catch): the collapse toggle
|
|
must use a DELEGATED document-level click handler, NOT a per-element
|
|
addEventListener. The live turn's DOM is snapshotted/restored via
|
|
outerHTML/innerHTML on session switch (snapshotLiveTurnHtmlForSession /
|
|
restoreLiveTurnHtmlForSession), which strips element-attached listeners.
|
|
A per-element listener would leave a restored toggle inert and collapsed
|
|
interim notes permanently unreachable for the rest of the turn.
|
|
"""
|
|
|
|
def test_delegated_document_click_handler_present(self):
|
|
src = read("static/messages.js")
|
|
assert "function _interimCollapseDelegatedClick" in src, (
|
|
"interim-collapse toggle must be handled by the delegated "
|
|
"_interimCollapseDelegatedClick handler so it survives a live-turn "
|
|
"DOM restore (innerHTML rebuild strips per-element listeners)."
|
|
)
|
|
assert "document.addEventListener('click', _interimCollapseDelegatedClick)" in src, (
|
|
"the delegated interim-collapse handler must be registered at the "
|
|
"document level (not on the toggle element)."
|
|
)
|
|
|
|
def test_delegated_handler_resolves_toggle_via_closest(self):
|
|
src = read("static/messages.js")
|
|
handler_start = src.index("function _interimCollapseDelegatedClick")
|
|
handler = src[handler_start:handler_start + 900]
|
|
assert ".closest('.interim-collapse-toggle')" in handler, (
|
|
"delegated handler must resolve the clicked toggle via "
|
|
"closest('.interim-collapse-toggle') so clicks on the toggle (or "
|
|
"its children) route correctly."
|
|
)
|
|
|
|
def test_toggle_creation_does_not_attach_per_element_listener(self):
|
|
"""The toggle-creation block must NOT bind a click listener directly on
|
|
the element — that is the bug the delegated handler replaces."""
|
|
src = read("static/messages.js")
|
|
handler = _extract_interim_handler(src)
|
|
assert "toggle.addEventListener('click'" not in handler, (
|
|
"toggle must not use a per-element click listener (lost on live-turn "
|
|
"DOM restore); use the delegated document handler instead."
|
|
)
|
|
|
|
def test_toggle_carries_threshold_data_attribute(self):
|
|
"""State must live in the DOM (data-threshold) so the stateless delegated
|
|
handler works on both freshly-created and restored toggles."""
|
|
src = read("static/messages.js")
|
|
assert "toggle.dataset.threshold" in src, (
|
|
"toggle must carry data-threshold so the delegated handler can "
|
|
"recompute the collapse set without closure state after a restore."
|
|
)
|