@@ -181,6 +181,7 @@ class Element {
181181 program += "\n render(sample);if(nodes.actors.children.length!==2||!nodes.goals.textContent.includes('Operator goal')||nodes.events.children.length!==1||nodes['agent-grid'].children.length!==1)throw Error('Live renderer failed');\n "
182182 program += """
183183const textNode={nodeType:3,nodeName:'#text',nodeValue:'previous'};const reader={nodeType:1,nodeName:'PRE',childNodes:[textNode],scrollTop:77};const freshReader={nodeType:1,nodeName:'PRE',childNodes:[{nodeType:3,nodeName:'#text',nodeValue:'updated'}]};updateNode(reader,freshReader);if(reader.childNodes[0]!==textNode||textNode.nodeValue!=='updated'||reader.scrollTop!==77)throw Error('Reading node replaced');
184+ latestInference={events:[{time:'2026-10-09T15:30:00Z'}],agents:[{label:'Live reader',stream_output:'fresh answer'}]};render(sample);if(!nodes['agent-grid'].children[0].children[0].children[0].textContent.includes('Live reader'))throw Error('Stale snapshot replaced live output');
184185const old={dataset:{key:'reader'},open:false};let disclosures=[old];document.querySelectorAll=()=>disclosures;rememberDisclosures();
185186const next={dataset:{key:'reader'},open:true};disclosures=[next];restoreDisclosures();if(next.open)throw Error('User-closed detail reopened');
186187next.open=true;rememberDisclosures();const newer={dataset:{key:'reader'},open:false};disclosures=[newer];restoreDisclosures();if(!newer.open)throw Error('User-open detail lost');
@@ -286,3 +287,54 @@ def test_operator_opt_in_records_returned_local_trace_and_preserves_public_outpu
286287 agent = live_status .agent_views (events , [])[0 ]
287288 assert agent ["returned_trace" ] == trace
288289 assert "Public answer" in agent ["declaration" ]
290+
291+
292+ def test_transaction_retains_reading_container_and_scroll_across_updates ():
293+ import shutil
294+ import subprocess
295+
296+ if not shutil .which ("node" ):
297+ pytest .skip ("Node unavailable" )
298+ program = r"""
299+ class Text {
300+ constructor(value){this.nodeType=3;this.nodeName='#text';this.nodeValue=value;this.parentNode=null}
301+ cloneNode(){return new Text(this.nodeValue)}
302+ remove(){if(this.parentNode){const p=this.parentNode;p.childNodes.splice(p.childNodes.indexOf(this),1);this.parentNode=null}}
303+ replaceWith(n){const p=this.parentNode;p.insertBefore(n,this);this.remove()}
304+ }
305+ class Element extends Text {
306+ constructor(tag){super('');this.nodeType=1;this.nodeName=tag.toUpperCase();this.childNodes=[];this.dataset={};this.style={};this.attrs={};this.scrollTop=0;this.scrollLeft=0}
307+ get children(){return this.childNodes.filter(n=>n.nodeType===1)}
308+ get attributes(){return Object.entries(this.attrs).map(([name,value])=>({name,value}))}
309+ get textContent(){return this.childNodes.map(n=>n.nodeType===3?n.nodeValue:n.textContent).join('')}
310+ set textContent(v){this.replaceChildren(new Text(String(v)))}
311+ append(...items){for(const n of items)this.insertBefore(typeof n==='string'?new Text(n):n,null)}
312+ insertBefore(n,before){n.remove();const i=before?this.childNodes.indexOf(before):this.childNodes.length;this.childNodes.splice(i,0,n);n.parentNode=this}
313+ replaceChildren(...items){for(const n of [...this.childNodes])n.remove();this.append(...items)}
314+ after(n){const p=this.parentNode;p.insertBefore(n,p.childNodes[p.childNodes.indexOf(this)+1]||null)}
315+ setAttribute(k,v){this.attrs[k]=String(v)}
316+ getAttribute(k){return this.attrs[k]??null}
317+ hasAttribute(k){return k in this.attrs}
318+ removeAttribute(k){delete this.attrs[k]}
319+ querySelectorAll(selector){const all=[];function visit(n){for(const c of n.children||[]){all.push(c);visit(c)}}visit(this);return all.filter(n=>selector[0]==='#'?n.id===selector.slice(1):selector==='pre'?n.nodeName==='PRE':selector==='[data-scroll-key]'?n.dataset.scrollKey:selector==='details[data-key]'?n.nodeName==='DETAILS'&&n.dataset.key:false)}
320+ querySelector(s){return this.querySelectorAll(s)[0]||null}
321+ cloneNode(deep){const n=new Element(this.nodeName);n.id=this.id;n.dataset={...this.dataset};n.attrs={...this.attrs};n.style={...this.style};n.open=this.open;if(deep)n.append(...this.childNodes.map(c=>c.cloneNode(true)));return n}
322+ }
323+ const body=new Element('body');const document={body,getElementById:id=>body.querySelector('#'+id),querySelectorAll:s=>body.querySelectorAll(s),createElement:t=>new Element(t),createElementNS:(ns,t)=>new Element(t),createTextNode:t=>new Text(t)};
324+ const window={scrollX:0,scrollY:400,scrollTo(x,y){this.scrollX=x;this.scrollY=y}};
325+ const location={reload(){throw Error('Unexpected reload')}};const setInterval=()=>{};const fetch=()=>new Promise(()=>{});
326+ """
327+ for identity in dashboard_layout .REQUIRED_IDS :
328+ program += f"{{const n=new Element('div');n.id={ json .dumps (identity )} ;body.append(n)}}\n "
329+ program += dashboard_layout .APP_SCRIPT
330+ program += r"""
331+ const readingCard=new Element('article');readingCard.dataset.scrollKey='gpu';readingCard.scrollTop=140;
332+ const detail=new Element('details');detail.dataset.key='gpu-proof';detail.open=true;
333+ const summary=new Element('summary');summary.textContent='Evidence';const pre=new Element('pre');pre.id='probe';pre.textContent='old output';pre.scrollTop=73;
334+ detail.append(summary,pre);readingCard.append(detail);body.append(readingCard);const originalText=pre.childNodes[0];
335+ for(let i=0;i<4;i++)stablePaint(()=>{$('probe').textContent='new output '+i});
336+ if(document.getElementById('probe')!==pre||pre.childNodes[0]!==originalText||pre.textContent!=='new output 3')throw Error('Reader DOM identity lost');
337+ if(!detail.open||readingCard.scrollTop!==140||pre.scrollTop!==73||window.scrollY!==400)throw Error('Reading position lost');
338+ """
339+ result = subprocess .run (["node" , "-" ], input = program , text = True , capture_output = True , timeout = 5 )
340+ assert result .returncode == 0 , result .stderr
0 commit comments