Merge PR #7628 into stage/0924-a

# Conflicts:
#	static/messages.js
This commit is contained in:
nesquena-hermes committed 2026-09-24 03:15:02 +00:00
commit 0b8d6d6fd8
13 files changed
+1206 -38

No files matched your search

+363
View File
@@ -0,0 +1,363 @@
"""Behavioral tests for #7628 review round: /compress paging restore + suffix
strategy for repeated identical turns.
Round-3 review (nesquena-hermes 2026-09-19) found two data-integrity blockers:
1. [CORE] A successful /compress returns the FULL transcript, but
`_applyManualCompressionResult` did not restore `_messagesTruncated` /
`_oldestIdx` — the bounded preflight's values (e.g. truncated=true,
oldestIdx=70) survived, producing a false "70 older messages" affordance
and shifting every absolute transcript index by 70.
2. [SILENT] `_restoreSettledSession` preservation used
`(_stagedMatchesCurrentPrefix || _stagedMatchesCurrentSuffix)`, so when
identical repeated turns made BOTH comparisons succeed the prefix branch
spliced at the wrong offset — silently dropping and duplicating rows.
The server truncation signal now decides: truncated => suffix-exclusive.
Node-backed tests extract the real functions from static/ and drive them
against a stub DOM (same harness pattern as test_issue5224).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
_ROOT = Path(__file__).resolve().parents[1]
_COMMANDS_JS = _ROOT / "static" / "commands.js"
_MESSAGES_JS = _ROOT / "static" / "messages.js"
_SESSIONS_JS = _ROOT / "static" / "sessions.js"
_NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(_NODE is None, reason="node is required to execute message runtime tests")
_DRIVER = r"""
const fs = require('fs');
const commandsSrc = fs.readFileSync(process.argv[2], 'utf8');
const messagesSrc = fs.readFileSync(process.argv[3], 'utf8');
const sessionsSrc = fs.readFileSync(process.argv[4], 'utf8');
const scenario = JSON.parse(process.argv[5] || '{}');
function extractFunction(source, name) {
const markers = [`async function ${name}(`, `function ${name}(${name}`, `function ${name}(`, `${name}=async(`, `${name}=async (`];
let start = -1;
for (const marker of markers) {
start = source.indexOf(marker);
if (start >= 0) break;
}
if (start < 0) throw new Error(`missing ${name}`);
let i = source.indexOf('{', start);
if (i < 0) throw new Error(`missing body for ${name}`);
let depth = 1; i++;
while (i < source.length) {
const ch = source[i];
if (ch === '{') depth++;
if (ch === '}') { depth--; if (depth === 0) return source.slice(start, i + 1); }
i++;
}
throw new Error(`unterminated body for ${name}`);
}
function install(name, src) {
const body = extractFunction(src, name);
// Drop the `async function name(` or `function name(` prefix, keep body.
const eq = body.indexOf('{');
const inner = body.slice(eq);
globalThis[name] = new Function(`return (${name === 'jumpToSessionStart' ? 'async function' : 'function'} ${inner})`)();
}
// ---- helpers from messages.js (mirror test_issue5224 set) ----
const MESSAGE_HELPERS = [
"_isMarkerOnlyAssistantMessage", "_streamRecoveryControlMessageText",
"_streamRecoveryControlMessage", "_filterRecoveryControlMessages",
"_replaceMarkerOnlyAssistantWithStreamError", "_messageIdentityKey",
"_isHistoricalAnchorActivityScene", "_carryForwardEphemeralTurnFields",
"_isTerminalStreamErrorMarkerMessage", "_restoreSettledSession",
"_mergeSettledToolCallsWithLiveMetadata",
];
for (const h of MESSAGE_HELPERS) {
const body = extractFunction(messagesSrc, h);
const factory = new Function(`${body}; return ${h};`);
globalThis[h] = factory();
}
// ---- compress result applier from commands.js ----
const COMPRESS_BODY = extractFunction(commandsSrc, '_applyManualCompressionResult');
// Rebuild as a normal function (it contains `await` – keep async).
globalThis._applyManualCompressionResult = new Function(
'return (' + COMPRESS_BODY + ')'
)();
function runtimeStubs() {
const calls = [];
globalThis.S = { session: { session_id: 's1' }, messages: [], toolCalls: [] };
globalThis.activeSid = 's1';
globalThis.streamId = 'stream-1';
globalThis.assistantText = false;
globalThis._messagesTruncated = !!scenario.prestateTruncated;
globalThis._oldestIdx = scenario.prestateOldestIdx || 0;
globalThis._messagesGeneration = 1;
globalThis._loadingOlder = false;
globalThis._loadingSessionId = null;
globalThis._messageRenderWindowSize = 50;
globalThis._msgLimitMax = scenario.msgLimitMax || 200;
globalThis._INITIAL_MSG_LIMIT = 50;
globalThis._EPHEMERAL_TURN_FIELDS = ['_turnUsage','_turnDuration','_turnTps','_gatewayRouting','_statusCard','_anchor_stream_id','_anchor_activity_scene'];
globalThis._isActiveSession = () => scenario.isActiveSession !== false;
globalThis._isSessionCurrentPane = () => scenario.isSessionCurrentPane !== false;
globalThis._isSessionActivelyViewed = () => !!scenario.isSessionActivelyViewed;
globalThis._closeSource = () => calls.push('closeSource');
globalThis._clearStreamEndRecovery = () => calls.push('clearStreamEndRecovery');
globalThis._clearOwnerInflightState = () => calls.push('clearOwnerInflight');
globalThis.clearLiveToolCards = () => calls.push('clearLiveToolCards');
globalThis.removeThinking = () => calls.push('removeThinking');
globalThis._flushReasoningToAnchor = () => calls.push('flushReasoning');
globalThis._applyToAnchor = () => calls.push('applyToAnchor');
globalThis._hydrateTodosFromSession = () => calls.push('hydrateTodos');
globalThis._scheduleAnchorRegistryCleanup = () => calls.push('scheduleAnchorRegistryCleanup');
globalThis._smdEndParser = () => calls.push('smdEndParser');
globalThis._markSessionCompletionUnread = () => calls.push('markCompletionUnread');
globalThis._markSessionViewed = () => calls.push('markSessionViewed');
globalThis.localStorage = { setItem: () => calls.push('setLocalStorageItem'), getItem: () => null, removeItem: () => calls.push('removeLocalStorageItem') };
globalThis._setActiveSessionUrl = () => calls.push('setActiveSessionUrl');
globalThis.showToast = () => calls.push('showToast');
globalThis._clearApprovalForOwner = () => calls.push('clearApprovalForOwner');
globalThis._clearClarifyForOwner = () => calls.push('clearClarifyForOwner');
globalThis._streamFadeCleanupReduceMotionListener = () => calls.push('streamFadeCleanup');
globalThis._cancelThrottledSnapshotTimer = () => calls.push('cancelThrottledSnapshot');
globalThis._clearAnchorProseIncrementalNode = () => calls.push('clearAnchorProse');
globalThis._cancelAnimationFramePendingStreamRender = () => calls.push('cancelRaf');
globalThis.finalizeThinkingCard = () => calls.push('finalizeThinkingCard');
globalThis.syncTopbar = () => calls.push('syncTopbar');
globalThis.renderMessages = () => calls.push('renderMessages');
globalThis.renderSessionList = () => calls.push('renderSessionList');
globalThis.updateQueueBadge = () => calls.push('updateQueueBadge');
globalThis._setActivePaneIdleIfOwner = () => calls.push('setActivePaneIdle');
globalThis.setBusy = () => calls.push('setBusy');
globalThis.setComposerStatus = () => calls.push('setComposerStatus');
globalThis.setCompressionUi = () => calls.push('setCompressionUi');
globalThis.setStatus = () => calls.push('setStatus');
globalThis.msgContent = (m) => (m && m.content && typeof m.content === 'string' ? m.content : '');
globalThis._isContextCompactionMessage = () => false;
globalThis._setCompressionSessionLock = () => calls.push('setCompressionSessionLock');
globalThis._messageRenderableMessageCount = () => scenario.messageRenderableCount || 50;
globalThis._currentMessageRenderWindowSize = () => scenario.currentWindowSize || 12;
globalThis._restoreMessageRenderWindowAfterSettledRender = () => calls.push('restoreRenderWindow');
globalThis.projectSessionArtifactsForOwner = () => calls.push('projectArtifacts');
globalThis._streamFinalized = false;
globalThis._persistTimer = null;
globalThis._attachProjectedAnchorSceneToLastAssistant = () => calls.push('attachProjected');
globalThis._isMessagePaneNearBottom = () => true;
globalThis._isMessageReaderUnpinned = () => false;
globalThis._queueDrainSid = null;
// reducer used by _restoreSettledSession (mirrors live code)
return calls;
}
(async () => {
const calls = runtimeStubs();
if (scenario.action === 'compress_restores_paging') {
// Pre-state: bounded preflight left truncated=true, oldestIdx=70.
globalThis._messagesTruncated = true;
globalThis._oldestIdx = 70;
const fullRows = [];
for (let i = 0; i < 100; i++) {
fullRows.push({ role: i % 2 === 0 ? 'user' : 'assistant', content: `row-${i}`, _ts: `ts-${i}` });
}
const payload = {
session: {
session_id: 's1',
message_count: 100,
messages: fullRows,
tool_calls: [],
_messages_truncated: false,
_messages_offset: 0,
},
};
await globalThis._applyManualCompressionResult(payload, '', 100, '/compress');
console.log(JSON.stringify({
action: 'compress_restores_paging',
messagesTruncated: !!globalThis._messagesTruncated,
oldestIdx: globalThis._oldestIdx,
messageCountAfter: Array.isArray(S.messages) ? S.messages.length : -1,
calls,
}));
return;
}
if (scenario.action === 'settle_repeated_turns') {
// Bounded settle against a transcript containing repeated identical turns
// (same role/content but distinct _ids). Round-2 bug: BOTH prefix and
// suffix matched, prefix won, and rows were dropped+duplicated.
const state = scenario.state || {};
globalThis.S.session = { session_id: 's1', message_count: state.messageCount };
globalThis.S.messages = state.messages || [];
globalThis.streamId = 'stream-7628';
globalThis.S.activeStreamId = 'stream-7628';
globalThis.api = async () => scenario.apiPayload || { session: null };
const status = await globalThis._restoreSettledSession({}, { status: true, preserveVisibleOnShorterTerminalSnapshot: true });
const msgs = Array.isArray(globalThis.S.messages) ? globalThis.S.messages : [];
console.log(JSON.stringify({
action: 'settle_repeated_turns',
status,
messages: msgs.map((m) => ({ role: m.role, content: m.content, _ts: m._ts, _id: m._id || null })),
oldestIdx: globalThis._oldestIdx,
messagesTruncated: !!globalThis._messagesTruncated,
terminalMarkerCount: msgs.filter(globalThis._isTerminalStreamErrorMarkerMessage).length,
calls,
}));
return;
}
if (scenario.action === 'load_older_paging') {
// After a bounded recovery (offset=70), "Load earlier" must request a page
// keyed off _oldestIdx (no gap and no overlap), and merge must close the
// window: final oldestIdx === requested offset.
globalThis._messagesTruncated = true;
globalThis._oldestIdx = scenario.oldestIdx || 70;
const existing = [];
for (let i = 70; i < 100; i++) existing.push({ role: i % 2 === 0 ? 'user' : 'assistant', content: `row-${i}`, _ts: `ts-${i}` });
globalThis.S.messages = existing;
globalThis.S.session = { session_id: 's1', message_count: 100 };
const requested = [];
globalThis.api = async (url, opts) => {
requested.push(String(url));
// Return one older page of size 50 (rows 20..69).
const older = [];
for (let i = 20; i < 70; i++) older.push({ role: i % 2 === 0 ? 'user' : 'assistant', content: `row-${i}`, _ts: `ts-${i}` });
return {
session: { session_id: 's1', message_count: 100, messages: older, _messages_truncated: true, _messages_offset: 20 },
};
};
// Extract the live loader from sessions.js.
const loaderBody = extractFunction(sessionsSrc, '_loadOlderMessages');
const loaderFactory = new Function(`${loaderBody}; return _loadOlderMessages;`);
const loader = loaderFactory();
await loader.call(globalThis);
console.log(JSON.stringify({
action: 'load_older_paging',
requested,
messagesTruncated: !!globalThis._messagesTruncated,
oldestIdx: globalThis._oldestIdx,
messagesLen: Array.isArray(S.messages) ? S.messages.length : -1,
loadingOlder: !!globalThis._loadingOlder,
}));
return;
}
throw new Error(`unknown action: ${scenario.action}`);
})().catch((err) => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
@pytest.fixture(scope="module")
def driver_path(tmp_path_factory):
driver = tmp_path_factory.mktemp("7628_driver") / "driver.js"
driver.write_text(_DRIVER, encoding="utf-8")
return str(driver)
def _run_scenario(driver_path: str, scenario: dict) -> dict:
command = [_NODE, driver_path, str(_COMMANDS_JS), str(_MESSAGES_JS), str(_SESSIONS_JS), json.dumps(scenario)]
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", check=False, timeout=30)
if result.returncode != 0:
raise RuntimeError(f"node driver failed: {result.stderr}")
return json.loads(result.stdout.strip())
def test_compress_success_restores_paging_state(driver_path):
"""A successful /compress returning the full transcript must reset the
bounded preflight's paging signals before render (#7628 blocker 1)."""
outcome = _run_scenario(driver_path, {"action": "compress_restores_paging"})
assert outcome["messagesTruncated"] is False, "compress success must clear _messagesTruncated"
assert outcome["oldestIdx"] == 0, f"compress success must reset _oldestIdx to 0, got {outcome['oldestIdx']}"
assert outcome["messageCountAfter"] == 100
def test_settle_bounded_preserves_no_drop_no_dup(driver_path):
"""A bounded settle against repeated identical turns must not drop or
duplicate rows (#7628 blocker 2, exact reviewer reproduction).
All user rows share one identity key (same role/content/_ts), all assistant
rows share one too — _messageIdentityKey collides across every pair. A
bounded tail [u3,a3,u2,a2] therefore matches BOTH the visible prefix
(u1,a1,u2,a2…) and the visible suffix (…u2,a2) at key level. The round-2
`||` code preferred the prefix branch: splice offset = staged.length
dropped u1/a1 and re-emitted u3/a3 (duplicate). The server truncation
signal must force suffix-exclusive splicing.
"""
_u = {"role": "user", "content": "What's the status?", "_ts": "t-u"}
_a = {"role": "assistant", "content": "All good.", "_ts": "t-a"}
# Visible before settle: u1,a1,u2,a2,u3,a3 + Connection-interrupted marker.
state_messages = [
{**_u, "_id": 1}, {**_a, "_id": 2},
{**_u, "_id": 3}, {**_a, "_id": 4},
{**_u, "_id": 5}, {**_a, "_id": 6},
{"role": "assistant", "content": "**Connection interrupted:** The browser lost the live SSE connection before the response finished.", "_ts": "t-err", "_id": 7},
]
outcome = _run_scenario(driver_path, {
"action": "settle_repeated_turns",
"isSessionCurrentPane": True,
"state": {
"messageCount": 12,
"messages": state_messages,
},
"apiPayload": {
"session": {
"session_id": "s1",
"active_stream_id": None,
"pending_user_message": None,
"message_count": 12,
"messages": [
# Reviewer's tail: [u3,a3,u2,a2] — identity keys collide
# with u1/a1/u2/a2/u3/a3 above, so prefix AND suffix both
# compare equal.
{**_u, "_id": 8}, {**_a, "_id": 9},
{**_u, "_id": 10}, {**_a, "_id": 11},
],
"_messages_truncated": True,
"_messages_offset": 4,
},
},
})
assert outcome["status"] == "restored", outcome
ids = [m["_id"] for m in outcome["messages"]]
# No dropped id, no dup id (the prefix-wins bug duplicated u3/a3 and
# dropped u1/a1 — every retained row must be unique).
assert len(ids) == len(set(ids)), f"duplicate rows survived settle: {ids}"
# Staged (authoritative) rows must all be present, plus the marker once.
assert set(ids) == {8, 9, 10, 11, 7}, f"unexpected transcript after fix: {ids}"
# Identity (role|_ts|content) counts: staged rows legitimately repeat
# (two identical user pairs in the tail), but the bug added a THIRD copy by
# splicing the visible tail back in. user<=2, assistant<=2, marker exactly 1.
keys = [(m["role"], m["_ts"], m["content"]) for m in outcome["messages"]]
kc = {}
for k in keys:
kc[k] = kc.get(k, 0) + 1
user_key = ("user", "t-u", "What's the status?")
asst_key = ("assistant", "t-a", "All good.")
assert kc.get(user_key, 0) == 2, f"user rows must appear exactly twice (staged), got {kc.get(user_key, 0)}: {keys}"
assert kc.get(asst_key, 0) == 2, f"assistant rows must appear exactly twice (staged), got {kc.get(asst_key, 0)}: {keys}"
assert outcome["terminalMarkerCount"] == 1, outcome
assert outcome["oldestIdx"] == 4, f"offset must follow server, got {outcome['oldestIdx']}"
assert outcome["messagesTruncated"] is True
def test_bounded_recovery_prepares_gap_free_load_earlier(driver_path):
"""After a bounded recovery (oldestIdx=70), Load Earlier must page from the
authoritative offset so the fetched range has no gap and no overlap
(#7628)."""
outcome = _run_scenario(driver_path, {"action": "load_older_paging", "oldestIdx": 70, "msgLimitMax": 80})
assert outcome["requested"], "loader must have issued an api call"
url = outcome["requested"][0]
assert "msg_before=70" in url, f"load-earlier must page from authoritative offset: {url}"
assert "msg_limit=" in url, url
@@ -0,0 +1,343 @@
"""Behavioural regressions for the #7628 review round-3 findings.
The round-3 gate reproduced two data-integrity defects against the shipped
functions and asked for tests that reach them (the source-assertion suite
stayed green through both reproductions):
1. **A successful `/compress` kept the bounded preflight's paging state.**
The success path assigned the full-transcript response without restoring
``_messagesTruncated`` / ``_oldestIdx``, so after a compression that
returned 100 full rows the client still showed "70 older messages" and
every absolute transcript index was off by 70 — which is what edit /
fork / regenerate address rows by.
2. **Prefix match won over suffix match when both succeeded.** A bounded
settle returns a *suffix* of the durable transcript, but the old
``prefix || suffix`` expression preferred the prefix branch, so a
transcript with repeated identical turns spliced at the wrong offset:
``u1/a1`` silently dropped, ``u3/a3`` duplicated.
These tests execute the real decision code rather than grepping it: the
splice strategy block is extracted from ``static/messages.js`` verbatim and
driven through a Node VM with the paging signals as the only variable, and
the compress success path is asserted at source level for the restore
ordering (that block issues a live ``/api/session`` fetch, so driving it
would need a network).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from tests.js_source_extract import extract_function
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
COMMANDS_JS = (ROOT / "static" / "commands.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
# ── extraction helpers ───────────────────────────────────────────────────────
_SPLICE_DEPS = [
"_messageIdentityKey",
"_isTerminalStreamErrorMarkerMessage",
"_streamRecoveryControlMessage",
"_filterRecoveryControlMessages",
"_carryForwardEphemeralTurnFields",
]
def _splice_block() -> str:
"""The bounded-settle splice decision, verbatim from messages.js.
Spans the terminal-marker detection and the prefix/suffix comparisons
through the ``S.messages`` assignment — the region the round-3 gate
patched. Everything before it (paging restore, session assignment) is set
up as harness state instead.
"""
start_marker = "const _currentVisibleEndsWithTerminalMarker=("
start = MESSAGES_JS.find(start_marker)
assert start >= 0, "terminal-marker detection not found in static/messages.js"
end_marker = "S.messages=_filterRecoveryControlMessages(_resolvedMessages || []);"
end = MESSAGES_JS.find(end_marker, start)
assert end >= 0, "S.messages assignment after the splice not found"
return MESSAGES_JS[start : end + len(end_marker)]
def _dependency_functions() -> list[str]:
"""The splice block's transitive pure dependencies, in definition order.
The extraction is closure-driven: start from the helpers the block names,
then keep pulling the helpers those helpers reference until the set is
closed. That keeps the harness honest when the production code grows a
new dependency — a missing one fails the node run instead of silently
stubbing behaviour away.
"""
import re
needed: list[str] = list(_SPLICE_DEPS)
resolved: list[str] = []
seen: set[str] = set()
while needed:
name = needed.pop(0)
if name in seen:
continue
seen.add(name)
try:
body = extract_function(MESSAGES_JS, name)
except AssertionError:
continue
resolved.append(name)
for ref in re.findall(r"\b(_[a-zA-Z][a-zA-Z0-9_]*)\s*\(", body):
if ref not in seen and ref != name:
needed.append(ref)
# Definition order matters only for hoisted `function` declarations, which
# JS hoists anyway; keep the resolved order for readability.
return [extract_function(MESSAGES_JS, name) for name in resolved]
def _compress_success_block() -> str:
"""The `/compress` success path that assigns the full-transcript response.
Anchored on the bounded preflight fetch's continuation: the block runs
after a successful compression response and must restore the paging
signals before ``renderMessages()``.
"""
anchor = "const summary=data&&data.summary;"
start = COMMANDS_JS.rfind(anchor)
assert start >= 0, "compress success block not found in static/commands.js"
assign = COMMANDS_JS.rfind("S.messages=", 0, start)
assert assign >= 0, "S.messages assignment before the summary block not found"
return COMMANDS_JS[assign:start]
# ── node harness ─────────────────────────────────────────────────────────────
_HARNESS = r"""
const deps = __DEPS__;
const splice = __SPLICE__;
const S = { session: { session_id: 'sid' }, messages: [] };
let _messagesTruncated = false;
let _oldestIdx = 0;
let _adoptRegenerationRevision = () => {};
let _hydrateTodosFromSession = () => {};
let _markSessionCompletionUnread = () => {};
let _setActiveSessionUrl = () => {};
let _attachProjectedAnchorSceneToLastAssistant = () => {};
let _replaceMarkerOnlyAssistantWithStreamError = () => false;
const store = {};
globalThis.localStorage = {
getItem: (k) => (Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
};
// Closure-scope constant that lives just above the extracted helper in
// messages.js (extract_function only captures the function body).
const _EPHEMERAL_TURN_FIELDS = ['_turnUsage','_turnDuration','_turnTps','_gatewayRouting','_statusCard','_anchor_stream_id','_anchor_activity_scene'];
for (const fn of deps) eval(fn);
// Scenario: repeated identical turns in the visible window, a terminal
// marker at the end, and a bounded durable tail whose ids ALSO appear
// earlier — so both the prefix and the suffix comparison succeed.
function makeTurn(n) {
return [
{ role: 'user', id: `u-${n}`, content: `prompt ${n}` },
{ role: 'assistant', id: `a-${n}`, content: `answer ${n}` },
];
}
S.messages = [];
for (const n of [1, 2, 3]) S.messages.push(...makeTurn(n));
// The real terminal marker shape: an assistant row whose content starts with
// the Connection-interrupted prefix (what _isTerminalStreamErrorMarkerMessage
// actually matches).
S.messages.push({
role: 'assistant',
id: 'marker',
content: '**Connection interrupted:** The browser lost the live SSE connection before the response finished.',
});
const durableTail = [
{ role: 'user', id: 'u-3', content: 'prompt 3' },
{ role: 'assistant', id: 'a-3', content: 'answer 3' },
];
const session = {
session_id: 'sid',
messages: durableTail,
_messages_truncated: __TRUNCATED__,
_messages_offset: 4,
};
_messagesTruncated = __TRUNCATED__;
_oldestIdx = __OFFSET__;
// The state the splice block reads, mirroring the lines just above it.
const _currentMessages = S.messages.slice();
const _currentVisibleMessages = _filterRecoveryControlMessages(_currentMessages);
const _stagedMessages = _carryForwardEphemeralTurnFields(_currentMessages, session.messages);
const preserveVisibleOnShorterTerminalSnapshot = true;
S.session = session;
eval(splice);
const out = {
truncated: _messagesTruncated,
oldestIdx: _oldestIdx,
rows: (S.messages || []).map(m => `${m.role}:${m.id}`),
};
console.log(JSON.stringify(out));
"""
def _run_node(script: str) -> dict:
result = subprocess.run(
[NODE, "-e", script],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, (
f"node subprocess failed:\n--- stdout ---\n{result.stdout}\n"
f"--- stderr ---\n{result.stderr}"
)
return json.loads(result.stdout.strip().splitlines()[-1])
def _drive(*, truncated: bool, offset: int) -> dict:
deps = "[" + ",".join(json.dumps(f) for f in _dependency_functions()) + "]"
splice = json.dumps(_splice_block())
script = (
_HARNESS.replace("__DEPS__", deps)
.replace("__SPLICE__", splice)
.replace("__TRUNCATED__", "true" if truncated else "false")
.replace("__OFFSET__", str(offset))
)
return _run_node(script)
# ── tests ────────────────────────────────────────────────────────────────────
def test_bounded_settle_with_repeated_identical_turns_drops_and_duplicates_nothing():
"""The round-3 SILENT finding: identical repeated turns must not splice wrong.
The visible window holds repeated identical turns, so the durable tail's
ids appear at BOTH the head and the tail of the window and both the
prefix and the suffix comparison succeed. The pre-fix
``prefix || suffix`` expression preferred the prefix branch and spliced at
the head offset — dropping ``u2/a2`` and ``u3/a3`` from the window even
though the server returned them as a truncated suffix (``_oldestIdx=4``).
The truncation signal must select the suffix branch instead.
The two branches produce observably different row lists on this shape,
which is what makes this a regression test rather than a tautology: with
the prefix branch restored the splice keeps the wrong rows.
"""
out = _drive(truncated=True, offset=4)
rows = out["rows"]
# The durable tail rows are present exactly once each — the pre-fix
# prefix branch duplicated them.
assert rows.count("user:u-3") == 1, rows
assert rows.count("assistant:a-3") == 1, rows
# No row appears twice anywhere.
assert len(rows) == len(set(rows)), f"duplicate rows in {rows}"
# The suffix splice keeps the durable tail plus the rows that follow it
# in the visible window (the terminal marker). The prefix branch would
# splice at the head offset and re-emit u2/a2/u3/a3.
assert rows == ["user:u-3", "assistant:a-3", "assistant:marker"], (
"the suffix splice must keep durable-tail + the rows after it; got "
f"{rows} (extra rows mean the prefix branch won)"
)
# Paging follows the response so absolute indices line up.
assert out["truncated"] is True
assert out["oldestIdx"] == 4
def test_bounded_settle_keeps_the_response_paging_signals():
"""Paging flags follow the server's truncation signal, not the preflight."""
out = _drive(truncated=True, offset=4)
assert out["truncated"] is True
assert out["oldestIdx"] == 4
def test_untruncated_snapshot_clears_the_bounded_preflight_state():
"""An untruncated response must clear the bounded preflight's state.
This is the `/compress` success-path shape from the CORE finding: the
preflight bounded the tail, the response carries the full transcript, and
the client must end at ``truncated=false`` / ``offset=0`` so absolute
transcript indices line up again.
"""
out = _drive(truncated=False, offset=0)
assert out["truncated"] is False, (
"an untruncated response must clear _messagesTruncated; the bounded "
f"preflight state leaked (got {out['truncated']})"
)
assert out["oldestIdx"] == 0
def test_compress_success_path_restores_paging_before_render():
"""Source lock: the compress success path restores paging before render.
The round-3 CORE reproduction was a successful `/compress` whose response
carried 100 full rows while the client stayed at
``_messagesTruncated=true, _oldestIdx=70``. The restore must appear
between the response assignment and ``renderMessages()`` — the same
ordering the settle and cancel paths already use.
"""
block = _compress_success_block()
assign_idx = block.find("S.messages=")
restore_idx = block.find("_messagesTruncated=")
offset_idx = block.find("_oldestIdx=")
render_idx = block.find("renderMessages()")
assert assign_idx >= 0, "compress success path must assign S.messages"
assert restore_idx > assign_idx, (
"_messagesTruncated must be restored from the response AFTER the "
"assignment — the preflight's bounded value otherwise survives"
)
assert offset_idx > assign_idx, "_oldestIdx must be restored from _messages_offset"
assert render_idx > restore_idx and render_idx > offset_idx, (
"paging must be restored BEFORE renderMessages() so the rendered rows "
"and any absolute index computation see the restored values"
)
# The restore must read the response's own signals, not hardcode false.
assert "_messages_truncated" in block, (
"the restore must read data.session._messages_truncated rather than "
"hardcoding _messagesTruncated=false"
)
assert "_messages_offset" in block, (
"the restore must read data.session._messages_offset"
)
def test_truncation_signal_selects_suffix_strategy_exclusively():
"""Source lock: the strategy is a truncation-gated ternary, never an `||`.
The pre-fix expression was ``_stagedMatchesCurrentPrefix ||
_stagedMatchesCurrentSuffix``: when repeated identical turns made both
comparisons succeed the prefix branch won and spliced at the wrong
offset. The gate is now ``_truncatedRecovery ? suffix : prefix``.
"""
block = _splice_block()
assert "_truncatedRecovery" in block, (
"the splice strategy must consult the truncation signal"
)
assert "_truncatedRecovery?_stagedMatchesCurrentSuffix" in block.replace(" ", ""), (
"a truncated recovery must select the SUFFIX comparison exclusively"
)
# The old defect: a bare `||` between the two comparisons.
assert "||_stagedMatchesCurrentSuffix" not in block.replace(" ", ""), (
"prefix and suffix comparisons must not be OR-ed; preferring the "
"prefix when both match splices repeated turns at the wrong offset"
)
+3 -1
View File
@@ -219,7 +219,9 @@ class TestCancelledTurnPersistenceGuards:
assert "const _applyCancelSessionPayload=(sessionPayload)=>" in block
assert "const _cancelSessionPayload=_cancelData&&typeof _cancelData.session==='object'?_cancelData.session:null;" in block
assert "if(_applyCancelSessionPayload(_cancelSessionPayload)) return;" in block
assert "const data=await api(`/api/session?session_id=${encodeURIComponent(activeSid)}`);" in block
# #7310/#7625: the HTTP fallback is a bounded tail now — the full
# transcript was being re-walked/re-redacted on every cancel recovery.
assert "const data=await api(`/api/session?session_id=${encodeURIComponent(activeSid)}&messages=1&resolve_model=0&msg_limit=30&expand_renderable=1`);" in block
assert block.index("if(_applyCancelSessionPayload(_cancelSessionPayload)) return;") < block.index("const data=await api("), (
"Cancel handler must apply the terminal SSE session payload before falling back "
"to /api/session so captured _partial reasoning/tool rows are visible immediately."
@@ -109,7 +109,6 @@ function buildRuntime() {
globalThis.removeThinking = () => calls.push('removeThinking');
globalThis._flushReasoningToAnchor = () => calls.push('flushReasoning');
globalThis._applyToAnchor = () => calls.push('applyToAnchor');
globalThis._attachProjectedAnchorSceneToLastAssistant = () => calls.push('attachProjected');
globalThis._hydrateTodosFromSession = () => calls.push('hydrateTodos');
globalThis._scheduleAnchorRegistryCleanup = () => calls.push('scheduleAnchorRegistryCleanup');
globalThis._smdEndParser = () => calls.push('smdEndParser');
@@ -141,6 +140,15 @@ function buildRuntime() {
globalThis._messageRenderWindowSize = 20;
globalThis._streamFinalized = !!scenario.streamFinalized;
globalThis._persistTimer = null;
globalThis._oldestIdx = Number.isFinite(scenario.oldestIdx) ? scenario.oldestIdx : 0;
globalThis._messagesTruncated = !!scenario.messagesTruncated;
globalThis.oldestIdxAtAttach = null;
globalThis.messagesTruncatedAtAttach = null;
globalThis._attachProjectedAnchorSceneToLastAssistant = () => {
calls.push('attachProjected');
globalThis.oldestIdxAtAttach = globalThis._oldestIdx;
globalThis.messagesTruncatedAtAttach = globalThis._messagesTruncated;
};
globalThis.api = async () => scenario.apiPayload || { session: null };
globalThis.msgContent = undefined;
globalThis._isPreservedCompressionTaskListMarkerOnlyText = () => false;
@@ -162,6 +170,10 @@ function buildRuntime() {
status,
messages: messages.map((m) => ({ role: m.role, content: m.content })),
terminalMarkerCount,
oldestIdx: globalThis._oldestIdx,
oldestIdxAtAttach: globalThis.oldestIdxAtAttach,
messagesTruncated: !!globalThis._messagesTruncated,
messagesTruncatedAtAttach: !!globalThis.messagesTruncatedAtAttach,
calls,
}));
return;
@@ -179,6 +191,10 @@ function buildRuntime() {
status,
messages: messages.map((m) => ({ role: m.role, content: m.content })),
terminalMarkerCount,
oldestIdx: globalThis._oldestIdx,
oldestIdxAtAttach: globalThis.oldestIdxAtAttach,
messagesTruncated: !!globalThis._messagesTruncated,
messagesTruncatedAtAttach: !!globalThis.messagesTruncatedAtAttach,
calls,
}));
return;
@@ -421,3 +437,109 @@ def test_terminal_error_marker_is_single_instance_and_not_duplicated(driver_path
assert outcome["terminalMarkerCount"] == 1, f"terminal marker should be deduped to one, got {outcome['terminalMarkerCount']}"
assert outcome["messages"][-1]["content"].startswith("**Connection interrupted:**")
def test_bounded_tail_preserves_terminal_marker_when_server_returns_suffix(driver_path):
"""A bounded settle tail is a suffix of the visible window, not a prefix.
Long-session recovery used to compare the 30-row tail against the start of
S.messages. The prefix check failed, so preserveVisibleOnShorterTerminalSnapshot
dropped the client-side Connection-interrupted marker (#7628).
"""
historical = []
for i in range(2):
historical.append({"role": "user", "content": f"Earlier question {i}", "_ts": f"u{i}"})
historical.append({"role": "assistant", "content": f"Earlier answer {i}", "_ts": f"a{i}"})
tail = [
{"role": "user", "content": "Current question", "_ts": "u-cur"},
{"role": "assistant", "content": "Current fragment one", "_ts": "a-cur"},
]
terminal = {
"role": "assistant",
"content": "**Connection interrupted:** The browser lost the live SSE connection before the response finished.",
"_ts": "err",
}
outcome = _run_scenario(driver_path, {
"action": "restore_shorter_terminal",
"oldestIdx": 0,
"messagesTruncated": False,
"state": {
"session": {"session_id": "session-5224", "message_count": 7},
"messages": historical + tail + [terminal],
"activeStreamId": "stream-5224",
},
"apiPayload": {
"session": {
"session_id": "session-5224",
"active_stream_id": None,
"pending_user_message": None,
"messages": tail,
"_messages_truncated": True,
"_messages_offset": 4,
},
},
"activeSid": "session-5224",
"streamId": "stream-5224",
"isActiveSession": True,
"isSessionCurrentPane": True,
"isSessionActivelyViewed": False,
})
observed = [(item["role"], item["content"]) for item in outcome["messages"]]
assert observed == [
("user", "Current question"),
("assistant", "Current fragment one"),
("assistant", "**Connection interrupted:** The browser lost the live SSE connection before the response finished."),
], f"bounded tail must keep the terminal recovery suffix: {observed}"
assert outcome["terminalMarkerCount"] == 1, (
f"expected the terminal marker to survive a suffix-only tail, got {outcome['terminalMarkerCount']}"
)
assert outcome["oldestIdx"] == 4, f"paging offset should come from the bounded response, got {outcome['oldestIdx']}"
assert outcome["oldestIdxAtAttach"] == 4, (
f"anchor persist must see the new offset, got {outcome['oldestIdxAtAttach']}"
)
assert outcome["messagesTruncated"] is True
assert outcome["messagesTruncatedAtAttach"] is True
def test_settle_refreshes_paging_offset_before_anchor_persist(driver_path):
"""_persistSettledAnchorScene reads _oldestIdx; a stale 0 would write the scene to the wrong row."""
outcome = _run_scenario(driver_path, {
"action": "restore_fuller_terminal",
"oldestIdx": 0,
"messagesTruncated": False,
"state": {
"session": {"session_id": "session-5224", "message_count": 1},
"messages": [
{"role": "assistant", "content": "Old interrupted shell", "_ts": "old"},
],
"activeStreamId": "stream-5224",
},
"apiPayload": {
"session": {
"session_id": "session-5224",
"active_stream_id": None,
"pending_user_message": None,
"messages": [
{"role": "user", "content": "Question about data?", "_ts": "u1"},
{"role": "assistant", "content": "Settled first segment", "_ts": "a1"},
{"role": "assistant", "content": "Settled second segment", "_ts": "a2"},
{"role": "assistant", "content": "Settled third segment", "_ts": "a3"},
],
"_messages_truncated": True,
"_messages_offset": 40,
},
},
"activeSid": "session-5224",
"streamId": "stream-5224",
"isActiveSession": True,
"isSessionCurrentPane": True,
"isSessionActivelyViewed": False,
})
assert outcome["status"] == "restored"
assert outcome["oldestIdx"] == 40
assert outcome["oldestIdxAtAttach"] == 40, (
f"stale offset 0 must not leak into anchor persist, got {outcome['oldestIdxAtAttach']}"
)
assert outcome["messagesTruncatedAtAttach"] is True
@@ -816,20 +816,26 @@ def test_stream_end_restore_attaches_projected_anchor_scene_before_render():
filter_idx = restore.index("S.messages=_filterRecoveryControlMessages(_resolvedMessages || []);")
attach_idx = restore.index("_attachProjectedAnchorSceneToLastAssistant(S.messages);")
render_idx = restore.index("syncTopbar();renderMessages({preserveScroll:true})")
assert carry_idx < filter_idx < attach_idx < render_idx
offset_idx = restore.index("_oldestIdx=session._messages_offset||0")
assert offset_idx < carry_idx < filter_idx < attach_idx < render_idx
assert "_stagedMatchesCurrentSuffix" in restore
def test_cancel_settlement_attaches_projected_anchor_scene_before_render():
cancel = _event_listener_body(MESSAGES_JS, "cancel")
fetch_idx = cancel.index("const _nextMsgs3018=(sessionPayload.messages||[]).filter(m=>m&&m.role);")
offset_idx = cancel.index("_oldestIdx=sessionPayload._messages_offset||0")
attach_idx = cancel.index("_attachProjectedAnchorSceneToLastAssistant(_nextMsgs3018);")
carry_idx = cancel.index("S.messages=_carryForwardEphemeralTurnFields(S.messages||[], _nextMsgs3018);")
render_idx = cancel.index("renderMessages({preserveScroll:true});")
assert fetch_idx < attach_idx < carry_idx < render_idx
assert fetch_idx < offset_idx < attach_idx < carry_idx < render_idx
embedded_idx = cancel.index("if(_applyCancelSessionPayload(_cancelSessionPayload)) return;")
fallback_get_idx = cancel.index("const data=await api(`/api/session?session_id=${encodeURIComponent(activeSid)}`);")
# #7310/#7625: the HTTP fallback is a bounded tail now (full-transcript
# reloads were stacking on every cancel recovery); ordering contract below
# is unchanged.
fallback_get_idx = cancel.index("const data=await api(`/api/session?session_id=${encodeURIComponent(activeSid)}&messages=1&resolve_model=0&msg_limit=30&expand_renderable=1`);")
fallback_apply_idx = cancel.index("if(data&&data.session) _applyCancelSessionPayload(data.session);")
assert embedded_idx < fallback_get_idx < fallback_apply_idx
+229
View File
@@ -0,0 +1,229 @@
"""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"