"""Regression test for issue #4658: collapsed tool-card rows in the transparent-stream view lost their inline summary/preview. Symptom: collapsed transparent tool rows showed only the bare tool name (`read_file`, `terminal`, `search_files`) with no hint of what the call did; expanding still worked. Caused by the interaction of two changes: * 69072ac34 moved buildToolCard's collapsed preview to an arg summary and #4411 (6951d10a) then blanked that preview for the common arg/shell case (it assumes the row NAME carries the target — true for the worklog view's action-label name), and * the transparent view (_decorateTransparentEventRow) overrides the row name back to the BARE tool name (_toolShortName), so it had neither the target-carrying label nor a preview. Fix: _decorateTransparentEventRow now populates the `.tool-card-preview` span from a quiet, TARGET-based summary (_transparentToolSummary) — path/command/ query/skill, never the raw result JSON — so collapsed rows are self-describing again while honoring the "keep collapsed previews quiet" intent (test_tool_card_preview_summary.py). This drives the ACTUAL functions from static/ui.js via node + jsdom-free DOM shims, and runs the same render path live streaming and persisted reload share (both go through _decorateTransparentEventRow(buildToolCard(tc))). """ import json import shutil import subprocess from pathlib import Path import pytest REPO_ROOT = Path(__file__).parent.parent.resolve() UI_JS_PATH = REPO_ROOT / "static" / "ui.js" NODE = shutil.which("node") pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") _DRIVER_SRC = r""" const fs = require('fs'); const src = fs.readFileSync(process.argv[2], 'utf8'); // ── Minimal DOM shims (enough for buildToolCard + _decorateTransparentEventRow) function makeEl(tag){ const el = { tagName: (tag||'div').toUpperCase(), _attrs: {}, _classes: new Set(), children: [], style: {}, dataset: {}, _html: '', textContent: '', parentNode: null, firstChild: null, classList: { add(c){el._classes.add(c)}, remove(c){el._classes.delete(c)}, toggle(c,on){const w=on===undefined?!el._classes.has(c):!!on; if(w)el._classes.add(c); else el._classes.delete(c);}, contains(c){return el._classes.has(c)}, }, setAttribute(k,v){el._attrs[k]=String(v);}, getAttribute(k){return k in el._attrs?el._attrs[k]:null;}, removeAttribute(k){delete el._attrs[k];}, hasAttribute(k){return k in el._attrs;}, appendChild(c){c.parentNode=el; el.children.push(c); el.firstChild=el.children[0]; return c;}, insertBefore(c,ref){c.parentNode=el; const i=el.children.indexOf(ref); if(i<0)el.children.push(c); else el.children.splice(i,0,c); el.firstChild=el.children[0]; return c;}, insertAdjacentHTML(){/* detail body not needed for collapsed-preview assertions */}, closest(){return null}, focus(){}, get innerHTML(){return el._html;}, set innerHTML(v){ el._html=String(v); // Parse just the spans we assert on out of the template buildToolCard emits. el.children=[]; const mk=(cls,text)=>{const c=makeEl('span'); c._classes=new Set(cls.split(' ').filter(Boolean)); c.textContent=text; c.parentNode=el; el.children.push(c);}; const grab=(cls)=>{const re=new RegExp('([\\s\\S]*?)<\\/span>'); const m=el._html.match(re); return m?m[1]:null;}; const nameLabel=grab('tool-card-name-label'); if(nameLabel!==null){ const nameSpan=makeEl('span'); nameSpan._classes=new Set(['tool-card-name']); nameSpan.textContent=nameLabel; nameSpan.parentNode=el; el.children.push(nameSpan); } const preview=grab('tool-card-preview'); if(preview!==null) mk('tool-card-preview', preview); const header=makeEl('div'); header._classes=new Set(['tool-card-header']); // re-home the parsed spans under a header element so querySelector('.tool-card-header') works header.children=el.children.slice(); header.children.forEach(c=>c.parentNode=header); const card=makeEl('div'); card._classes=new Set(['tool-card']); card.appendChild(header); el.children=[card]; el.firstChild=card; }, querySelector(sel){ return el._find(sel, false)[0]||null; }, querySelectorAll(sel){ return el._find(sel, true); }, _find(sel, all){ const want=sel.split(',').map(s=>s.trim().replace(/^\./,'')); const out=[]; const walk=(node)=>{ for(const c of (node.children||[])){ if(want.some(w=>c._classes&&c._classes.has(w))) out.push(c); walk(c);} }; walk(el); return out; }, }; return el; } global.document = { createElement: (t)=>makeEl(t), querySelectorAll:()=>[], querySelector:()=>null }; global.window = {}; global.CSS = { escape: s=>s }; global.t = undefined; global.li = () => ''; global.esc = s => String(s==null?'':s).replace(/&/g,'&').replace(//g,'>'); global.toolIcon = () => ''; global._snippetLooksLikeDiff = () => false; global._colorDiffLines = s => s; global._attachCopyButton = () => {}; global._attachProgressBar = () => {}; global._wireTransparentHeaderToggle = () => {}; global._transparentToolDetailHtml = () => '
'; // _toolDisclosureIdentity pulls in worklog hashing helpers irrelevant to the // collapsed-preview assertion; stub it (only feeds a data- attribute). global._toolDisclosureIdentity = () => ''; function extractFunc(name){ const re=new RegExp('function\\s+'+name+'\\s*\\('); const start=src.search(re); if(start<0) throw new Error(name+' not found'); let i=src.indexOf('{',start); let depth=1; i++; while(depth>0&&i