Fixes the #7310 stall class: six frontend recovery paths (offline/bfcache refreshSession, stream-end settle, cancel sync, /compress preflight, /retry, /undo) issued bare GET /api/session reads that re-walked, re-redacted and re-serialized the ENTIRE transcript (4-15s / 28MB on a 5k-row session, #7625), and the same shape shipped the full session-level tool_calls list because windowing was gated on msg_limit being present. Changes: - Server: add explicit msg_limit=all escape hatch; window session-level tool_calls by actual truncation (length comparison) instead of the presence of a limit parameter. The bare no-limit shape keeps its full-transcript contract (pinned by test_state_db_reconciliation_preserves_sidecar_order_when_timestamps_collide and the run-journal snapshot test). - Client: refreshSession / _restoreSettledSession / cancel sync / /compress preflight / /retry / /undo now request the bounded tail (msg_limit=30) and restore the _messages_truncated/_messages_offset paging gate instead of hardcoding 'not truncated'; jump-to-start (_ensureAllMessagesLoaded) and outline jump opt in to the full transcript via msg_limit=all. Tests: test_session_default_window.py covers helper semantics, per-call-site bounded/all shapes, truncation-signal restoration, and a regression guard that no bare GET /api/session fetch remains in static/. 454 related tests pass; reverse-verified (reverting the fixes turns the tests red).
172 lines
6.6 KiB
Python
172 lines
6.6 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 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" |