* perf(providers): O(1) codex cache merge membership checks (#3656) Co-authored-by: Pamnard <pamnard@users.noreply.github.com> * fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627) Co-authored-by: Rod Boev <rod.boev@gmail.com> * fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650) Co-authored-by: happy5318 <happy5318@users.noreply.github.com> * fix(clarify): don't stash clarify draft while submission is in flight (#3651) Co-authored-by: carryzuo00 <carryzuo00@gmail.com> * perf(sessions): batch lineage report child fetch by parent id (#3659) Co-authored-by: Pamnard <pamnard@users.noreply.github.com> * perf(sessions): batch orphan sidecar state.db existence probes (#3657) Co-authored-by: Pamnard <pamnard@users.noreply.github.com> * test(streaming): pin DOM-INFLIGHT reattach invariant (#3572) Co-authored-by: Rod Boev <rod.boev@gmail.com> * docs(changelog): v0.51.268 — Release IJ (stage-b1) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Pamnard <pamnard@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: happy5318 <happy5318@users.noreply.github.com> Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
213 lines
10 KiB
Python
213 lines
10 KiB
Python
"""Static-analysis contract tests for the DOM-INFLIGHT reattach sync invariant (#3040).
|
|
|
|
Every site that writes to INFLIGHT[] must persist the change so that a reconnect
|
|
or session-switch can seed the new closure from durable state rather than a stale
|
|
DOM snapshot. These tests pin the structural invariants to catch regressions that
|
|
remove a persistence call or add a new INFLIGHT write without pairing it with one.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
MESSAGES_JS = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
_LINES = MESSAGES_JS.splitlines()
|
|
|
|
|
|
def _line_window(line_no: int, radius: int = 20) -> str:
|
|
"""Return up to `radius` lines on either side of `line_no` (1-based) as one string."""
|
|
start = max(0, line_no - 1 - radius)
|
|
end = min(len(_LINES), line_no - 1 + radius + 1)
|
|
return "\n".join(_LINES[start:end])
|
|
|
|
|
|
def _inflight_write_line_numbers() -> list[int]:
|
|
"""Return 1-based line numbers for every INFLIGHT[activeSid] assignment site."""
|
|
results = []
|
|
for i, line in enumerate(_LINES, start=1):
|
|
if re.search(r"INFLIGHT\[activeSid\]\s*=\s*\{", line) or re.search(
|
|
r"INFLIGHT\[activeSid\]\s*\|\|\s*\(INFLIGHT\[activeSid\]\s*=\s*\{", line
|
|
):
|
|
results.append(i)
|
|
return results
|
|
|
|
|
|
def test_inflight_write_sites_exist():
|
|
"""Sanity-check: messages.js must contain at least 3 INFLIGHT[activeSid] write sites."""
|
|
sites = _inflight_write_line_numbers()
|
|
assert len(sites) >= 3, (
|
|
f"Expected at least 3 INFLIGHT[activeSid] assignment sites, found {len(sites)}. "
|
|
"If send() was refactored, update the expected count."
|
|
)
|
|
|
|
|
|
def _extract_send_body() -> str:
|
|
"""Extract the body of the send() function up to attachLiveStream() as one string."""
|
|
# send() starts with `async function send(` and the INFLIGHT writes in the
|
|
# optimistic path and catch block both precede the attachLiveStream call.
|
|
send_start = MESSAGES_JS.find("async function send(")
|
|
assert send_start != -1, "async function send() not found in messages.js."
|
|
attach_call = MESSAGES_JS.find("attachLiveStream(activeSid, streamId", send_start)
|
|
assert attach_call != -1, "attachLiveStream call not found inside send()."
|
|
return MESSAGES_JS[send_start:attach_call]
|
|
|
|
|
|
def test_send_inflight_writes_are_paired_with_saveInflightState():
|
|
"""The INFLIGHT[activeSid] assignments in send() must be covered by saveInflightState()
|
|
calls within the same send() scope.
|
|
|
|
send() makes the optimistic UI write before calling /api/chat/start so that a tab
|
|
reload during the network round-trip can restore the user's message. If the write
|
|
is not persisted, the only recovery window is lost.
|
|
|
|
There are 3 write sites but only 2 saveInflightState calls because the catch-block
|
|
fallback write (line ~530) shares its persistence call with the post-start path:
|
|
if /api/chat/start succeeds, the post-start block's saveInflightState covers it;
|
|
if /api/chat/start fails, the error handler deletes INFLIGHT[activeSid] anyway.
|
|
A minimum of 2 saveInflightState calls pins this intentional design.
|
|
"""
|
|
send_body = _extract_send_body()
|
|
|
|
# Count how many INFLIGHT assignments appear in send() before attachLiveStream.
|
|
write_count = len(re.findall(r"INFLIGHT\[activeSid\]\s*=\s*\{", send_body))
|
|
assert write_count >= 2, (
|
|
f"Expected at least 2 INFLIGHT[activeSid] write sites in send(), found {write_count}. "
|
|
"The optimistic write and the catch-block fallback should both be present."
|
|
)
|
|
|
|
# There must be at least 2 saveInflightState calls covering the write paths.
|
|
# (The catch-block fallback write shares the post-start persistence call.)
|
|
persist_count = send_body.count("saveInflightState(")
|
|
assert persist_count >= 2, (
|
|
f"send() has only {persist_count} saveInflightState() call(s) before attachLiveStream "
|
|
"(expected at least 2: one before /api/chat/start for optimistic reload recovery, "
|
|
"one after start for stream-id persistence). "
|
|
"Removing either leaves a reload window with unrecoverable INFLIGHT state."
|
|
)
|
|
|
|
|
|
def test_syncInflightAssistantMessage_calls_throttled_persist():
|
|
"""syncInflightAssistantMessage() must call _throttledPersist() to flush the live
|
|
assistant text into persistent INFLIGHT state on every token and interim_assistant event."""
|
|
fn_start = MESSAGES_JS.find("function syncInflightAssistantMessage(){")
|
|
assert fn_start != -1, "syncInflightAssistantMessage() not found in messages.js."
|
|
fn_body_start = MESSAGES_JS.find("{", fn_start)
|
|
# Walk braces to find the closing brace of the function body.
|
|
depth = 1
|
|
i = fn_body_start + 1
|
|
while i < len(MESSAGES_JS) and depth:
|
|
if MESSAGES_JS[i] == "{":
|
|
depth += 1
|
|
elif MESSAGES_JS[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
fn_body = MESSAGES_JS[fn_body_start:i]
|
|
assert "_throttledPersist()" in fn_body, (
|
|
"syncInflightAssistantMessage() must call _throttledPersist() so each "
|
|
"live-text update is scheduled for persistence. Without this, a reconnect "
|
|
"seeds from the last tool-event snapshot and loses all interim assistant text."
|
|
)
|
|
|
|
|
|
def test_interim_assistant_calls_syncInflightAssistantMessage_after_accumulating_text():
|
|
"""The interim_assistant event handler must call syncInflightAssistantMessage()
|
|
after it appends to assistantText and visibleInterimSnippets.
|
|
|
|
Without this call, the INFLIGHT assistant message is not updated before a
|
|
potential reconnect, causing already-streamed content to be lost.
|
|
"""
|
|
handler_start = MESSAGES_JS.find("source.addEventListener('interim_assistant',")
|
|
assert handler_start != -1, "interim_assistant event listener not found in messages.js."
|
|
# Extract the handler body up to the matching closing brace.
|
|
brace_pos = MESSAGES_JS.find("{", handler_start)
|
|
depth = 1
|
|
i = brace_pos + 1
|
|
while i < len(MESSAGES_JS) and depth:
|
|
if MESSAGES_JS[i] == "{":
|
|
depth += 1
|
|
elif MESSAGES_JS[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
handler_body = MESSAGES_JS[brace_pos:i]
|
|
|
|
assert "assistantText" in handler_body, (
|
|
"interim_assistant handler does not reference assistantText — handler may have been refactored."
|
|
)
|
|
assert "visibleInterimSnippets" in handler_body, (
|
|
"interim_assistant handler does not reference visibleInterimSnippets — "
|
|
"interim snippet tracking was removed."
|
|
)
|
|
# syncInflightAssistantMessage() must appear AFTER the accumulation lines.
|
|
accum_pos = handler_body.find("visibleInterimSnippets.push(")
|
|
sync_pos = handler_body.find("syncInflightAssistantMessage()")
|
|
assert sync_pos != -1, (
|
|
"interim_assistant handler does not call syncInflightAssistantMessage(). "
|
|
"Without this, INFLIGHT is not updated with interim text and a reconnect loses progress."
|
|
)
|
|
assert accum_pos != -1 and sync_pos > accum_pos, (
|
|
"syncInflightAssistantMessage() must appear AFTER visibleInterimSnippets.push() "
|
|
"so that the persisted snapshot includes the freshly accumulated snippet."
|
|
)
|
|
|
|
|
|
def test_attachLiveStream_reconnect_seeds_from_INFLIGHT_not_querySelector():
|
|
"""On reconnect, attachLiveStream() must seed assistantText from INFLIGHT state,
|
|
not from a live DOM query (querySelector).
|
|
|
|
Seeding from the DOM is fragile: an off-screen or BFCache-restored session may
|
|
have no rendered content, so the closure would start empty and duplicate tokens
|
|
would overwrite the already-displayed response.
|
|
"""
|
|
fn_start = MESSAGES_JS.find("function attachLiveStream(")
|
|
assert fn_start != -1, "attachLiveStream() not found in messages.js."
|
|
# Capture up to 60 lines after the function signature for the seed section.
|
|
fn_head_end = fn_start
|
|
for _ in range(60):
|
|
fn_head_end = MESSAGES_JS.find("\n", fn_head_end + 1)
|
|
if fn_head_end == -1:
|
|
break
|
|
seed_section = MESSAGES_JS[fn_start:fn_head_end]
|
|
|
|
# The reconnect seed must come from INFLIGHT[activeSid].
|
|
assert "INFLIGHT[activeSid]" in seed_section, (
|
|
"attachLiveStream() seed section does not reference INFLIGHT[activeSid]. "
|
|
"Reconnect must read accumulated text from durable INFLIGHT state."
|
|
)
|
|
assert "reconnecting" in seed_section, (
|
|
"attachLiveStream() seed section does not branch on `reconnecting`. "
|
|
"The reconnect path must distinguish a fresh open from a re-attach."
|
|
)
|
|
# The seed must NOT use querySelector to read DOM content for the initial text.
|
|
idx = seed_section.find("let assistantText")
|
|
assert idx != -1, "let assistantText not found in seed section — capture window too narrow or variable renamed"
|
|
seed_up_to_assistantText = seed_section[:idx]
|
|
assert "querySelector" not in seed_up_to_assistantText, (
|
|
"attachLiveStream() reads DOM via querySelector before seeding assistantText. "
|
|
"Use INFLIGHT state exclusively; DOM queries are unreliable for off-screen sessions."
|
|
)
|
|
|
|
|
|
def test_syncInflightAssistantMessage_exists_as_closure_inside_attachLiveStream():
|
|
"""syncInflightAssistantMessage() must be defined as a closure inside attachLiveStream()
|
|
so it closes over the live assistantText and reasoningText accumulators.
|
|
|
|
If it were a module-level function it could not access the per-stream state,
|
|
breaking the token→INFLIGHT sync for every concurrent stream.
|
|
"""
|
|
attach_start = MESSAGES_JS.find("function attachLiveStream(")
|
|
assert attach_start != -1, "attachLiveStream() not found."
|
|
# syncInflightAssistantMessage definition must appear after attachLiveStream opens.
|
|
sync_def = MESSAGES_JS.find("function syncInflightAssistantMessage(){", attach_start)
|
|
assert sync_def != -1, (
|
|
"syncInflightAssistantMessage() is not defined inside attachLiveStream(). "
|
|
"It must be a closure over the stream accumulators (assistantText, reasoningText)."
|
|
)
|
|
# And there must NOT be a top-level definition before attachLiveStream.
|
|
top_level_def = MESSAGES_JS.find("function syncInflightAssistantMessage(){")
|
|
assert top_level_def == sync_def, (
|
|
"syncInflightAssistantMessage() has a top-level definition. "
|
|
"It must live inside attachLiveStream() to close over per-stream state."
|
|
)
|