"""Regression tests for sidebar lineage collapse helpers.""" import json import shutil import subprocess from pathlib import Path import pytest REPO_ROOT = Path(__file__).parent.parent.resolve() SESSIONS_JS_PATH = REPO_ROOT / "static" / "sessions.js" NODE = shutil.which("node") pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") def _run_node(source: str) -> str: # Pass source via stdin rather than `-e ` argv — the latter is # capped at MAX_ARG_STRLEN (131072 bytes on Linux) and tests that embed # the entire sessions.js file can exceed that. stdin has no such limit. result = subprocess.run( [NODE], input=source, cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=10, ) if result.returncode != 0: raise RuntimeError(result.stderr) return result.stdout.strip() def test_sidebar_lineage_collapse_keeps_latest_tip_and_counts_segments(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_collapseSessionLineageForSidebar')); const sessions = [ {{session_id:'root', title:'Hermes WebUI', message_count:10, updated_at:10, last_message_at:10, _lineage_root_id:'root', _lineage_tip_id:'root'}}, {{session_id:'tip', title:'Hermes WebUI', message_count:20, updated_at:20, last_message_at:20, _lineage_root_id:'root', _lineage_tip_id:'tip'}}, {{session_id:'solo', title:'Other', message_count:5, updated_at:15, last_message_at:15}}, ]; const collapsed = _collapseSessionLineageForSidebar(sessions); console.log(JSON.stringify(collapsed)); """ collapsed = json.loads(_run_node(source)) by_sid = {row["session_id"]: row for row in collapsed} assert set(by_sid) == {"tip", "solo"} assert by_sid["tip"]["_lineage_collapsed_count"] == 2 assert [seg["session_id"] for seg in by_sid["tip"]["_lineage_segments"]] == ["tip", "root"] def test_sidebar_active_state_can_fall_back_to_url_session_during_boot(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} global.S = {{ session: null }}; global.window = {{ location: {{ pathname: '/session/url-active', search: '', hash: '' }} }}; eval(extractFunc('_sessionIdFromLocation')); eval(extractFunc('_activeSessionIdForSidebar')); console.log(_activeSessionIdForSidebar()); """ assert _run_node(source) == "url-active" def test_collapsed_lineage_contains_active_hidden_segment(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_collapseSessionLineageForSidebar')); eval(extractFunc('_sessionLineageContainsSession')); const sessions = [ {{session_id:'root', title:'Hermes WebUI', message_count:10, updated_at:10, last_message_at:10, _lineage_root_id:'root', _lineage_tip_id:'tip'}}, {{session_id:'tip', title:'Hermes WebUI', message_count:20, updated_at:20, last_message_at:20, _lineage_root_id:'root', _lineage_tip_id:'tip'}}, ]; const collapsed = _collapseSessionLineageForSidebar(sessions); console.log(JSON.stringify({{sid: collapsed[0].session_id, containsRoot: _sessionLineageContainsSession(collapsed[0], 'root')}})); """ result = _run_node(source) assert '"sid":"tip"' in result assert '"containsRoot":true' in result def test_parent_present_webui_compression_child_without_lineage_metadata_collapses(): """WebUI-native compression continuations may only carry parent_session_id. When both the preserved parent snapshot and the new continuation are present in the sidebar payload, the continuation should still collapse with its parent instead of appearing as a separate branch-like conversation (#2489). """ js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_collapseSessionLineageForSidebar')); const sessions = [ {{session_id:'parent', title:'Long WebUI conversation', message_count:50, updated_at:10, last_message_at:10, pre_compression_snapshot:true}}, {{session_id:'child', title:'Long WebUI conversation', parent_session_id:'parent', message_count:12, updated_at:20, last_message_at:20}}, ]; const collapsed = _collapseSessionLineageForSidebar(sessions); console.log(JSON.stringify(collapsed)); """ collapsed = json.loads(_run_node(source)) assert [row["session_id"] for row in collapsed] == ["child"] assert collapsed[0]["_lineage_key"] == "parent" assert collapsed[0]["_lineage_collapsed_count"] == 2 assert [seg["session_id"] for seg in collapsed[0]["_lineage_segments"]] == ["child", "parent"] def test_stale_optimistic_compression_tips_collapse_even_when_parents_are_visible(): """Active compression can leave old streaming tips in browser memory. The server/index already expose only the latest tip, but client-side optimistic rows from previous tips may still include parent_session_id links. Those rows carry explicit lineage metadata and must collapse as one sidebar conversation instead of rendering 7/8/9/10 segment duplicates. """ js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_collapseSessionLineageForSidebar')); const sessions = [ {{session_id:'seg7', title:'Graphify', parent_session_id:'seg6', message_count:1141, updated_at:70, last_message_at:70, _lineage_root_id:'root', _compression_segment_count:7}}, {{session_id:'seg8', title:'Graphify', parent_session_id:'seg7', message_count:1254, updated_at:80, last_message_at:80, _lineage_root_id:'root', _compression_segment_count:8, pending_user_message:'old'}}, {{session_id:'seg9', title:'Graphify', parent_session_id:'seg8', message_count:1404, updated_at:90, last_message_at:90, _lineage_root_id:'root', _compression_segment_count:9, active_stream_id:'old-stream'}}, {{session_id:'seg10', title:'Graphify', parent_session_id:'seg9', message_count:1490, updated_at:100, last_message_at:100, _lineage_root_id:'root', _compression_segment_count:10, active_stream_id:'current-stream'}}, ]; const collapsed = _collapseSessionLineageForSidebar(sessions); console.log(JSON.stringify(collapsed)); """ collapsed = json.loads(_run_node(source)) assert [row["session_id"] for row in collapsed] == ["seg10"] assert collapsed[0]["_lineage_collapsed_count"] == 4 assert collapsed[0]["_compression_segment_count"] == 10 assert [seg["session_id"] for seg in collapsed[0]["_lineage_segments"]] == ["seg10", "seg9", "seg8", "seg7"] def test_sidebar_lineage_collapse_prefers_highest_compression_segment_over_touched_parent(): """A touched parent segment must not hide the newer compressed tip. Opening or polling an older segment can refresh its updated_at without adding messages. The collapsed sidebar row must still pick the highest compression segment, otherwise the visible chat jumps back to a parent that lacks the completed assistant answer. """ js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_collapseSessionLineageForSidebar')); const sessions = [ {{session_id:'seg13', title:'Schaue dir die Release (fork)', message_count:2490, updated_at:200, last_message_at:200, _lineage_root_id:'root', _compression_segment_count:13}}, {{session_id:'seg14', title:'Schaue dir die Release (fork)', message_count:2532, updated_at:150, last_message_at:150, _lineage_root_id:'root', _compression_segment_count:14}}, ]; const collapsed = _collapseSessionLineageForSidebar(sessions); console.log(JSON.stringify(collapsed)); """ collapsed = json.loads(_run_node(source)) assert [row["session_id"] for row in collapsed] == ["seg14"] assert collapsed[0]["_lineage_collapsed_count"] == 2 assert [seg["session_id"] for seg in collapsed[0]["_lineage_segments"]] == ["seg14", "seg13"] def test_sidebar_attaches_child_sessions_to_collapsed_hidden_parent_lineage(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTimestampMs')); eval(extractFunc('_isChildSession')); eval(extractFunc('_sessionLineageKey')); eval(extractFunc('_sidebarLineageKeyForRow')); eval(extractFunc('_collapseSessionLineageForSidebar')); eval(extractFunc('_attachChildSessionsToSidebarRows')); const raw = [ {{session_id:'root', title:'Root', updated_at:10, last_message_at:10, _lineage_root_id:'root', _lineage_tip_id:'tip'}}, {{session_id:'tip', title:'Tip', updated_at:20, last_message_at:20, _lineage_root_id:'root', _lineage_tip_id:'tip'}}, {{session_id:'child', title:'Subtask', parent_session_id:'tip', relationship_type:'child_session', _parent_lineage_root_id:'root', updated_at:30, last_message_at:30}}, ]; const collapsed = _collapseSessionLineageForSidebar(raw); const attached = _attachChildSessionsToSidebarRows(collapsed, raw); console.log(JSON.stringify(attached)); """ rows = json.loads(_run_node(source)) assert [row["session_id"] for row in rows] == ["tip"] assert rows[0]["_child_session_count"] == 1 assert rows[0]["_child_sessions"][0]["session_id"] == "child" def test_cross_surface_webui_child_session_remains_top_level_when_parent_is_messaging(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_isChildSession')); eval(extractFunc('_sidebarLineageKeyForRow')); eval(extractFunc('_attachChildSessionsToSidebarRows')); const collapsed = [{{session_id:'telegram_parent', title:'Telegram parent', source_label:'Telegram'}}]; const raw = [ collapsed[0], {{ session_id:'webui_tip', title:'Current WebUI continuation', parent_session_id:'telegram_parent', relationship_type:'child_session', parent_source:'telegram', source_label:'Telegram', session_source:'messaging', raw_source:'telegram', _cross_surface_child_session:true, }}, ]; const rows = _attachChildSessionsToSidebarRows(collapsed, raw); console.log(JSON.stringify(rows)); """ rows = json.loads(_run_node(source)) assert [row["session_id"] for row in rows] == ["telegram_parent", "webui_tip"] assert rows[1].get("_orphan_child_session") is True assert "_child_sessions" not in rows[0] def test_session_segment_count_prefers_visible_collapsed_backend_and_materialized_counts(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionSegmentCount')); const cases = [ _sessionSegmentCount({{_lineage_collapsed_count:3, _compression_segment_count:2, _lineage_segments:[{{session_id:'a'}}, {{session_id:'b'}}]}}), _sessionSegmentCount({{_compression_segment_count:25}}), _sessionSegmentCount({{_lineage_segments:[{{session_id:'tip'}}, {{session_id:'root'}}, {{session_id:'older'}}]}}), _sessionSegmentCount({{_lineage_collapsed_count:1, _compression_segment_count:1}}), _sessionSegmentCount(null), ]; console.log(JSON.stringify(cases)); """ assert json.loads(_run_node(source)) == [3, 25, 3, 0, 0] def test_sidebar_lineage_segment_badge_is_detailed_density_only_and_localized(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8") assert "session-lineage-count" in js assert "const density=(window._sidebarDensity==='detailed'?'detailed':'compact');" in js assert "const showLineageMetadata=density==='detailed';" in js assert "const segmentCount=showLineageMetadata?_sessionSegmentCount(s):0;" in js assert "const lineageSegments=showLineageMetadata?_lineageSegmentsForRender(s,lineageKey):[];" in js assert "const needsLineageReport=showLineageMetadata?_lineageReportNeedsFetch(s,lineageKey,segmentCount):false;" in js assert "const canExpandLineageSegments=showLineageMetadata&&Boolean(" in js assert "t('session_meta_segments', segmentCount)" in js assert "titleRow.appendChild(segmentCountEl);" in js assert ".session-lineage-count{" in css def test_lineage_segment_expansion_static_contract(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8") assert "const _expandedLineageKeys = new Set();" in js assert "const _lineageReportCache = new Map();" in js assert "const _lineageReportInflight = new Map();" in js assert "session-lineage-count,.session-lineage-segments,.session-lineage-segment" in js assert "segmentCountEl.setAttribute('aria-expanded'" in js assert "_expandedLineageKeys.has(lineageKey)" in js assert "_expandedLineageKeys.add(lineageKey)" in js assert "_expandedLineageKeys.delete(lineageKey)" in js assert "_fetchLineageReportForRow(s,lineageKey).then" in js assert "'/api/session/lineage/report?session_id='" in js assert "encodeURIComponent(s.session_id)" in js assert "className='session-lineage-segments'" in js assert "className='session-lineage-segment'" in js assert "const segTitle=_sessionDisplayTitle(seg)||t('session_lineage_segment_untitled');" in js assert "row.title=t('session_lineage_segment_open');" in js assert "await loadSession(seg.session_id);" in js assert ".session-lineage-count.expandable{" in css assert ".session-lineage-count.expandable:hover" in css assert ".session-lineage-segments{" in css assert ".session-lineage-segment{" in css def test_lineage_report_fetch_is_needed_only_when_backend_count_exceeds_materialized_segments(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} const _lineageReportCache = new Map(); const _lineageReportInflight = new Map(); eval(extractFunc('_lineageReportCacheKey')); eval(extractFunc('_lineageLocalSegmentCount')); eval(extractFunc('_lineageReportNeedsFetch')); const backendOnly = {{session_id:'tip', _lineage_key:'root', _compression_segment_count:25}}; const localFull = {{ session_id:'tip', _lineage_key:'root', _compression_segment_count:2, _lineage_segments:[{{session_id:'tip'}}, {{session_id:'root'}}], }}; const before = _lineageReportNeedsFetch(backendOnly, 'root', 25); _lineageReportCache.set('root', {{segments:[{{session_id:'tip'}}, {{session_id:'root'}}]}}); const afterCache = _lineageReportNeedsFetch(backendOnly, 'root', 25); const fullLocal = _lineageReportNeedsFetch(localFull, 'root', 2); console.log(JSON.stringify({{before, afterCache, fullLocal}})); """ assert json.loads(_run_node(source)) == {"before": True, "afterCache": False, "fullLocal": False} def test_cached_lineage_report_segments_merge_with_materialized_segments_without_duplicates_or_children(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} const _lineageReportCache = new Map(); eval(extractFunc('_lineageReportCacheKey')); eval(extractFunc('_lineageSegmentsForRender')); _lineageReportCache.set('root', {{ segments:[ {{session_id:'tip', title:'Tip', role:'tip', started_at:30}}, {{session_id:'root', title:'Root', role:'hidden_segment', started_at:20}}, {{session_id:'older', title:'Older', role:'hidden_segment', started_at:10}}, {{session_id:'child', title:'Child', role:'child_session', started_at:40}}, ], children:[{{session_id:'child', title:'Child', role:'child_session'}}], }}); const row = {{ session_id:'tip', _lineage_key:'root', _lineage_segments:[{{session_id:'tip', title:'Tip'}}, {{session_id:'root', title:'Root'}}], }}; const segments = _lineageSegmentsForRender(row, 'root').map(seg => seg.session_id); console.log(JSON.stringify(segments)); """ assert json.loads(_run_node(source)) == ["root", "older"] def test_lineage_report_fetch_uses_endpoint_once_and_caches_result(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} const _lineageReportCache = new Map(); const _lineageReportInflight = new Map(); let _lineageReportCacheGeneration = 0; const calls = []; function api(path) {{ calls.push(path); return Promise.resolve({{found:true, segments:[{{session_id:'tip'}}, {{session_id:'root'}}]}}); }} eval(extractFunc('_lineageReportCacheKey')); eval(extractFunc('_fetchLineageReportForRow')); (async()=>{{ const row = {{session_id:'tip', _lineage_key:'root'}}; const [first, second] = await Promise.all([ _fetchLineageReportForRow(row, 'root'), _fetchLineageReportForRow(row, 'root'), ]); await _fetchLineageReportForRow(row, 'root'); console.log(JSON.stringify({{ calls, cached:_lineageReportCache.has('root'), same:first===second, }})); }})().catch(err=>{{console.error(err); process.exit(1);}}); """ result = json.loads(_run_node(source)) assert result == { "calls": ["/api/session/lineage/report?session_id=tip"], "cached": True, "same": True, } def test_active_hidden_lineage_segment_auto_expands_parent(): js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} const _expandedChildSessionKeys = new Set(); const _expandedLineageKeys = new Set(); eval(extractFunc('_sidebarLineageKeyForRow')); eval(extractFunc('_syncSidebarExpansionForActiveSession')); const rows = [{{ session_id:'seg10', _lineage_key:'root', _lineage_segments:[ {{session_id:'seg10', updated_at:100}}, {{session_id:'seg9', updated_at:90}}, {{session_id:'seg8', updated_at:80}}, ], }}]; _syncSidebarExpansionForActiveSession(rows, 'seg8'); console.log(JSON.stringify({{lineage:[..._expandedLineageKeys], child:[..._expandedChildSessionKeys]}})); """ assert json.loads(_run_node(source)) == {"lineage": ["root"], "child": []} def test_lineage_segment_locale_keys_are_defined_for_sidebar_locales(): i18n = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8") required = [ "session_meta_segments:", "session_lineage_segment_untitled:", "session_lineage_segment_open:", ] locale_count = i18n.count("session_meta_messages:") for key in required: assert i18n.count(key) >= locale_count, f"{key} missing from one or more locale blocks" def test_session_meta_segments_softened_label_no_literal_segment_in_english(): """Regression: the sidebar badge for compressed/lineage rows must not visibly say 'X segments' by default — the technical internal term should be replaced with softer user-facing copy (#2155). This verifies the English base locale's session_meta_segments key so that t() fallback for untranslated locales also produces softened copy. """ import re i18n_text = (REPO_ROOT / 'static' / 'i18n.js').read_text(encoding='utf-8') # Locate the English base-locale block (first occurrence, before any _lang guard). first_lang = i18n_text.index('_lang: \'en\'') second_lang = i18n_text.index('_lang:', first_lang + 1) english_slice = i18n_text[first_lang:second_lang] assert 'session_meta_segments:' in english_slice, 'session_meta_segments missing from English locale' # Capture only the arrow-function value (not the key name which also contains 'segment'). match = re.search( r"session_meta_segments:\s*(\(\w+\)\s*=>\s*[^,]+)", english_slice, ) assert match, 'session_meta_segments value not found in English locale' rendered = match.group(1) assert 'segment' not in rendered, ( f"session_meta_segments English value still contains the technical word 'segment': {rendered}. " "Expected softened copy like 'prior turn(s)' instead. See #2155." ) def test_sidebar_search_and_rows_use_read_only_display_title(): """Stale persisted titles should not drive sidebar search/render when display_title exists.""" js = SESSIONS_JS_PATH.read_text(encoding="utf-8") assert "function _sessionDisplayTitle" in js assert "function _sessionTitleTags" in js assert "_allSessions.filter(s=>_sessionDisplayTitle(s).toLowerCase().includes(q))" in js assert "_allSessions.filter(s => _sessionDisplayTitle(s).toLowerCase().includes(q.toLowerCase()))" in js assert "const rawTitle=_sessionDisplayTitle(s);" in js assert "const tags=_sessionTitleTags(rawTitle);" in js assert "const segTitle=_sessionDisplayTitle(seg)||t('session_lineage_segment_untitled');" in js assert "const childTitle=_sessionDisplayTitle(child)||'Untitled child session';" in js def test_child_session_parent_segment_note_uses_display_title(): """A child attached through a hidden parent segment should show the reconciled segment title.""" js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_isChildSession')); eval(extractFunc('_sidebarLineageKeyForRow')); eval(extractFunc('_sessionDisplayTitle')); eval(extractFunc('_attachChildSessionsToSidebarRows')); const parentRow={{ session_id:'tip', title:'Hermes WebUI #8', _lineage_root_id:'root', _lineage_segments:[ {{session_id:'tip', title:'Hermes WebUI #8', display_title:'Hermes WebUI #177'}}, {{session_id:'old-parent', title:'Hermes WebUI #8', display_title:'Hermes WebUI #176'}}, ], }}; const child={{ session_id:'child', title:'Child Session', relationship_type:'child_session', parent_session_id:'old-parent', }}; const rows = _attachChildSessionsToSidebarRows([parentRow], [parentRow, child]); console.log(JSON.stringify(rows[0]._child_sessions[0])); """ child = json.loads(_run_node(source)) assert child["_parent_segment_id"] == "old-parent" assert child["_parent_segment_title"] == "Hermes WebUI #176" def test_default_webui_numbered_titles_are_not_treated_as_hash_tags(): """The reconciled title 'Hermes WebUI #177' must render with its number intact.""" js = SESSIONS_JS_PATH.read_text(encoding="utf-8") source = f""" const src = {js!r}; 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 < src.length) {{ if (src[i] === '{{') depth++; else if (src[i] === '}}') depth--; i++; }} return src.slice(start, i); }} eval(extractFunc('_sessionTitleIsDefaultWebUI')); eval(extractFunc('_sessionTitleTags')); console.log(JSON.stringify({{ webui:_sessionTitleTags('Hermes WebUI #177'), custom:_sessionTitleTags('Deploy #prod'), }})); """ assert json.loads(_run_node(source)) == {"webui": [], "custom": ["#prod"]}