From 782335b62afa90187407ccee3ab41825c9ba69c1 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Mon, 29 Jun 2026 10:39:08 -0400 Subject: [PATCH 1/5] feat(settings): rank settings search results by match source (#5149) --- static/panels.js | 181 +++++- tests/test_3850_settings_search.py | 29 +- ...t_5148_settings_search_busy_input_terms.py | 16 +- tests/test_5149_settings_search_ranking.py | 566 ++++++++++++++++++ 4 files changed, 763 insertions(+), 29 deletions(-) create mode 100644 tests/test_5149_settings_search_ranking.py diff --git a/static/panels.js b/static/panels.js index 240d6654..4ca801c5 100644 --- a/static/panels.js +++ b/static/panels.js @@ -6911,6 +6911,51 @@ function switchSettingsSection(name,opts){ if(opts&&opts.fromSidebarItem)_closeMobileSidebarAfterPanelSelection(); } +function _normalizeSettingsSearchText(value) { + return String(value || '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +} + +function _extractSettingsDescriptionText(field, labelEl) { + const chunks = []; + const settingsSearch = (field.dataset && field.dataset.settingsSearch) || ''; + if (settingsSearch) chunks.push(settingsSearch); + field.querySelectorAll('[data-i18n]').forEach(node => { + if (labelEl && (node === labelEl || labelEl.contains(node))) return; + const key = node.dataset ? node.dataset.i18n : null; + if (key) { + chunks.push(t(key)); + return; + } + chunks.push(node.textContent); + }); + return chunks.join(' '); +} + +function _extractSettingsValueText(field) { + const chunks = []; + const controls = [...field.querySelectorAll('select, input, textarea')]; + controls.forEach(control => { + const tagName = (control.tagName || '').toLowerCase(); + if (tagName === 'select') { + control.querySelectorAll('option').forEach(option => { + if (option.dataset && option.dataset.i18n) { + chunks.push(t(option.dataset.i18n)); + } else { + chunks.push(option.textContent); + } + }); + return; + } + const type = (control.getAttribute && control.getAttribute('type')) || control.type || ''; + if (tagName === 'input' && ['checkbox', 'radio', 'file', 'submit', 'reset', 'button'].includes(type)) return; + if (control.value) chunks.push(control.value); + }); + return chunks.join(' '); +} + async function _buildSettingsIndex() { if (_settingsIndex) return; // Memoize the in-flight build so concurrent searches share one pass; the @@ -6920,6 +6965,9 @@ async function _buildSettingsIndex() { // Ensure lazy-loaded panes are populated before reading the DOM await Promise.all([loadProvidersPanel(), loadPluginsPanel(), loadExtensionsPanel()]); const index = []; + const add = (entry) => { + index.push({ ...entry, _settingsSearchIndex: index.length }); + }; const sectionMap = { settingsPaneConversation: 'conversation', settingsPaneAppearance: 'appearance', @@ -6943,31 +6991,96 @@ async function _buildSettingsIndex() { const labelEl = field.querySelector('label[data-i18n], label [data-i18n], label'); if (!labelEl) return; const i18nKey = labelEl.dataset ? labelEl.dataset.i18n : undefined; - const label = (i18nKey && t(i18nKey)) || labelEl.textContent.trim(); - if (label) { - const searchBlob = [label, field.textContent, field.dataset ? field.dataset.settingsSearch : ''] - .filter(Boolean) - .join(' ') - .replace(/\s+/g, ' ') - .trim(); - index.push({ label, searchBlob, sectionKey, i18nKey, el: field }); - } + const titleText = (i18nKey && t(i18nKey)) || labelEl.textContent.trim(); + if (!titleText) return; + const valueText = _normalizeSettingsSearchText(_extractSettingsValueText(field)); + const descriptionText = _normalizeSettingsSearchText(_extractSettingsDescriptionText(field, labelEl)); + const searchBlob = [titleText, valueText, descriptionText, field.textContent] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + add({ + label: titleText, + titleText, + valueText, + descriptionText, + searchBlob, + sectionKey, + i18nKey, + el: field, + }); }); if (sectionKey === 'providers') { pane.querySelectorAll('.provider-card').forEach(card => { const cardName = ((card.querySelector('.provider-card-name') || {}).textContent || '').trim(); - if (cardName) index.push({ label: cardName, sectionKey, el: card, cardName }); + if (cardName) { + const titleText = cardName; + const valueText = _normalizeSettingsSearchText(_extractSettingsValueText(card)); + const descriptionText = _normalizeSettingsSearchText(_extractSettingsDescriptionText(card)); + const searchBlob = [cardName, valueText, descriptionText, card.textContent] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + add({ + label: cardName, + titleText, + valueText, + descriptionText, + searchBlob, + sectionKey, + el: card, + cardName, + }); + } card.querySelectorAll('.provider-card-field').forEach(field => { const fieldLabel = ((field.querySelector('.provider-card-label') || {}).textContent || '').trim(); const label = [cardName, fieldLabel].filter(Boolean).join(' '); - if (label) index.push({ label, sectionKey, el: field, cardName, fieldLabel }); + if (!label) return; + const valueText = _normalizeSettingsSearchText(_extractSettingsValueText(field)); + const descriptionText = _normalizeSettingsSearchText(_extractSettingsDescriptionText(field)); + const searchBlob = [label, valueText, descriptionText, field.textContent] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + add({ + label, + titleText: label, + valueText, + descriptionText, + searchBlob, + sectionKey, + el: field, + cardName, + fieldLabel, + }); }); }); } if (sectionKey === 'plugins') { pane.querySelectorAll('.plugin-card').forEach(card => { const cardName = ((card.querySelector('.provider-card-name') || {}).textContent || '').trim(); - if (cardName) index.push({ label: cardName, sectionKey, el: card, cardName }); + if (!cardName) return; + const titleText = cardName; + const valueText = _normalizeSettingsSearchText(_extractSettingsValueText(card)); + const descriptionText = _normalizeSettingsSearchText(_extractSettingsDescriptionText(card)); + const searchBlob = [cardName, valueText, descriptionText, card.textContent] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + add({ + label: cardName, + titleText, + valueText, + descriptionText, + searchBlob, + sectionKey, + el: card, + cardName, + }); }); } } @@ -6998,16 +7111,29 @@ async function filterSettings(query) { system: t('settings_tab_system') || 'System', help: t('settings_tab_help') || 'Help', }; - const matches = (_settingsIndex || []).filter(entry => - (entry.searchBlob || entry.label).toLowerCase().includes(q) - ); + const matches = (_settingsIndex || []).map((entry) => { + const score = _scoreSettingsSearchMatch(entry, q); + return score ? { entry, score, index: entry._settingsSearchIndex } : null; + }).filter(Boolean); if (!matches.length) { resultsEl.innerHTML = `
${esc(t('settings_search_no_results') || 'No settings found.')}
`; resultsEl.style.display = ''; return; } resultsEl.innerHTML = ''; - for (const m of matches.slice(0, 12)) { + matches.sort((left, right) => { + if (left.score.bucketIndex !== right.score.bucketIndex) { + return left.score.bucketIndex - right.score.bucketIndex; + } + if (left.score.matchType !== right.score.matchType) { + return left.score.matchType === 'prefix' ? -1 : 1; + } + if (left.score.matchIndex !== right.score.matchIndex) { + return left.score.matchIndex - right.score.matchIndex; + } + return left.index - right.index; + }); + for (const { entry: m } of matches.slice(0, 12)) { const item = document.createElement('button'); item.type = 'button'; item.className = 'settings-search-result'; @@ -7026,6 +7152,29 @@ async function filterSettings(query) { resultsEl.style.display = ''; } +function _scoreSettingsSearchMatch(entry, q) { + const query = (q || '').toLowerCase().trim(); + if (!query) return null; + const buckets = [ + ['titleText', 0], + ['valueText', 1], + ['descriptionText', 2], + ['searchBlob', 3], + ]; + for (const [bucketName, bucketIndex] of buckets) { + const hay = _normalizeSettingsSearchText(entry[bucketName]); + if (!hay) continue; + const matchIndex = hay.indexOf(query); + if (matchIndex < 0) continue; + return { + bucketIndex, + matchType: matchIndex === 0 ? 'prefix' : 'contains', + matchIndex, + }; + } + return null; +} + function _navigateToSettingsField(entry) { // The panes were populated when the index was built, so skip the tab-switch // lazy reload: loadProvidersPanel()/loadPluginsPanel() rebuild the pane DOM diff --git a/tests/test_3850_settings_search.py b/tests/test_3850_settings_search.py index e2415d13..856a5675 100644 --- a/tests/test_3850_settings_search.py +++ b/tests/test_3850_settings_search.py @@ -49,6 +49,15 @@ class TestSettingsSearch: assert "searchBlob" in body, ( "_buildSettingsIndex must store a searchBlob for settings fields" ) + assert "titleText" in body, ( + "_buildSettingsIndex must store title bucket text" + ) + assert "valueText" in body, ( + "_buildSettingsIndex must store value bucket text" + ) + assert "descriptionText" in body, ( + "_buildSettingsIndex must store description bucket text" + ) def test_panels_js_has_filter_settings_function(self): """panels.js must contain filterSettings function.""" @@ -56,12 +65,15 @@ class TestSettingsSearch: "panels.js must contain filterSettings(query) function" ) body = PANELS_JS[PANELS_JS.find("function filterSettings(query)"):] - assert "(entry.searchBlob || entry.label).toLowerCase().includes(q)" in body, ( - "filterSettings must do case-insensitive substring matching over the search blob" + assert "_scoreSettingsSearchMatch" in body, ( + "filterSettings must call _scoreSettingsSearchMatch for ranking" ) assert "esc(m.label)" in body, ( "filterSettings must keep rendering the visible label text" ) + assert ".sort(" in body, ( + "filterSettings must order matches deterministically" + ) def test_panels_js_has_navigate_to_field_function(self): """panels.js must contain _navigateToSettingsField function.""" @@ -169,7 +181,11 @@ class TestSettingsSearch: """Plugins pane entries must index plugin cards by plugin name.""" idx = PANELS_JS.find("function _buildSettingsIndex()") assert idx >= 0, "_buildSettingsIndex not found" - body = PANELS_JS[idx:idx + 3500] + end = PANELS_JS.find("function _resolveSettingsField(entry)", idx) + body = PANELS_JS[idx:end] + assert "if (sectionKey === 'plugins')" in body, ( + "_buildSettingsIndex should include a plugins section branch" + ) assert "pane.querySelectorAll('.plugin-card')" in body, ( "_buildSettingsIndex must scan plugin cards so Plugins search is not empty" ) @@ -214,8 +230,11 @@ class TestSettingsSearchReviewFixes: label[data-i18n]. Otherwise most checkbox settings are unsearchable.""" idx = PANELS_JS.find("function _buildSettingsIndex") assert idx >= 0, "_buildSettingsIndex not found" - body = PANELS_JS[idx:idx + 1600] - assert "label[data-i18n], label [data-i18n], label" in body, ( + body = PANELS_JS[idx:idx + 2600] + assert "field.querySelector(" in body, ( + "field index query should run a querySelector against field" + ) + assert "label[data-i18n]" in body, ( "field index query must include 'label [data-i18n]' (span-in-label) " "and plain 'label' so toggle settings are searchable" ) diff --git a/tests/test_5148_settings_search_busy_input_terms.py b/tests/test_5148_settings_search_busy_input_terms.py index 8122dbfd..36942530 100644 --- a/tests/test_5148_settings_search_busy_input_terms.py +++ b/tests/test_5148_settings_search_busy_input_terms.py @@ -24,24 +24,24 @@ class TestBusyInputSettingsSearchTerms: body = PANELS_JS[idx:] assert "searchBlob" in body, "_buildSettingsIndex must build a searchBlob" assert "field.textContent" in body, "_buildSettingsIndex must include field text in searchBlob" - assert "field.dataset ? field.dataset.settingsSearch : ''" in body, ( - "_buildSettingsIndex must include supplemental search terms in searchBlob" + assert "settingsSearch" in body, ( + "_buildSettingsIndex must include supplemental data-settings-search terms in searchBlob" ) def test_filter_settings_uses_search_blob_and_keeps_label_rendering(self): - """filterSettings must match searchBlob, render labels, and keep the cap.""" + """filterSettings must rank by source and keep visible labels.""" idx = PANELS_JS.find("function filterSettings(query)") assert idx >= 0, "filterSettings not found" body = PANELS_JS[idx:] - assert "(entry.searchBlob || entry.label).toLowerCase().includes(q)" in body, ( - "filterSettings must search the richer blob instead of label-only text" - ) assert "esc(m.label)" in body, ( "filterSettings must keep rendering the visible label" ) assert ".slice(0, 12)" in body, ( "filterSettings must keep the existing 12-result cap" ) - assert ".sort(" not in body, ( - "filterSettings must not introduce ranking or reordering" + assert ".sort(" in body, ( + "filterSettings must apply deterministic ranking" + ) + assert "_scoreSettingsSearchMatch" in body, ( + "filterSettings must score ranked matches by source" ) diff --git a/tests/test_5149_settings_search_ranking.py b/tests/test_5149_settings_search_ranking.py new file mode 100644 index 00000000..51c1582f --- /dev/null +++ b/tests/test_5149_settings_search_ranking.py @@ -0,0 +1,566 @@ +"""Browserless behavioral coverage for settings search ranking.""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +PANELS_JS = ROOT / "static" / "panels.js" +NODE = shutil.which("node") + +pytestmark = pytest.mark.skipif( + NODE is None, + reason="node is required to execute browserless settings-search ranking harness", +) + + +_DRIVER = r""" +const fs = require('fs'); + +const panelsPath = process.argv[2]; +const scenario = JSON.parse(process.argv[3] || '{}'); +const src = fs.readFileSync(panelsPath, 'utf8'); + +function extractSearchFunctions(source) { + const start = source.indexOf('function _normalizeSettingsSearchText(value)'); + if (start < 0) throw new Error('missing settings search helpers'); + const endMarker = 'function _navigateToSettingsField(entry)'; + const end = source.indexOf(endMarker, start); + if (end < 0) throw new Error('missing end marker before _navigateToSettingsField'); + return source.slice(start, end); +} + +function normalizeText(value) { + return String(value || '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +} + +function parseDataAttributeName(name) { + return String(name || '') + .split('-') + .map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + +function createClassList(owner) { + const tokens = new Set(); + const update = (value) => { + owner._className = Array.from(tokens).join(' '); + }; + return { + contains(name) { + return tokens.has(name); + }, + add(name) { + if (!name) return; + const token = String(name).trim(); + if (!token) return; + tokens.add(token); + update(); + }, + remove(name) { + tokens.delete(String(name)); + update(); + }, + toggle(name, force) { + const token = String(name); + const enabled = force !== undefined ? force : !tokens.has(token); + if (enabled) tokens.add(token); + else tokens.delete(token); + update(); + return enabled; + }, + _set(value) { + tokens.clear(); + for (const token of String(value || '').split(/\s+/)) { + if (token) tokens.add(token); + } + update(); + }, + }; +} + +class FakeElement { + constructor(tagName = 'div') { + this.tagName = String(tagName || '').toLowerCase(); + this.children = []; + this.parentElement = null; + this.parentNode = null; + this.attributes = {}; + this.dataset = {}; + this.style = {}; + this.classList = createClassList(this); + this.classList._set(''); + this._text = ''; + this._innerHTML = ''; + this._className = ''; + this.value = ''; + } + + set id(value) { + this._id = String(value || ''); + } + + get id() { + return this._id || ''; + } + + set className(value) { + this._className = String(value || ''); + this.classList._set(this._className); + } + + get className() { + return this._className; + } + + get textContent() { + const childText = this.children.map((child) => child.textContent).filter(Boolean).join(' '); + return [this._text, childText].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); + } + + set textContent(value) { + this._text = String(value || ''); + } + + set innerHTML(value) { + this._innerHTML = String(value || ''); + } + + get innerHTML() { + return this._innerHTML; + } + + setAttribute(name, value) { + const key = String(name || ''); + const v = String(value || ''); + this.attributes[key] = v; + if (key.startsWith('data-')) { + this.dataset[parseDataAttributeName(key.slice(5))] = v; + } + if (key === 'class') this.className = v; + if (key === 'id') this.id = v; + } + + getAttribute(name) { + if (name === 'class') return this.className; + if (name === 'id') return this.id; + return this.attributes[String(name)] || null; + } + + appendChild(child) { + if (!child) return child; + this.children.push(child); + child.parentElement = this; + child.parentNode = this; + return child; + } + + contains(node) { + let current = node; + while (current) { + if (current === this) return true; + current = current.parentElement; + } + return false; + } + + closest(selector) { + let current = this; + while (current) { + if (matchesSelector(current, selector)) return current; + current = current.parentElement; + } + return null; + } + + querySelectorAll(selector) { + return querySelectorAll(this, selector); + } + + querySelector(selector) { + const found = this.querySelectorAll(selector); + return found[0] || null; + } + + addEventListener() {} +} + +function allNodes(root) { + const nodes = [root]; + for (const child of root.children || []) { + nodes.push(...allNodes(child)); + } + return nodes; +} + +function isNodeMatch(node, selector) { + if (!node || !selector) return false; + const tagOnly = selector.match(/^([a-z0-9-]+)$/i); + if (selector === '*') return true; + if (selector === '[data-i18n]') { + return !!(node.dataset && node.dataset.i18n); + } + if (selector.startsWith('[') && selector.endsWith(']')) { + const body = selector.slice(1, -1); + const [key, expected] = body.split('=').map((it) => it.trim()); + const actual = key.startsWith('data-') + ? (node.dataset ? node.dataset[parseDataAttributeName(key.slice(5))] : null) + : (node.getAttribute ? node.getAttribute(key) : null); + if (expected === undefined) return !!actual; + const expectedValue = expected.replace(/^["']|["']$/g, ''); + return actual === expectedValue; + } + if (selector.startsWith('.')) { + return node.classList.contains(selector.slice(1)); + } + const withAttr = selector.match(/^([a-z0-9-]+)\[([^\]]+)\]$/i); + if (withAttr) { + const tag = withAttr[1].toLowerCase(); + const attr = withAttr[2]; + if (node.tagName !== tag) return false; + if (attr === 'data-i18n') return !!(node.dataset && node.dataset.i18n); + return !!node.getAttribute(attr); + } + return node.tagName === selector.toLowerCase(); +} + +function matchesSelector(node, selector) { + const value = selector.trim(); + if (!value.includes(' ')) return isNodeMatch(node, value); + const [ancestorSelector, targetSelector] = value.split(/\s+/); + if (!ancestorSelector || !targetSelector) return false; + const ancestors = allNodes(node); + for (const candidate of ancestors) { + if (!isNodeMatch(candidate, ancestorSelector)) continue; + for (const child of allNodes(candidate).slice(1)) { + if (isNodeMatch(child, targetSelector)) return true; + } + } + return false; +} + +function querySelectorAll(node, selector) { + const parts = selector.split(',').map((part) => part.trim()).filter(Boolean); + const found = []; + const seen = new Set(); + for (const part of parts) { + if (part.includes(' ')) { + const [ancestorSelector, targetSelector] = part.split(/\s+/); + for (const candidate of allNodes(node)) { + if (!isNodeMatch(candidate, ancestorSelector)) continue; + for (const child of allNodes(candidate).slice(1)) { + if (!isNodeMatch(child, targetSelector)) continue; + if (!seen.has(child)) { + seen.add(child); + found.push(child); + } + } + } + continue; + } + for (const candidate of allNodes(node)) { + if (!isNodeMatch(candidate, part)) continue; + if (!seen.has(candidate)) { + seen.add(candidate); + found.push(candidate); + } + } + } + return found; +} + +const registry = new Map(); + +function register(node) { + if (node && node.id) registry.set(node.id, node); + return node; +} + +function $(id) { + return registry.get(id) || null; +} + +function createElement(tagName) { + return new FakeElement(tagName); +} + +function makePane(id) { + return register(Object.assign(createElement('div'), { id })); +} + +function makeLabel(text) { + const label = createElement('label'); + label.textContent = text; + return label; +} + +function makeSettingsField({ + labelText, + descriptionText = '', + descriptionI18nKey = '', + settingsSearch = '', + options = [], +}) { + const field = createElement('div'); + field.className = 'settings-field'; + if (settingsSearch) field.dataset.settingsSearch = settingsSearch; + const label = makeLabel(labelText); + field.appendChild(label); + if (options.length) { + const select = createElement('select'); + for (const optionText of options) { + const option = createElement('option'); + option.textContent = optionText; + select.appendChild(option); + } + field.appendChild(select); + } + if (descriptionText || descriptionI18nKey) { + const description = createElement('div'); + if (descriptionI18nKey) description.dataset.i18n = descriptionI18nKey; + description.textContent = descriptionText || ''; + field.appendChild(description); + } + return field; +} + +function makeProviderCard(name) { + const card = createElement('div'); + card.className = 'provider-card'; + const cardName = createElement('div'); + cardName.className = 'provider-card-name'; + cardName.textContent = name; + card.appendChild(cardName); + return card; +} + +function makeProviderField(cardName, fieldLabel, valueText) { + const field = createElement('div'); + field.className = 'provider-card-field'; + const label = createElement('div'); + label.className = 'provider-card-label'; + label.textContent = fieldLabel; + field.appendChild(label); + if (valueText) { + const input = createElement('input'); + input.value = valueText; + field.appendChild(input); + } + return field; +} + +function makePluginCard(name) { + const card = createElement('div'); + card.className = 'provider-card plugin-card'; + const cardName = createElement('div'); + cardName.className = 'provider-card-name'; + cardName.textContent = name; + card.appendChild(cardName); + return card; +} + +function setupDom(mode) { + const conversation = makePane('settingsPaneConversation'); + const providers = makePane('settingsPaneProviders'); + const plugins = makePane('settingsPanePlugins'); + register(createElement('div')).id = 'settingsPaneAppearance'; + register(createElement('div')).id = 'settingsPanePreferences'; + register(createElement('div')).id = 'settingsPaneExtensions'; + register(createElement('div')).id = 'settingsPaneSystem'; + register(createElement('div')).id = 'settingsPaneHelp'; + const settingsSearch = createElement('input'); + settingsSearch.id = 'settingsSearch'; + register(settingsSearch); + const results = createElement('div'); + results.id = 'settingsSearchResults'; + register(results); + + if (mode === 'title-vs-description') { + conversation.appendChild(makeSettingsField({ + labelText: 'Descriptor-only field', + descriptionText: 'this contains priority in the descriptor', + })); + conversation.appendChild(makeSettingsField({ + labelText: 'Priority title field', + descriptionText: 'nothing about query there', + })); + } else if (mode === 'value-vs-description') { + conversation.appendChild(makeSettingsField({ + labelText: 'Value-later field', + descriptionText: 'contains rank token in descriptor only', + })); + conversation.appendChild(makeSettingsField({ + labelText: 'Value field', + options: ['rank-value option'], + })); + } else if (mode === 'same-tier-order') { + conversation.appendChild(makeSettingsField({ + labelText: 'Description-first', + descriptionText: 'first field has needle around index 5', + })); + conversation.appendChild(makeSettingsField({ + labelText: 'Description-second', + descriptionText: 'second field has the needle a little later in the text', + })); + } else if (mode === 'label-rendering') { + conversation.appendChild(makeSettingsField({ + labelText: 'Visible Label', + options: ['token-label-option'], + descriptionText: 'description with token-label-option', + })); + } else if (mode === 'twelve-result-cap') { + for (let i = 0; i < 13; i++) { + conversation.appendChild(makeSettingsField({ + labelText: `Entry ${String(i + 1).padStart(2, '0')}`, + descriptionText: `shared token for cap ${i}`, + })); + } + } else if (mode === 'provider-plugin') { + conversation.appendChild(makeSettingsField({ + labelText: 'Conversation field', + descriptionText: 'provider plugin terms plugin', + })); + const providerCard = makeProviderCard('Provider Alpha'); + providerCard.appendChild(makeProviderField('provider', 'API Key', 'sk-test')); + providers.appendChild(providerCard); + plugins.appendChild(makePluginCard('Plugin Sample')); + } + + return { + conversation, + providers, + plugins, + }; +} + +function runScenario(command) { + setupDom(command); + return new Promise(async (resolve, reject) => { + try { + const block = extractSearchFunctions(src); + eval(block); + globalThis.$ = $; + globalThis.document = { + createElement: createElement, + }; + globalThis.t = (key) => { + const translations = { + settings_tab_conversation: 'Conversation', + settings_tab_appearance: 'Appearance', + settings_tab_preferences: 'Preferences', + providers_tab_title: 'Providers', + settings_tab_plugins: 'Plugins', + settings_tab_extensions: 'Extensions', + settings_tab_system: 'System', + settings_tab_help: 'Help', + }; + return translations[key] || String(key || ''); + }; + globalThis.esc = (value) => String(value || ''); + globalThis.loadProvidersPanel = async () => undefined; + globalThis.loadPluginsPanel = async () => undefined; + globalThis.loadExtensionsPanel = async () => undefined; + globalThis._settingsIndex = null; + globalThis._settingsIndexPromise = null; + globalThis._settingsSearchSeq = 0; + + let query = ''; + if (command === 'title-vs-description') query = 'priority'; + else if (command === 'value-vs-description') query = 'rank'; + else if (command === 'same-tier-order') query = 'needle'; + else if (command === 'label-rendering') query = 'token-label-option'; + else if (command === 'twelve-result-cap') query = 'shared'; + else if (command === 'provider-plugin') query = 'plugin'; + await filterSettings(query); + + const results = $('settingsSearchResults'); + const labels = []; + const html = results.innerHTML || ''; + for (const child of results.children || []) { + if (!child || typeof child.innerHTML !== 'string') continue; + const re = /([^<]*)<\/span>/g; + let match; + while ((match = re.exec(child.innerHTML)) !== null) labels.push(match[1]); + } + resolve({ + query, + labels, + html, + resultCount: (results.children || []).length, + noResults: html.includes('settings-search-empty'), + }); + } catch (err) { + reject(err); + } + }); +} + +(async () => { + const payload = await runScenario(scenario.command); + process.stdout.write(JSON.stringify(payload)); +})(); +""" + + +@pytest.fixture(scope="module") +def driver_path(tmp_path_factory): + return str(tmp_path_factory.mktemp("settings-search-ranking-driver") / "driver.js") + + +@pytest.fixture +def driver_file(driver_path): + Path(driver_path).write_text(_DRIVER, encoding="utf-8") + return driver_path + + +def _run_driver(driver_file, command): + process = subprocess.run( + [NODE, driver_file, str(PANELS_JS), json.dumps({"command": command})], + capture_output=True, + text=True, + check=False, + timeout=60, + ) + if process.returncode != 0: + raise RuntimeError(process.stderr.strip() or process.stdout.strip()) + return json.loads(process.stdout) + + +def test_title_matches_outrank_descriptor_only(driver_file): + payload = _run_driver(driver_file, "title-vs-description") + assert payload["labels"][0] == "Priority title field" + assert not payload["noResults"] + + +def test_value_or_option_match_outranks_descriptor_match(driver_file): + payload = _run_driver(driver_file, "value-vs-description") + assert payload["labels"][0] == "Value field" + assert not payload["noResults"] + + +def test_same_tier_order_uses_prefix_and_earlier_index(driver_file): + payload = _run_driver(driver_file, "same-tier-order") + assert payload["labels"][0] == "Description-first" + assert payload["labels"][1] == "Description-second" + + +def test_rendered_label_is_visible_field_title(driver_file): + payload = _run_driver(driver_file, "label-rendering") + assert payload["labels"][0] == "Visible Label" + + +def test_results_cap_still_applies_to_ranked_matches(driver_file): + payload = _run_driver(driver_file, "twelve-result-cap") + assert payload["resultCount"] == 12 + assert not payload["noResults"] + + +def test_provider_and_plugin_cards_remain_searchable(driver_file): + payload = _run_driver(driver_file, "provider-plugin") + assert "Plugin Sample" in payload["labels"] From c802c18242ab85780b99a64d71dfd551bcaf64c2 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Mon, 29 Jun 2026 10:50:05 -0400 Subject: [PATCH 2/5] test(settings): harden ranking proof coverage (#5149) --- tests/test_5149_settings_search_ranking.py | 29 +++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/test_5149_settings_search_ranking.py b/tests/test_5149_settings_search_ranking.py index 51c1582f..d9973a77 100644 --- a/tests/test_5149_settings_search_ranking.py +++ b/tests/test_5149_settings_search_ranking.py @@ -384,16 +384,16 @@ function setupDom(mode) { if (mode === 'title-vs-description') { conversation.appendChild(makeSettingsField({ labelText: 'Descriptor-only field', - descriptionText: 'this contains priority in the descriptor', + descriptionI18nKey: 'settings_desc_priority_only', })); conversation.appendChild(makeSettingsField({ labelText: 'Priority title field', - descriptionText: 'nothing about query there', + descriptionI18nKey: 'settings_desc_irrelevant', })); } else if (mode === 'value-vs-description') { conversation.appendChild(makeSettingsField({ labelText: 'Value-later field', - descriptionText: 'contains rank token in descriptor only', + descriptionI18nKey: 'settings_desc_rank_only', })); conversation.appendChild(makeSettingsField({ labelText: 'Value field', @@ -401,12 +401,16 @@ function setupDom(mode) { })); } else if (mode === 'same-tier-order') { conversation.appendChild(makeSettingsField({ - labelText: 'Description-first', - descriptionText: 'first field has needle around index 5', + labelText: 'Description-prefix', + descriptionI18nKey: 'settings_desc_needle_prefix', })); conversation.appendChild(makeSettingsField({ - labelText: 'Description-second', - descriptionText: 'second field has the needle a little later in the text', + labelText: 'Description-earlier-contains', + descriptionI18nKey: 'settings_desc_needle_contains_early', + })); + conversation.appendChild(makeSettingsField({ + labelText: 'Description-later-contains', + descriptionI18nKey: 'settings_desc_needle_contains_late', })); } else if (mode === 'label-rendering') { conversation.appendChild(makeSettingsField({ @@ -459,6 +463,12 @@ function runScenario(command) { settings_tab_extensions: 'Extensions', settings_tab_system: 'System', settings_tab_help: 'Help', + settings_desc_priority_only: 'this contains priority in the descriptor', + settings_desc_irrelevant: 'nothing about query there', + settings_desc_rank_only: 'contains rank token in descriptor only', + settings_desc_needle_prefix: 'needle appears at the start of this description', + settings_desc_needle_contains_early: 'this field has needle near the front', + settings_desc_needle_contains_late: 'this field waits a while before it reaches needle', }; return translations[key] || String(key || ''); }; @@ -546,8 +556,9 @@ def test_value_or_option_match_outranks_descriptor_match(driver_file): def test_same_tier_order_uses_prefix_and_earlier_index(driver_file): payload = _run_driver(driver_file, "same-tier-order") - assert payload["labels"][0] == "Description-first" - assert payload["labels"][1] == "Description-second" + assert payload["labels"][0] == "Description-prefix" + assert payload["labels"][1] == "Description-earlier-contains" + assert payload["labels"][2] == "Description-later-contains" def test_rendered_label_is_visible_field_title(driver_file): From 533eaf7c2616c629569a91a4718d7c3cd16d3740 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Mon, 29 Jun 2026 11:04:59 -0400 Subject: [PATCH 3/5] test(settings): close ranking proof gaps (#5149) --- static/panels.js | 3 -- tests/test_5149_settings_search_ranking.py | 50 ++++++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/static/panels.js b/static/panels.js index 4ca801c5..1e795c45 100644 --- a/static/panels.js +++ b/static/panels.js @@ -7125,9 +7125,6 @@ async function filterSettings(query) { if (left.score.bucketIndex !== right.score.bucketIndex) { return left.score.bucketIndex - right.score.bucketIndex; } - if (left.score.matchType !== right.score.matchType) { - return left.score.matchType === 'prefix' ? -1 : 1; - } if (left.score.matchIndex !== right.score.matchIndex) { return left.score.matchIndex - right.score.matchIndex; } diff --git a/tests/test_5149_settings_search_ranking.py b/tests/test_5149_settings_search_ranking.py index d9973a77..0021a862 100644 --- a/tests/test_5149_settings_search_ranking.py +++ b/tests/test_5149_settings_search_ranking.py @@ -386,6 +386,10 @@ function setupDom(mode) { labelText: 'Descriptor-only field', descriptionI18nKey: 'settings_desc_priority_only', })); + conversation.appendChild(makeSettingsField({ + labelText: 'Blob field', + descriptionText: 'priority appears only in this plain-text fallback', + })); conversation.appendChild(makeSettingsField({ labelText: 'Priority title field', descriptionI18nKey: 'settings_desc_irrelevant', @@ -395,6 +399,10 @@ function setupDom(mode) { labelText: 'Value-later field', descriptionI18nKey: 'settings_desc_rank_only', })); + conversation.appendChild(makeSettingsField({ + labelText: 'Blob value field', + descriptionText: 'rank only lives in this plain-text fallback text', + })); conversation.appendChild(makeSettingsField({ labelText: 'Value field', options: ['rank-value option'], @@ -418,6 +426,12 @@ function setupDom(mode) { options: ['token-label-option'], descriptionText: 'description with token-label-option', })); + } else if (mode === 'supplemental-term') { + conversation.appendChild(makeSettingsField({ + labelText: 'Supplemental alias field', + descriptionText: 'description without the alias query', + settingsSearch: 'steer alias query', + })); } else if (mode === 'twelve-result-cap') { for (let i = 0; i < 13; i++) { conversation.appendChild(makeSettingsField({ @@ -485,8 +499,9 @@ function runScenario(command) { else if (command === 'value-vs-description') query = 'rank'; else if (command === 'same-tier-order') query = 'needle'; else if (command === 'label-rendering') query = 'token-label-option'; + else if (command === 'supplemental-term') query = 'steer alias'; else if (command === 'twelve-result-cap') query = 'shared'; - else if (command === 'provider-plugin') query = 'plugin'; + else if (command === 'provider-plugin') query = scenario.query || 'plugin'; await filterSettings(query); const results = $('settingsSearchResults'); @@ -531,7 +546,12 @@ def driver_file(driver_path): def _run_driver(driver_file, command): process = subprocess.run( - [NODE, driver_file, str(PANELS_JS), json.dumps({"command": command})], + [ + NODE, + driver_file, + str(PANELS_JS), + json.dumps(command if isinstance(command, dict) else {"command": command}), + ], capture_output=True, text=True, check=False, @@ -545,12 +565,16 @@ def _run_driver(driver_file, command): def test_title_matches_outrank_descriptor_only(driver_file): payload = _run_driver(driver_file, "title-vs-description") assert payload["labels"][0] == "Priority title field" + assert payload["labels"][1] == "Descriptor-only field" + assert payload["labels"][2] == "Blob field" assert not payload["noResults"] def test_value_or_option_match_outranks_descriptor_match(driver_file): payload = _run_driver(driver_file, "value-vs-description") assert payload["labels"][0] == "Value field" + assert payload["labels"][1] == "Value-later field" + assert payload["labels"][2] == "Blob value field" assert not payload["noResults"] @@ -566,6 +590,11 @@ def test_rendered_label_is_visible_field_title(driver_file): assert payload["labels"][0] == "Visible Label" +def test_supplemental_search_terms_remain_behaviorally_searchable(driver_file): + payload = _run_driver(driver_file, "supplemental-term") + assert payload["labels"] == ["Supplemental alias field"] + + def test_results_cap_still_applies_to_ranked_matches(driver_file): payload = _run_driver(driver_file, "twelve-result-cap") assert payload["resultCount"] == 12 @@ -573,5 +602,18 @@ def test_results_cap_still_applies_to_ranked_matches(driver_file): def test_provider_and_plugin_cards_remain_searchable(driver_file): - payload = _run_driver(driver_file, "provider-plugin") - assert "Plugin Sample" in payload["labels"] + provider_payload = _run_driver( + driver_file, + {"command": "provider-plugin", "query": "alpha"}, + ) + field_payload = _run_driver( + driver_file, + {"command": "provider-plugin", "query": "key"}, + ) + plugin_payload = _run_driver( + driver_file, + {"command": "provider-plugin", "query": "plugin"}, + ) + assert "Provider Alpha" in provider_payload["labels"] + assert "Provider Alpha API Key" in field_payload["labels"] + assert "Plugin Sample" in plugin_payload["labels"] From b5dc306723e24635584aa45a7f89862a9c842c54 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Mon, 29 Jun 2026 11:18:07 -0400 Subject: [PATCH 4/5] test(settings): decouple ranking proofs from insertion order (#5149) --- tests/test_5149_settings_search_ranking.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_5149_settings_search_ranking.py b/tests/test_5149_settings_search_ranking.py index 0021a862..6bb05d43 100644 --- a/tests/test_5149_settings_search_ranking.py +++ b/tests/test_5149_settings_search_ranking.py @@ -382,32 +382,36 @@ function setupDom(mode) { register(results); if (mode === 'title-vs-description') { - conversation.appendChild(makeSettingsField({ - labelText: 'Descriptor-only field', - descriptionI18nKey: 'settings_desc_priority_only', - })); conversation.appendChild(makeSettingsField({ labelText: 'Blob field', descriptionText: 'priority appears only in this plain-text fallback', })); + conversation.appendChild(makeSettingsField({ + labelText: 'Descriptor-only field', + descriptionI18nKey: 'settings_desc_priority_only', + })); conversation.appendChild(makeSettingsField({ labelText: 'Priority title field', descriptionI18nKey: 'settings_desc_irrelevant', })); } else if (mode === 'value-vs-description') { - conversation.appendChild(makeSettingsField({ - labelText: 'Value-later field', - descriptionI18nKey: 'settings_desc_rank_only', - })); conversation.appendChild(makeSettingsField({ labelText: 'Blob value field', descriptionText: 'rank only lives in this plain-text fallback text', })); + conversation.appendChild(makeSettingsField({ + labelText: 'Value-later field', + descriptionI18nKey: 'settings_desc_rank_only', + })); conversation.appendChild(makeSettingsField({ labelText: 'Value field', options: ['rank-value option'], })); } else if (mode === 'same-tier-order') { + conversation.appendChild(makeSettingsField({ + labelText: 'Description-later-contains', + descriptionI18nKey: 'settings_desc_needle_contains_late', + })); conversation.appendChild(makeSettingsField({ labelText: 'Description-prefix', descriptionI18nKey: 'settings_desc_needle_prefix', @@ -416,10 +420,6 @@ function setupDom(mode) { labelText: 'Description-earlier-contains', descriptionI18nKey: 'settings_desc_needle_contains_early', })); - conversation.appendChild(makeSettingsField({ - labelText: 'Description-later-contains', - descriptionI18nKey: 'settings_desc_needle_contains_late', - })); } else if (mode === 'label-rendering') { conversation.appendChild(makeSettingsField({ labelText: 'Visible Label', From d35e3a9d103a35854600c490b0094afa0c198bd5 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Mon, 29 Jun 2026 11:38:50 -0400 Subject: [PATCH 5/5] refactor(settings): trim unreachable descriptor fallback (#5149) --- static/panels.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/static/panels.js b/static/panels.js index 1e795c45..40deae65 100644 --- a/static/panels.js +++ b/static/panels.js @@ -6925,11 +6925,7 @@ function _extractSettingsDescriptionText(field, labelEl) { field.querySelectorAll('[data-i18n]').forEach(node => { if (labelEl && (node === labelEl || labelEl.contains(node))) return; const key = node.dataset ? node.dataset.i18n : null; - if (key) { - chunks.push(t(key)); - return; - } - chunks.push(node.textContent); + if (key) chunks.push(t(key)); }); return chunks.join(' '); }