Four upstream tests read renderMessages out of ui.js and run a slice of it against a fake segment object. Since the message block goes in through _insertSegmentBlock, three of them stopped at "ReferenceError: _insertSegmentBlock is not defined" and the fourth still pinned the old seg.insertAdjacentHTML line, which took twelve of the fifteen test shards red on this branch and nowhere else. test_status_command_card.py defines the helper in its own harness, one line that does what the real helper does without a browser. test_compaction_current_summary_fallback.py and test_anchor_fallback_ownership.py evaluate the real helper from ui.js alongside the functions they already evaluate; their DOM shims return no template content, so it takes its insertAdjacentHTML fallback exactly as before. test_sprint49.py pins the new call, and its point is unchanged: the footer still rides in the same block as the body. A new case in tests/test_issue2051_duplicate_assistant_answer.py pins why the helper exists, which answers a review note: the block is parsed into a template and the nodes are MOVED, so a repeated call adds nothing, and insertAdjacentHTML survives only as the fallback. All three insertions go through it. tests/test_anchor_fallback_ownership.py, tests/test_compaction_current_summary_fallback.py, tests/test_sprint49.py, tests/test_status_command_card.py: 34 passed. The three #2051 files: 12 passed.
533 lines
21 KiB
Python
533 lines
21 KiB
Python
"""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, '>').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
|