"""Render-level regression: stale compaction markers must not hide the current summary. A session can carry historical ``[CONTEXT COMPACTION]`` markers in its loaded transcript while ``session.compression_anchor_summary`` already describes a newer compaction that no loaded marker references. ``renderMessages()`` must still render that current summary as its own settled card (as ``master`` did) and the preserved task cards must be attached to exactly one owner. The scenario drives the real ``renderMessages()`` through the same Node harness used by ``tests/test_anchor_fallback_ownership.py``; only the DOM shim gains a minimal ``innerHTML`` → ``firstElementChild`` parser so the compaction card nodes built by ``renderMessages()`` are real elements. """ from __future__ import annotations import json import textwrap from tests.test_anchor_fallback_ownership import ( _function_source, _run_node_script, _ui_js, ) _REAL_FUNCTIONS = ( "_transparentStreamOrderedParts", "_legacySettledFallbackHasToolMetadata", "_isContextCompactionText", "_isContextCompactionMessage", "_compactionSummarySegment", "_isPreservedCompressionTaskListMarkerText", "_isPreservedCompressionTaskListMessage", "_preservedCompressionTaskListPreview", "_latestTodoToolItems", "_hasActiveTodoItems", "_latestPreservedCompressionTaskListMessages", "_compressionStatusCardHtml", "_preservedCompressionTaskListCardHtml", "_preservedCompressionTaskListCardsHtml", "_latestCompressionReferenceMessage", "_shouldShowSettledCompressionReference", "_compactionDigestText", "_compactionCardPreview", "_compressionReferenceCardHtml", "_loadedCompactionMarkerRawIdxs", "_selectCompactionCardPlacements", "_insertCompactionCardNodes", "_insertPreservedCompressionTaskFallback", "_pinCompactionCardAtTop", "_pinSettledCompressionReferenceAtTop", # #2051: renderMessages() inserts a message block through this helper, so it has to be # evaluated with it. The shim's createElement() returns no template `content`, so the # helper takes its insertAdjacentHTML fallback here, exactly as before. "_insertSegmentBlock", "renderMessages", ) _DOM_SHIM = r""" class FakeClassList { constructor(el) { this.el = el; } _set() { return new Set(String(this.el.className || '').split(/\s+/).filter(Boolean)); } contains(name) { return this._set().has(name); } add(...names) { const set = this._set(); names.forEach((name) => set.add(name)); this.el.className = Array.from(set).join(' '); } remove(...names) { const set = this._set(); names.forEach((name) => set.delete(name)); this.el.className = Array.from(set).join(' '); } toggle(name) { const set = this._set(); if (set.has(name)) set.delete(name); else set.add(name); this.el.className = Array.from(set).join(' '); return set.has(name); } } function unescapeHtml(value) { return String(value) .replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<') .replace(/>/g, '>').replace(/&/g, '&'); } // Minimal HTML → FakeElement tree parser. Only used when renderMessages() // reads `firstElementChild` after assigning innerHTML (the compaction / // reference / preserved-task card nodes). Text nodes are dropped; the // assertions below rely on data-* attributes only. function parseHtmlInto(parent, html) { const tagRe = /<(\/)?([A-Za-z][\w-]*)((?:\s+[\w:-]+(?:="[^"]*")?)*)\s*(\/)?>/g; const stack = [parent]; let match; while ((match = tagRe.exec(html))) { if (match[1]) { if (stack.length > 1) stack.pop(); continue; } const el = new FakeElement(match[2]); const attrRe = /([\w:-]+)(?:="([^"]*)")?/g; let attr; while ((attr = attrRe.exec(match[3] || ''))) { el.setAttribute(attr[1], attr[2] === undefined ? '' : unescapeHtml(attr[2])); } stack[stack.length - 1].appendChild(el); if (!match[4]) stack.push(el); } } class FakeElement { constructor(tag = 'div') { this.tagName = tag.toUpperCase(); this.children = []; this.parentElement = null; this.dataset = {}; this.attributes = {}; this.className = ''; this.id = ''; this.hidden = false; this._innerHTML = ''; this._pendingHtml = null; this.style = {}; this.classList = new FakeClassList(this); } set innerHTML(value) { this._innerHTML = String(value); for (const child of this.children) child.parentElement = null; this.children = []; this._pendingHtml = this._innerHTML ? this._innerHTML : null; } get innerHTML() { return this._innerHTML; } get firstElementChild() { if (this._pendingHtml !== null) { const html = this._pendingHtml; this._pendingHtml = null; parseHtmlInto(this, html); } return this.children[0] || null; } appendChild(child) { if (child.parentElement) child.remove(); child.parentElement = this; this.children.push(child); return child; } insertBefore(child, ref) { if (child.parentElement) child.remove(); child.parentElement = this; const idx = this.children.indexOf(ref); if (idx < 0) this.children.push(child); else this.children.splice(idx, 0, child); return child; } remove() { if (!this.parentElement) return; const idx = this.parentElement.children.indexOf(this); if (idx >= 0) this.parentElement.children.splice(idx, 1); this.parentElement = null; } setAttribute(name, value) { this.attributes[name] = String(value); if (name === 'id') this.id = String(value); if (name === 'class') this.className = String(value); if (name.startsWith('data-')) this.dataset[dataKey(name)] = String(value); } getAttribute(name) { if (name === 'id') return this.id || null; if (name === 'class') return this.className || null; if (name.startsWith('data-')) { const value = this.dataset[dataKey(name)]; return value === undefined ? null : String(value); } return this.attributes[name] === undefined ? null : this.attributes[name]; } removeAttribute(name) { delete this.attributes[name]; if (name.startsWith('data-')) delete this.dataset[dataKey(name)]; } matches(selector) { return matchesSelector(this, selector); } closest(selector) { let node = this; while (node) { if (matchesSelector(node, selector)) return node; node = node.parentElement; } return null; } querySelectorAll(selector) { const found = []; const visit = (node) => { for (const child of node.children) { if (matchesSelector(child, selector)) found.push(child); visit(child); } }; visit(this); return found; } querySelector(selector) { return this.querySelectorAll(selector)[0] || null; } insertAdjacentHTML() {} } function dataKey(name) { return String(name).slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); } function matchesSelector(el, selector) { return String(selector || '').split(',').some((part) => matchesSimple(el, part.trim())); } function matchesSimple(el, selector) { if (!selector) return false; const negated = []; const baseSelector = selector.replace(/:not\(([^()]*)\)/g, (_, inner) => { negated.push(String(inner || '').trim()); return ''; }).trim(); if (negated.some((inner) => inner && matchesSimple(el, inner))) return false; if (!baseSelector) return true; const classMatches = [...baseSelector.matchAll(/\.([A-Za-z0-9_-]+)/g)].map((m) => m[1]); if (classMatches.some((name) => !el.classList.contains(name))) return false; const attrMatches = [...baseSelector.matchAll(/\[([^=\]]+)(?:=["']?([^"'\]]+)["']?)?\]/g)]; for (const match of attrMatches) { const value = el.getAttribute(match[1]); if (value === null) return false; if (match[2] !== undefined && String(value) !== String(match[2])) return false; } const idMatch = baseSelector.match(/#([A-Za-z0-9_-]+)/); if (idMatch && el.id !== idMatch[1]) return false; const tagMatch = baseSelector.match(/^[A-Za-z][A-Za-z0-9_-]*/); if (tagMatch && el.tagName.toLowerCase() !== tagMatch[0].toLowerCase()) return false; return true; } const elements = { msgInner: new FakeElement('div'), emptyState: new FakeElement('div'), }; global.window = {}; global.document = { createElement: (tag) => new FakeElement(tag), getElementById: (id) => elements[id] || null, }; global.performance = { now: () => 1 }; global.requestAnimationFrame = (fn) => fn(); global.setTimeout = (fn) => fn(); function $(id) { return elements[id] || null; } function isTransparentStream() { return false; } function isCompactWorklogMode() { return true; } function isSimplifiedToolCalling() { return true; } function t(key) { return key; } function li() { return ''; } function esc(value) { return String(value == null ? '' : value) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function msgContent(message) { if (Array.isArray(message.content)) { return message.content.filter((p) => p && p.type === 'text').map((p) => p.text || p.content || '').join('\n'); } return String(message.content || ''); } let S; const INFLIGHT = {}; let _loadingSessionId = null; let _messageRenderWindowSid = null; let _messageUserUnpinned = false; let _programmaticScroll = false; let _programmaticScrollSetAt = 0; let _sessionHtmlCacheSid = null; let _messagesTruncated = false; let _oldestIdx = 0; let virtualStart = 0; const _sessionHtmlCache = new Map(); const _recycleStash = new Map(); const _msgNodeRecycleEnabled = false; const _recycleResetAttrs = []; const _ERR_MSG_RE = /__never__/; function _captureMessageScrollSnapshot() { return null; } function _resetMessageRenderWindow(sid) { _messageRenderWindowSid = sid; } function _getVisibleMessagesWithIdx() { return S.messages.map((m, rawIdx) => ( m && m.role !== 'tool' && !_isContextCompactionMessage(m) && !_isPreservedCompressionTaskListMessage(m) ) ? { m, rawIdx } : null).filter(Boolean); } function _messageVirtualKeepTailCount() { return 20; } function _currentMessageVirtualWindow(vis) { const start = Math.min(virtualStart, vis.length); return { virtualized: start > 0, start, end: vis.length, topPad: start > 0 ? 120 * start : 0, bottomPad: 0, total: vis.length, tailStart: vis.length, }; } function _messageVirtualWindowKeyFor() { return 'all'; } function _messageRenderCacheSignature() { return 'sig'; } function _compressionStateForCurrentSession() { return null; } function clearCompressionUi() {} function _handoffStateForCurrentSession() { return null; } function _captureWorklogDetailDisclosureState() { return null; } function _applySessionNavigationPrefs() {} function _messageVirtualSpacer() { return new FakeElement('div'); } function _wireMessageWindowLoadEarlierButton() {} function _compressionAnchorIndex() { return null; } function _assistantTurnFinalVisibleContentMap() { return new Map(); } function _assistantTurnVisibleContentMap() { return new Map(); } function _engineAwareCompressionCopy() { return { label: 'context_compaction_label', preview: 'reference_only_label' }; } function _createAssistantTurn() { const turn = new FakeElement('div'); turn.className = 'assistant-turn'; const blocks = new FakeElement('div'); blocks.className = 'assistant-turn-blocks'; turn.appendChild(blocks); return turn; } function _assistantTurnBlocks(turn) { return turn ? turn.querySelector('.assistant-turn-blocks') : null; } function _setLatestAssistantTurnLandmark() {} function _assistantRoleHtml() { return ''; } function _userMessageDomId(rawIdx) { return `user-${rawIdx}`; } function _messageSessionIndexForRawIdx(rawIdx) { return rawIdx; } function _messageViewportAnchorKeyForMessage() { return 'k'; } function _stripAttachedFilesMarkerForDisplay(value) { return String(value || ''); } function _stripWorkspaceDisplayPrefix(value) { return String(value || ''); } function _stripLeadingAssistantThinkingMarkup(value) { return String(value || ''); } function _getCachedRender(value) { return String(value || ''); } function _formatInServerTz() { return ''; } function _formatMessageFooterTimestamp() { return ''; } function _questionJumpButtonHtml() { return ''; } function _formatTurnTps() { return ''; } function isTpsDisplayEnabled() { return false; } function _renderAttachmentHtml() { return ''; } function _isMarkerOnlyAssistantCompressionMessage() { return false; } function _isAssistantEmptyPlaceholderContent() { return false; } function _assistantTurnAnchorSettledFinalAnswer() { return null; } function _worklogReasoningTextFromMessage() { return ''; } function _assistantMessageBelongsInWorklog() { return false; } function _assistantThinkingBelongsInWorklog() { return false; } function _assistantReasoningPayloadText() { return ''; } function _statusCardHtml() { return ''; } function _collectHandoffSummaryStates() { return []; } function _handoffCardsNode() { return null; } function renderCompressionUi() {} function _assistantToolAnchorIdxForMessage(messages, rawIdx) { return rawIdx; } function _cliToolResultSnippet(value) { return String(value || ''); } function _cliPatchSnippetFromArgs() { return ''; } function _cliToolCardSnippet(value) { return String(value || ''); } function _cliToolCardHasDiffSnippet() { return false; } function _toolArgsSnapshot(args) { return args || {}; } function _worklogReasonHtmlFromAnchor() { return ''; } function _normalizeThinkingEchoCompare(value) { return String(value || ''); } function _toolWorklogListEl(group) { return group; } function ensureActivityGroup(parent) { const group = new FakeElement('div'); if (parent) parent.appendChild(group); return group; } function _appendWorklogStep() {} function _syncToolCallGroupSummary() {} function _restoreWorklogDetailDisclosureState() {} function _scrollAfterMessageRender() {} function _maybeRecoverVirtualizedBlankViewport() { return false; } function _updateMessageVirtualMeasurements() {} function postProcessRenderedMessages() {} function _postProcessWithAnchorSuppression() {} function _formatGatewayModelLabel() { return ''; } function _gatewayRoutingFailoverText() { return ''; } function _gatewayModelWarningText() { return ''; } function _usedModelTurnChipLabel() { return ''; } function _formatTurnDuration() { return ''; } function _renderSettledAnchorSceneForMessage() { return false; } """ _SCENARIO = r""" const TOTAL = 220; const STALE_MARKER_IDXS = [40, 100, 160]; const TASK_LIST_IDX = 161; const CURRENT_TOKEN = 'CURRENT-SUMMARY-TOKEN-7124'; const TASK_MARKER = '[Your active task list was preserved across context compression]'; function staleMarker(rawIdx) { return '[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below.\n' + '## Historical Task Snapshot\nUser asked: "stale request ' + rawIdx + '"\n' + '## Goal\nSTALE-MARKER-' + rawIdx + ' historical state\n--- END OF CONTEXT SUMMARY ---'; } function buildMessages() { const messages = []; for (let i = 0; i < TOTAL; i++) { if (STALE_MARKER_IDXS.includes(i)) { messages.push({ role: 'assistant', content: staleMarker(i) }); } else if (i === TASK_LIST_IDX) { messages.push({ role: 'user', content: TASK_MARKER + '\n- [ ] finish the compaction fix' }); } else if (i % 2 === 0) { messages.push({ role: 'user', content: 'question ' + i }); } else { messages.push({ role: 'assistant', content: 'answer ' + i }); } } return messages; } function render(opts) { elements.msgInner = new FakeElement('div'); _sessionHtmlCache.clear(); _sessionHtmlCacheSid = null; virtualStart = opts.virtualStart || 0; _messagesTruncated = !!opts.truncated; _oldestIdx = opts.truncated ? 50 : 0; S = { session: { session_id: opts.sid, compression_anchor_summary: opts.summary, }, messages: buildMessages(), toolCalls: [], busy: false, }; renderMessages(); const inner = elements.msgInner; const cards = inner.querySelectorAll('[data-compression-card]'); const rawText = (node) => String(node.getAttribute('data-raw-text') || ''); const summaryCards = cards.filter((node) => rawText(node).includes(CURRENT_TOKEN)); const taskCards = cards.filter((node) => rawText(node).startsWith(TASK_MARKER)); const owners = inner.querySelectorAll('[data-compaction-task-owner]'); const ownerHasSummary = owners.length === 1 && owners[0].querySelectorAll('[data-compression-card]').some((node) => rawText(node).includes(CURRENT_TOKEN)); const ownerHasTask = owners.length === 1 && owners[0].querySelectorAll('[data-compression-card]').some((node) => rawText(node).startsWith(TASK_MARKER)); const placements = inner.querySelectorAll('[data-compaction-placement]'); const summaryTopIdx = summaryCards.length ? inner.children.findIndex((child) => child.querySelectorAll('[data-compression-card]').some((node) => rawText(node).includes(CURRENT_TOKEN))) : -1; const loadOlderIdx = inner.children.findIndex((child) => child.id === 'loadOlderIndicator'); return { renderedRows: inner.querySelectorAll('[data-msg-idx]').length, currentSummaryVisible: summaryCards.length > 0, currentSummaryCards: summaryCards.length, staleCards: placements.map((node) => node.getAttribute('data-compaction-placement')), staleCardTexts: placements.map((node) => { const card = node.querySelector('[data-compression-card]'); return card ? (/STALE-MARKER-(\d+)/.exec(rawText(card))?.[1] || null) : null; }), taskCards: taskCards.length, taskOwners: owners.length, ownerHasSummary, ownerHasTask, standaloneFallbacks: inner.querySelectorAll('[data-compaction-task-fallback]').length, summaryTopIdx, loadOlderIdx, }; } const NEWER_SUMMARY = 'Compacted context: ' + CURRENT_TOKEN + ' — newest state after the last compaction'; const fullWindow = render({ sid: 'full', summary: NEWER_SUMMARY }); // Visible index 170 maps to raw idx >= 174 (three markers and the task-list // message are not renderable rows), so every stale marker is pre-window. const virtualWindow = render({ sid: 'virtual', summary: NEWER_SUMMARY, virtualStart: 170 }); const truncatedTail = render({ sid: 'truncated', summary: NEWER_SUMMARY, truncated: true }); // Control: the current summary IS referenced by the latest loaded marker, so // the marker card renders it and no second (fallback) card may appear. const matchingSummary = render({ sid: 'matching', summary: 'STALE-MARKER-160 historical state' }); const matchingCards = elements.msgInner.querySelectorAll('[data-compression-card]') .filter((node) => String(node.getAttribute('data-raw-text') || '').includes('STALE-MARKER-160')); console.log(JSON.stringify({ fullWindow, virtualWindow, truncatedTail, matchingSummary: { ...matchingSummary, latestMarkerCards: matchingCards.length, }, })); """ def _scenario_script() -> str: js = _ui_js() evals = "\n".join( f"eval({json.dumps(_function_source(js, name))});" for name in _REAL_FUNCTIONS ) return textwrap.dedent(_DOM_SHIM) + "\n" + evals + "\n" + textwrap.dedent(_SCENARIO) def test_stale_markers_do_not_suppress_current_summary_and_task_owner_is_unique(): result = json.loads(_run_node_script(_scenario_script())) full = result["fullWindow"] assert full["renderedRows"] > 200 assert full["currentSummaryVisible"] is True assert full["currentSummaryCards"] == 1 assert full["staleCards"] == ["inline", "inline", "inline"] assert full["staleCardTexts"] == ["40", "100", "160"] assert full["taskCards"] == 1 assert full["taskOwners"] == 1 assert full["ownerHasSummary"] is True assert full["ownerHasTask"] is True assert full["standaloneFallbacks"] == 0 virtual = result["virtualWindow"] assert virtual["currentSummaryVisible"] is True assert virtual["currentSummaryCards"] == 1 assert virtual["staleCards"] == ["pre-window", "pre-window", "pre-window"] assert virtual["staleCardTexts"] == ["40", "100", "160"] assert virtual["taskCards"] == 1 assert virtual["taskOwners"] == 1 assert virtual["ownerHasSummary"] is True assert virtual["standaloneFallbacks"] == 0 truncated = result["truncatedTail"] assert truncated["currentSummaryVisible"] is True assert truncated["currentSummaryCards"] == 1 assert truncated["loadOlderIdx"] >= 0 # The settled current-summary card is pinned directly under the # load-earlier affordance, before the stale marker cards and rows. assert truncated["summaryTopIdx"] == truncated["loadOlderIdx"] + 1 assert truncated["taskCards"] == 1 assert truncated["taskOwners"] == 1 assert truncated["ownerHasSummary"] is True assert truncated["standaloneFallbacks"] == 0 matching = result["matchingSummary"] assert matching["currentSummaryVisible"] is False assert matching["latestMarkerCards"] == 1 assert matching["staleCards"] == ["inline", "inline", "inline"] assert matching["taskCards"] == 1 assert matching["taskOwners"] == 1 assert matching["ownerHasTask"] is True assert matching["standaloneFallbacks"] == 0