test_issue6867 extracts _restoreSettledSession into a bare harness where the module-level _messagesTruncated/_oldestIdx lets are absent; a bare read threw ReferenceError and the whole recovery path died before it could restore. Use typeof guards like the surrounding paging restores. The semantics are unchanged in the real page (both are always declared).
229 lines
9.5 KiB
Python
229 lines
9.5 KiB
Python
"""Tests: GET /api/session full-transcript escape hatch + bounded recovery
|
|
calls — #7310 / #7625.
|
|
|
|
Bare ``/api/session`` reads (no ``msg_limit``) were issued by six frontend
|
|
recovery paths (offline/bfcache ``refreshSession``, stream-end settle, cancel
|
|
sync, /compress preflight, /retry, /undo). Each used to re-walk, re-redact and
|
|
re-serialize the ENTIRE transcript (4-15s / 28MB on a 5k-row session, measured
|
|
in #7625). Those call sites now request a bounded tail explicitly
|
|
(``msg_limit=30``), while the two paths that address rows by absolute
|
|
transcript index (outline jump, jump-to-start) opt in to the full transcript
|
|
via the explicit ``msg_limit=all`` escape hatch.
|
|
|
|
The bare no-limit HTTP shape deliberately keeps its historical full-transcript
|
|
contract: upstream contract tests pin tool-row preservation
|
|
(``test_state_db_reconciliation_preserves_sidecar_order_when_timestamps_collide``)
|
|
and the runtime-journal live snapshot
|
|
(``test_paginated_session_followup_does_not_repeat_runtime_snapshot``) on that
|
|
shape, so the fix is enforced at the frontend call sites rather than by
|
|
changing the server default.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from api.routes import (
|
|
_MAX_MSG_LIMIT,
|
|
_parse_msg_limit,
|
|
_resolve_effective_msg_limit,
|
|
)
|
|
|
|
_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
# ── helper: _resolve_effective_msg_limit ──
|
|
|
|
|
|
def test_bare_request_keeps_full_transcript_contract():
|
|
"""A bare reload (no msg_limit, no msg_before) keeps the historical
|
|
full-transcript contract — server-side default windowing was rejected
|
|
because contract tests pin tool-row preservation and the runtime-journal
|
|
snapshot on this shape (#7310/#7625)."""
|
|
limit, explicit_all = _resolve_effective_msg_limit(None)
|
|
assert limit is None
|
|
assert explicit_all is False
|
|
|
|
|
|
def test_empty_and_malformed_keep_contract():
|
|
assert _resolve_effective_msg_limit("") == (None, False)
|
|
assert _resolve_effective_msg_limit("not-a-number") == (None, False)
|
|
|
|
|
|
def test_explicit_all_resolves_to_full_transcript():
|
|
""""msg_limit=all → (None, True): the explicit full-transcript escape
|
|
hatch for outline jump / jump-to-start."""
|
|
limit, explicit_all = _resolve_effective_msg_limit("all")
|
|
assert limit is None
|
|
assert explicit_all is True
|
|
|
|
|
|
def test_explicit_all_case_insensitive():
|
|
assert _resolve_effective_msg_limit("ALL") == (None, True)
|
|
assert _resolve_effective_msg_limit(" All ") == (None, True)
|
|
|
|
|
|
def test_numeric_limit_unchanged():
|
|
"""Explicit numeric limits keep the clamp behaviour — real pagination is
|
|
unaffected."""
|
|
assert _resolve_effective_msg_limit("30") == (30, False)
|
|
assert _resolve_effective_msg_limit("9999") == (_MAX_MSG_LIMIT, False)
|
|
assert _resolve_effective_msg_limit("0") == (1, False)
|
|
|
|
|
|
def test_parse_msg_limit_all_returns_none():
|
|
"""The parse helper returns None for 'all' — the handler distinguishes it
|
|
from a bare request via _resolve_effective_msg_limit."""
|
|
assert _parse_msg_limit("all") is None
|
|
assert _parse_msg_limit("ALL") is None
|
|
|
|
|
|
# ── frontend call sites: bounded shapes (no bare GET /api/session left) ──
|
|
|
|
_UI_JS = (_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
_MESSAGES_JS = (_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
|
_SESSIONS_JS = (_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
_COMMANDS_JS = (_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
|
_OUTLINE_JS = (_ROOT / "static" / "outline.js").read_text(encoding="utf-8")
|
|
|
|
_BOUNDED = "&messages=1&resolve_model=0&msg_limit=30&expand_renderable=1"
|
|
_ALL_QS = "&messages=1&resolve_model=0&msg_limit=all"
|
|
|
|
|
|
def test_refresh_session_uses_bounded_tail():
|
|
"""Offline/bfcache recovery refreshSession must not pull the full
|
|
transcript (#7310/#7625)."""
|
|
assert _BOUNDED in _UI_JS
|
|
|
|
|
|
def test_settle_and_cancel_paths_use_bounded_tail():
|
|
"""stream_end settle recovery AND cancel sync must not pull the full
|
|
transcript."""
|
|
assert _BOUNDED in _MESSAGES_JS
|
|
assert _MESSAGES_JS.count(_BOUNDED) >= 2
|
|
|
|
|
|
def test_cancel_payload_restores_truncation_signal():
|
|
"""_applyCancelSessionPayload keeps the Load-earlier paging gate honest
|
|
after a bounded cancel reload."""
|
|
assert "!!sessionPayload._messages_truncated" in _MESSAGES_JS
|
|
assert "sessionPayload._messages_offset||0" in _MESSAGES_JS
|
|
|
|
|
|
def test_settle_restores_truncation_signal():
|
|
"""_restoreSettledSession keeps the Load-earlier paging gate honest after
|
|
a bounded settle reload."""
|
|
assert "!!session._messages_truncated" in _MESSAGES_JS
|
|
assert "session._messages_offset||0" in _MESSAGES_JS
|
|
|
|
|
|
def _restore_settled_session_body():
|
|
start = _MESSAGES_JS.index("async function _restoreSettledSession")
|
|
end = _MESSAGES_JS.index("function _handleStreamError", start)
|
|
return _MESSAGES_JS[start:end]
|
|
|
|
|
|
def _apply_cancel_session_payload_body():
|
|
start = _MESSAGES_JS.index("const _applyCancelSessionPayload=(sessionPayload)=>")
|
|
end = _MESSAGES_JS.index("Prefer the canonical session snapshot", start)
|
|
return _MESSAGES_JS[start:end]
|
|
|
|
|
|
def test_settle_refreshes_paging_before_anchor_persist():
|
|
"""Anchor persist reads _oldestIdx; a bounded tail must refresh it first (#7628)."""
|
|
restore = _restore_settled_session_body()
|
|
offset_idx = restore.index("_oldestIdx=session._messages_offset||0")
|
|
attach_idx = restore.index("_attachProjectedAnchorSceneToLastAssistant(S.messages);")
|
|
assert offset_idx < attach_idx
|
|
|
|
|
|
def test_cancel_refreshes_paging_before_anchor_persist():
|
|
"""Cancel recovery has the same persist-before-offset trap as settle (#7628)."""
|
|
cancel = _apply_cancel_session_payload_body()
|
|
offset_idx = cancel.index("_oldestIdx=sessionPayload._messages_offset||0")
|
|
attach_idx = cancel.index("_attachProjectedAnchorSceneToLastAssistant(_nextMsgs3018);")
|
|
assert offset_idx < attach_idx
|
|
|
|
|
|
def test_settle_preserves_terminal_marker_against_bounded_suffix():
|
|
"""Long-session bounded tails are suffixes of S.messages, not prefixes (#7628)."""
|
|
restore = _restore_settled_session_body()
|
|
assert "_stagedMatchesCurrentSuffix" in restore
|
|
assert "(_truncatedRecovery?_stagedMatchesCurrentSuffix:_stagedMatchesCurrentPrefix)" in restore
|
|
|
|
|
|
def test_settle_truncation_signal_decides_match_strategy():
|
|
"""The server's truncation signal must decide prefix-vs-suffix, never an `||`:
|
|
bounded settles get suffix-exclusive matching (identical repeated turns make
|
|
BOTH comparisons succeed; the prefix branch then splices at the wrong offset
|
|
and silently drops/duplicates rows) (#7628)."""
|
|
restore = _restore_settled_session_body()
|
|
assert "_truncatedRecovery=(typeof _messagesTruncated!=='undefined'&&!!_messagesTruncated)||(typeof _oldestIdx!=='undefined'&&!!(_oldestIdx>0))" in restore
|
|
# The splice offset must follow the SAME truncation decision as the match
|
|
# strategy — no fallthrough to prefix when both match.
|
|
assert "_truncatedRecovery?_stagedSuffixStart+_stagedMessages.length:_stagedMessages.length" in restore
|
|
|
|
|
|
def test_apperror_embedded_session_refreshes_paging_before_anchor_persist():
|
|
"""The apperror embedded-session path has the same persist-before-offset trap (#7628)."""
|
|
start = _MESSAGES_JS.index("source.addEventListener('apperror'")
|
|
end = _MESSAGES_JS.index("source.addEventListener('warning'", start)
|
|
block = _MESSAGES_JS[start:end]
|
|
offset_idx = block.index("_oldestIdx=d.session._messages_offset||0")
|
|
attach_idx = block.index("_attachProjectedAnchorSceneToLastAssistant(_nextMsgs3018);")
|
|
assert offset_idx < attach_idx
|
|
|
|
|
|
def test_compress_preflight_uses_bounded_tail():
|
|
"""/compress preflight only needs session existence + the current tail."""
|
|
assert _BOUNDED in _COMMANDS_JS
|
|
|
|
|
|
def test_retry_uses_bounded_tail():
|
|
"""/retry recovery must not pull the full transcript."""
|
|
assert _COMMANDS_JS.count(_BOUNDED) >= 1
|
|
|
|
|
|
def test_undo_uses_bounded_tail():
|
|
"""/undo recovery must not pull the full transcript."""
|
|
assert _COMMANDS_JS.count(_BOUNDED) >= 2
|
|
|
|
|
|
def test_recovery_paths_restore_truncation_signal():
|
|
"""preflight/retry/undo must NOT hardcode _messagesTruncated=false — they
|
|
read the server's truncation signal instead."""
|
|
assert "!!(live.session._messages_truncated)" in _COMMANDS_JS
|
|
assert "!!(data.session._messages_truncated)" in _COMMANDS_JS
|
|
|
|
|
|
def test_jump_to_start_requests_explicit_full_transcript():
|
|
"""_ensureAllMessagesLoaded genuinely needs everything (absolute-index
|
|
addressing of the earliest rows) — it must opt in via msg_limit=all."""
|
|
assert _ALL_QS in _SESSIONS_JS
|
|
|
|
|
|
def test_outline_jump_requests_explicit_full_transcript():
|
|
"""Outline jump addresses rows by absolute transcript index — it must opt
|
|
in via msg_limit=all."""
|
|
assert _ALL_QS in _OUTLINE_JS
|
|
|
|
|
|
def test_no_bare_session_fetch_remains_in_frontend():
|
|
"""Regression guard: NO static/ caller may issue a bare GET /api/session
|
|
(no messages=/msg_limit= params). Use the bounded tail shape or the
|
|
explicit msg_limit=all escape hatch."""
|
|
bare_template = re.compile(
|
|
r"api\(`/api/session\?session_id=\$\{encodeURIComponent\([^)]*\}\)`\)"
|
|
)
|
|
bare_concat = re.compile(
|
|
r"api\('/api/session\?session_id='\s*\+\s*encodeURIComponent\([^)]*\)\)"
|
|
)
|
|
for name, src in (
|
|
("ui.js", _UI_JS),
|
|
("messages.js", _MESSAGES_JS),
|
|
("sessions.js", _SESSIONS_JS),
|
|
("commands.js", _COMMANDS_JS),
|
|
("outline.js", _OUTLINE_JS),
|
|
):
|
|
assert not bare_template.findall(src), f"{name} still has a bare template-literal /api/session fetch"
|
|
assert not bare_concat.findall(src), f"{name} still has a bare concatenated /api/session fetch" |