From 6b15d7efa748cfe37977284f5d7083f8c0b121c2 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 4 Aug 2026 04:39:46 +0000 Subject: [PATCH] fix(search): stop pinning the omnibox to a paid engine, and make it choosable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The address bar was hardcoded to Kagi, which is subscription-only past its trial — so a fresh install landed on a login wall instead of results, and the Settings dropdown didn't help because it only ever drove the new-tab box. - The omnibox engine now comes from `$DATA/search-engine`, written by `tron search `. Default is DuckDuckGo: it answers without an account. - Drop the Kagi `suggestions_url`. It fired a request on every keystroke in the address bar, which leaks the query before you press enter and stalls typing when the endpoint is slow or answers 401. - Only ever overwrite a search engine that is absent or that we wrote ourselves. Profiles we had already pinned to Kagi get repaired; a choice the user made in chrome://settings/search is left alone. An explicit `tron search` outranks both. - Write Preferences via a temp file and rename. It holds the whole profile, and a truncated in-place write loses every setting in it. - Say plainly in both Settings copies which box each picker governs. They are separate because an MV3 extension cannot set the browser's default engine — there is no API, and chrome_settings_overrides isn't available on Linux. The new-tab box defaults to DuckDuckGo too; leaving it on Kagi would reproduce the same paywall in the other search box. Tests: 9 launcher cases covering the default, the no-suggest rule, the repair, the don't-stomp guard, explicit override and apply-once; 7 for `tron search`, extracted from install.sh's heredoc since `sh -n install.sh` never parses it. Co-Authored-By: Claude Opus 5 --- apps/desktop/extensions/ai-sidebar/newtab.js | 10 +- .../extensions/ai-sidebar/options.html | 11 +- .../ai-sidebar/settings-sections.js | 2 +- apps/desktop/launcher/tronbrowser | 99 ++++++++++++--- apps/desktop/test/launcher.test.ts | 110 ++++++++++++++++ apps/web/public/install.sh | 34 +++++ apps/web/public/settings-sections.js | 2 +- apps/web/public/settings.html | 9 +- apps/web/test/install-search.test.ts | 118 ++++++++++++++++++ 9 files changed, 366 insertions(+), 29 deletions(-) create mode 100644 apps/web/test/install-search.test.ts diff --git a/apps/desktop/extensions/ai-sidebar/newtab.js b/apps/desktop/extensions/ai-sidebar/newtab.js index c031646..7a15fef 100644 --- a/apps/desktop/extensions/ai-sidebar/newtab.js +++ b/apps/desktop/extensions/ai-sidebar/newtab.js @@ -11,7 +11,11 @@ const DEFAULT_TICKERS = 'SPY, AAPL, NVDA, BTC-USD'; const DEFAULT_LEAGUES = 'nfl, nba'; const splitList = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean); -// --- Search: Web (Kagi by default) or AI (sidebar) --- +// --- Search: Web (DuckDuckGo by default) or AI (sidebar) --- +// This is the NEW-TAB box only. The address bar's engine is a browser-level +// setting the extension cannot reach; `tron search ` sets that one. +// Default is DuckDuckGo because it answers without an account — Kagi is +// subscription-only past its trial, so it cannot be the out-of-box default. const SEARCH_ENGINES = { kagi: { name: 'Kagi', url: 'https://kagi.com/search?q=' }, neosearch: { name: 'NeoSearch', url: 'https://neosearch.org/?q=' }, @@ -37,7 +41,7 @@ const TOR_SEARCH_ENGINES = { excavator: { name: 'Excavator', url: 'http://2fd6cemt4gmccflhm6imvdfvli3nf7zn6rfrwpsy7uhxrgbypvwf5fad.onion/?q=' }, }; let searchMode = 'web'; -let searchEngine = 'kagi'; +let searchEngine = 'ddg'; let torSearchEngine = 'ahmia'; let torEnabled = false; @@ -45,7 +49,7 @@ let torEnabled = false; // is on, otherwise the clearnet default. function activeEngine() { if (torEnabled) return TOR_SEARCH_ENGINES[torSearchEngine] || TOR_SEARCH_ENGINES.ahmia; - return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.kagi; + return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.ddg; } function setMode(mode) { diff --git a/apps/desktop/extensions/ai-sidebar/options.html b/apps/desktop/extensions/ai-sidebar/options.html index 056fa44..1c91398 100644 --- a/apps/desktop/extensions/ai-sidebar/options.html +++ b/apps/desktop/extensions/ai-sidebar/options.html @@ -130,15 +130,16 @@

bittorrented.com — Live TV, Radio & Podcasts

Search

-

Default search engine for the new-tab search box. Kagi is the - default; NeoSearch, Xprivo, DuckDuckGo, and others are available as alternatives. (The omnibox default - is set separately in chrome://settings/search.)

+

This sets the new-tab search box only. + The address bar is a browser-level setting this page cannot reach — change that + one with tron search <engine>, or in + chrome://settings/search.

diff --git a/apps/desktop/extensions/ai-sidebar/settings-sections.js b/apps/desktop/extensions/ai-sidebar/settings-sections.js index 90d1f9b..9c2fbdf 100644 --- a/apps/desktop/extensions/ai-sidebar/settings-sections.js +++ b/apps/desktop/extensions/ai-sidebar/settings-sections.js @@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) { const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']); // Populate current values (on every mount, e.g. after a cloud pull). - if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi'; + if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg'; if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia'; if (el('tickers')) el('tickers').value = cur.tickers ?? ''; if (el('leagues')) el('leagues').value = cur.leagues ?? ''; diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index 7616ff7..5245259 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -186,29 +186,96 @@ if flag not in exp: PY fi -# Default the omnibox to Kagi once per profile, then respect the user's -# chrome://settings/search choice. The sentinel filename carries the engine so -# that changing the default here re-applies once on profiles created earlier. -if command -v python3 >/dev/null 2>&1 && [ ! -f "$DATA/.tron-search-kagi" ]; then - TB_PREFS="$DATA/Default/Preferences" python3 - <<'PY' 2>/dev/null || true -import json, os +# --- Omnibox search engine ------------------------------------------------- +# The address bar's engine is a browser-level setting. An MV3 extension cannot +# change it at runtime and manifest `chrome_settings_overrides` isn't available +# on Linux, so the launcher is the only thing here that can set it — which is +# why `tron search ` writes a file we read rather than the extension +# doing it. The new-tab box keeps its own picker in Settings; the two are +# separate on purpose and both say so in their UI. +# +# No suggestions_url anywhere below. A suggest endpoint fires a request on every +# keystroke in the address bar, which both leaks the query before you press +# enter and stalls typing when the endpoint is slow or answers 401. +search_engine_spec() { + case "$1" in + ddg) echo "DuckDuckGo|duckduckgo.com|https://duckduckgo.com/?q={searchTerms}" ;; + kagi) echo "Kagi|kagi.com|https://kagi.com/search?q={searchTerms}" ;; + neosearch) echo "NeoSearch|neosearch.org|https://neosearch.org/?q={searchTerms}" ;; + xprivo) echo "Xprivo|xprivo.com|https://www.xprivo.com/search/?q={searchTerms}" ;; + oxiverse) echo "Oxiverse|search.oxiverse.com|https://search.oxiverse.com/?q={searchTerms}&tab=web" ;; + # Altpower is deliberately absent: its query lives in the URL fragment, which + # never reaches a server as a search. It works from the new-tab box, which + # builds the URL itself, but not as an omnibox engine. + *) return 1 ;; + esac +} + +# What the user asked for (`tron search `), or a default that works +# without an account. Kagi is subscription-only past its trial, so defaulting +# the omnibox to it leaves a fresh install unable to search at all. +SEARCH_WANT="$(cat "$DATA/search-engine" 2>/dev/null || true)" +SEARCH_WANT="$(printf '%s' "$SEARCH_WANT" | tr -d '[:space:]')" +SEARCH_EXPLICIT=1 +if [ -z "$SEARCH_WANT" ]; then SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0; fi +if ! search_engine_spec "$SEARCH_WANT" >/dev/null 2>&1; then + echo "TronBrowser: unknown search engine '$SEARCH_WANT' — using ddg. See 'tron search'." >&2 + SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0 +fi + +# Which engine we last set. Profiles from before this file existed recorded it +# in the sentinel's NAME; treat that as "we set Kagi" so the repair below can +# recognise its own handiwork. +SEARCH_MARK="$DATA/.tron-search" +SEARCH_PREV="$(cat "$SEARCH_MARK" 2>/dev/null || true)" +SEARCH_PREV="$(printf '%s' "$SEARCH_PREV" | tr -d '[:space:]')" +if [ -z "$SEARCH_PREV" ] && [ -f "$DATA/.tron-search-kagi" ]; then SEARCH_PREV="kagi"; fi + +if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; then + SEARCH_PREV_URL="" + if [ -n "$SEARCH_PREV" ]; then + SEARCH_PREV_URL="$(search_engine_spec "$SEARCH_PREV" 2>/dev/null | cut -d'|' -f3 || true)" + fi + TB_PREFS="$DATA/Default/Preferences" \ + TB_ENGINE="$(search_engine_spec "$SEARCH_WANT")" \ + TB_PREV_URL="$SEARCH_PREV_URL" \ + TB_FORCE="$SEARCH_EXPLICIT" \ + python3 - <<'PY' 2>/dev/null || true +import json, os, tempfile p = os.environ["TB_PREFS"] +name, keyword, url = os.environ["TB_ENGINE"].split("|") +prev_url = os.environ.get("TB_PREV_URL", "") +force = os.environ.get("TB_FORCE") == "1" os.makedirs(os.path.dirname(p), exist_ok=True) try: d = json.load(open(p)) if os.path.exists(p) else {} except Exception: d = {} -d.setdefault("default_search_provider_data", {})["template_url_data"] = { - "short_name": "Kagi", - "keyword": "kagi.com", - "url": "https://kagi.com/search?q={searchTerms}", - "suggestions_url": "https://kagi.com/api/autosuggest?q={searchTerms}", - "favicon_url": "https://kagi.com/favicon.ico", - "safe_for_autoreplace": False, -} -json.dump(d, open(p, "w")) + +# Only ever overwrite an engine that is absent or that we put there ourselves. +# Without this, changing the default below would stomp the choice of everyone +# who had already picked their own in chrome://settings/search. An explicit +# `tron search` is the one case that outranks whatever is currently set. +cur = d.get("default_search_provider_data", {}).get("template_url_data") or {} +cur_url = cur.get("url", "") +if force or not cur_url or (prev_url and cur_url == prev_url): + d.setdefault("default_search_provider_data", {})["template_url_data"] = { + "short_name": name, + "keyword": keyword, + "url": url, + "favicon_url": "https://" + keyword.split("/")[0] + "/favicon.ico", + "safe_for_autoreplace": False, + } + # Preferences is the profile. A truncated write loses every setting in it, + # so land it as a rename rather than in place. + fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p)) + with os.fdopen(fd, "w") as f: + json.dump(d, f) + os.replace(tmp, p) PY - mkdir -p "$DATA"; : > "$DATA/.tron-search-kagi" + mkdir -p "$DATA" + printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true + rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true fi # Show the feed (ai-sidebar New Tab page) on startup. We can't pass a startup URL — diff --git a/apps/desktop/test/launcher.test.ts b/apps/desktop/test/launcher.test.ts index 4572822..9d01547 100644 --- a/apps/desktop/test/launcher.test.ts +++ b/apps/desktop/test/launcher.test.ts @@ -116,6 +116,116 @@ describe('launcher flags', () => { }); }); +// --- Omnibox search engine --------------------------------------------------- +// The launcher is the only thing that can set Chromium's default search engine +// (no MV3 API for it, and chrome_settings_overrides isn't available on Linux), +// so the rules about when it may overwrite one live here. + +const KAGI_URL = 'https://kagi.com/search?q={searchTerms}'; + +const prefsPath = (home: string) => join(home, 'profile', 'Default', 'Preferences'); + +/** The default_search_provider_data the launcher left in the profile. */ +function dse(home: string): Record { + const p = prefsPath(home); + if (!existsSync(p)) return {}; + const json = JSON.parse(readFileSync(p, 'utf8')); + return json.default_search_provider_data?.template_url_data ?? {}; +} + +/** A profile that already has a search engine set, and a record of who set it. */ +function seed(opts: { url?: string; marker?: string; legacyKagi?: boolean; want?: string }): string { + const home = mkdtempSync(join(tmpdir(), 'tron-launcher-')); + homes.push(home); + const data = join(home, 'profile'); + mkdirSync(join(data, 'Default'), { recursive: true }); + if (opts.url) { + writeFileSync( + prefsPath(home), + JSON.stringify({ + default_search_provider_data: { template_url_data: { short_name: 'Seeded', url: opts.url } }, + // A real profile has more than the one key; it must survive our write. + bookmark_bar: { show_on_all_tabs: true }, + }), + ); + } + if (opts.legacyKagi) writeFileSync(join(data, '.tron-search-kagi'), ''); + if (opts.marker) writeFileSync(join(data, '.tron-search'), `${opts.marker}\n`); + if (opts.want) writeFileSync(join(data, 'search-engine'), `${opts.want}\n`); + return home; +} + +describe('omnibox search engine', () => { + it('defaults a fresh profile to an engine that works without an account', () => { + // Kagi is subscription-only past its trial, so defaulting to it left a new + // install unable to search at all — it just landed on a login wall. + const { home } = run([]); + expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}'); + }); + + it('never sets a suggestions_url', () => { + // A suggest endpoint fires per keystroke in the address bar: it leaks the + // query before you hit enter, and stalls typing when it is slow or 401s. + const { home } = run([]); + expect(dse(home)).not.toHaveProperty('suggestions_url'); + }); + + it('honors the engine chosen with `tron search`', () => { + const home = seed({ want: 'neosearch' }); + run([], { home }); + expect(dse(home).url).toBe('https://neosearch.org/?q={searchTerms}'); + }); + + it('falls back and says so when the chosen engine is unknown', () => { + const home = seed({ want: 'notanengine' }); + const { stderr } = run([], { home }); + expect(stderr).toContain("unknown search engine 'notanengine'"); + expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}'); + }); + + it('repairs a profile it had previously pinned to Kagi', () => { + // The actual bug: everyone who installed before this had Kagi written into + // their profile by us, and nothing moved them off it. + const home = seed({ url: KAGI_URL, legacyKagi: true }); + run([], { home }); + expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}'); + }); + + it('leaves an engine the user picked themselves alone', () => { + // We may correct our own default. We may not overwrite a deliberate choice. + const chosen = 'https://www.google.com/search?q={searchTerms}'; + const home = seed({ url: chosen, legacyKagi: true }); + run([], { home }); + expect(dse(home).url).toBe(chosen); + }); + + it('lets an explicit `tron search` override even a user-set engine', () => { + const home = seed({ url: 'https://www.google.com/search?q={searchTerms}', want: 'kagi' }); + run([], { home }); + expect(dse(home).url).toBe(KAGI_URL); + }); + + it('stops touching the setting once it has applied it', () => { + // Second launch must not re-apply, or changing the engine in + // chrome://settings/search would be undone on every start. + const first = run([]); + const chosen = 'https://www.startpage.com/sp/search?q={searchTerms}'; + writeFileSync( + prefsPath(first.home), + JSON.stringify({ default_search_provider_data: { template_url_data: { url: chosen } } }), + ); + run([], { home: first.home }); + expect(dse(first.home).url).toBe(chosen); + }); + + it('keeps the rest of Preferences when it rewrites the engine', () => { + const home = seed({ url: KAGI_URL, legacyKagi: true }); + run([], { home }); + const json = JSON.parse(readFileSync(prefsPath(home), 'utf8')); + expect(json.bookmark_bar?.show_on_all_tabs).toBe(true); + }); +}); + describe('engine reporting', () => { it('names the engine it is about to run', () => { const { stderr } = run([], { version: 'Chromium 141.0.0.0' }); diff --git a/apps/web/public/install.sh b/apps/web/public/install.sh index ba59b40..93a34c8 100755 --- a/apps/web/public/install.sh +++ b/apps/web/public/install.sh @@ -97,6 +97,8 @@ Usage: tron replay Replay a recorded trace against the session tron upgrade Update to the latest release tron clean Clear browser caches (keeps bookmarks, logins, history) + tron search [engine] Show or set the ADDRESS BAR's search engine + (the new-tab box is set in TronBrowser Settings) tron remove Uninstall TronBrowser (keeps your profile data) tron version Print the installed version tron help Show this help @@ -268,6 +270,38 @@ case "${1:-}" in rm -rf "$_data/Default/Cache" "$_data/Default/Code Cache" "$_data/Default/Service Worker" echo "Freed ~${_freed}MB from $_data. Bookmarks, passwords and logins untouched." done ;; + search) + # The ADDRESS BAR's engine. Chromium reads it from the profile at startup and + # only the launcher can write it there — an MV3 extension has no API for this + # on Linux — so this records the choice and the next launch applies it. The + # new-tab box has its own separate picker in TronBrowser Settings. + _engines="ddg kagi neosearch xprivo oxiverse" + _dirs="${TRONBROWSER_DATA:-$HOME/.tronbrowser}" + if [ -d "$HOME/TronBrowser" ]; then _dirs="$_dirs $HOME/TronBrowser"; fi + _want="${2:-}" + if [ -z "$_want" ]; then + for _d in $_dirs; do + _cur="$(cat "$_d/search-engine" 2>/dev/null || true)" + echo "Address bar: ${_cur:-ddg (default)} [$_d]" + done + echo "Available: $_engines" + echo "Set with: tron search ddg" + echo "(The new-tab search box is set separately in TronBrowser Settings.)" + exit 0 + fi + _ok=0 + for _e in $_engines; do + if [ "$_e" = "$_want" ]; then _ok=1; fi + done + if [ "$_ok" != "1" ]; then + echo "tron search: unknown engine '$_want'. Available: $_engines" >&2 + exit 1 + fi + for _d in $_dirs; do + mkdir -p "$_d" + printf '%s\n' "$_want" > "$_d/search-engine" + done + echo "Address bar set to '$_want'. Restart TronBrowser to apply ('tron restart')." ;; remove|uninstall) rm -rf "$APP_DIR" rm -f "$PREFIX/bin/tron" "$PREFIX/bin/tronbrowser" "$PREFIX/share/applications/tronbrowser.desktop" diff --git a/apps/web/public/settings-sections.js b/apps/web/public/settings-sections.js index 90d1f9b..9c2fbdf 100644 --- a/apps/web/public/settings-sections.js +++ b/apps/web/public/settings-sections.js @@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) { const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']); // Populate current values (on every mount, e.g. after a cloud pull). - if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi'; + if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg'; if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia'; if (el('tickers')) el('tickers').value = cur.tickers ?? ''; if (el('leagues')) el('leagues').value = cur.leagues ?? ''; diff --git a/apps/web/public/settings.html b/apps/web/public/settings.html index abc67c1..246c3ac 100644 --- a/apps/web/public/settings.html +++ b/apps/web/public/settings.html @@ -77,13 +77,16 @@

API keys (bring your own)

Search

-

Default search engine for the new-tab search box.

+

This sets the new-tab search box only. + The address bar is a browser-level setting this page cannot reach — change that + one with tron search <engine>, or in + chrome://settings/search.

diff --git a/apps/web/test/install-search.test.ts b/apps/web/test/install-search.test.ts new file mode 100644 index 0000000..d5a80ea --- /dev/null +++ b/apps/web/test/install-search.test.ts @@ -0,0 +1,118 @@ +// `tron search` sets the ADDRESS BAR's engine, which the launcher applies on the +// next start. It lives inside install.sh's `<<'TRON'` heredoc, so `sh -n +// install.sh` never parses it — these tests extract the generated CLI and run it, +// which is the only thing that checks that code at all. + +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const INSTALL_SH = join(HERE, '..', 'public', 'install.sh'); + +const roots: string[] = []; +let cli = ''; + +/** Pull the `tron` CLI back out of the heredoc that install.sh writes it from. */ +beforeAll(() => { + const src = readFileSync(INSTALL_SH, 'utf8'); + const start = src.indexOf("<<'TRON'\n"); + const end = src.indexOf('\nTRON\n', start); + expect(start, 'CLI heredoc not found in install.sh').toBeGreaterThan(-1); + expect(end, 'CLI heredoc has no terminator').toBeGreaterThan(start); + const body = src.slice(start + "<<'TRON'\n".length, end); + + const dir = mkdtempSync(join(tmpdir(), 'tron-cli-')); + roots.push(dir); + cli = join(dir, 'tron'); + writeFileSync(cli, body, { mode: 0o755 }); + + // It has to be a valid shell script before any behaviour matters. + expect(spawnSync('sh', ['-n', cli], { encoding: 'utf8' }).status, 'generated CLI is not valid sh').toBe(0); +}); + +function home(): string { + const h = mkdtempSync(join(tmpdir(), 'tron-search-')); + roots.push(h); + return h; +} + +function search(h: string, args: string[] = [], env: Record = {}) { + const r = spawnSync('sh', [cli, 'search', ...args], { + encoding: 'utf8', + env: { + PATH: process.env.PATH ?? '/usr/bin:/bin', + HOME: h, + TRONBROWSER_AUTO_UPGRADE: '0', + ...env, + }, + }); + return { status: r.status, out: `${r.stdout}${r.stderr}` }; +} + +const chosen = (h: string, data?: string) => { + const p = join(data ?? join(h, '.tronbrowser'), 'search-engine'); + return existsSync(p) ? readFileSync(p, 'utf8').trim() : null; +}; + +afterAll(() => { + for (const r of roots) rmSync(r, { recursive: true, force: true }); +}); + +describe('tron search', () => { + it('reports the default when nothing has been chosen', () => { + const h = home(); + const { status, out } = search(h); + expect(status).toBe(0); + expect(out).toContain('ddg'); + }); + + it('records the chosen engine where the launcher looks for it', () => { + const h = home(); + const { status, out } = search(h, ['neosearch']); + expect(status).toBe(0); + expect(chosen(h)).toBe('neosearch'); + expect(out).toContain('Restart TronBrowser'); + }); + + it('reports the engine back once it is set', () => { + const h = home(); + search(h, ['kagi']); + expect(search(h).out).toContain('kagi'); + }); + + it('rejects an engine the launcher cannot apply', () => { + // Silently accepting a name the launcher will refuse means the setting + // looks applied and then isn't. + const h = home(); + const { status, out } = search(h, ['altavista']); + expect(status).not.toBe(0); + expect(out).toContain('unknown engine'); + expect(chosen(h)).toBeNull(); + }); + + it('says the new-tab box is a separate setting', () => { + // The whole reported bug was believing one dropdown governed both. + expect(search(home()).out).toContain('new-tab'); + }); + + it('respects TRONBROWSER_DATA', () => { + const h = home(); + const alt = join(h, 'elsewhere'); + mkdirSync(alt, { recursive: true }); + search(h, ['xprivo'], { TRONBROWSER_DATA: alt }); + expect(chosen(h, alt)).toBe('xprivo'); + expect(chosen(h)).toBeNull(); + }); + + it('also writes the visible profile dir when one exists (snap layout)', () => { + const h = home(); + mkdirSync(join(h, 'TronBrowser'), { recursive: true }); + search(h, ['ddg']); + expect(chosen(h)).toBe('ddg'); + expect(chosen(h, join(h, 'TronBrowser'))).toBe('ddg'); + }); +});