tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 2f8ce2d3d1e117ebda8d02d185565b981cfa53ce
parent 7de886f333b2e9fadeff38ec4a715fbdbc2a9566
Author: Tom Zhang <tzhang@mozilla.com>
Date:   Mon,  1 Dec 2025 18:57:49 +0000

Bug 2002840 - add function to return real time info injection message & tests r=ai-models-reviewers,cgopal

Differential Revision: https://phabricator.services.mozilla.com/D274355

Diffstat:
Mbrowser/base/content/test/static/browser_all_files_referenced.js | 4++++
Abrowser/components/aiwindow/models/ChatUtils.mjs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/models/moz.build | 6+++++-
Abrowser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/models/tests/xpcshell/xpcshell.toml | 2++
5 files changed, 292 insertions(+), 1 deletion(-)

diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js @@ -341,6 +341,10 @@ var allowlist = [ { file: "moz-src:///browser/components/aiwindow/models/IntentClassifier.sys.mjs", }, + // Bug 2002840 - add function to return real time info injection message & tests (backed out due to unused file) + { + file: "moz-src:///browser/components/aiwindow/models/ChatUtils.mjs", + }, ]; if (AppConstants.NIGHTLY_BUILD) { diff --git a/browser/components/aiwindow/models/ChatUtils.mjs b/browser/components/aiwindow/models/ChatUtils.mjs @@ -0,0 +1,111 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + PageDataService: + "moz-src:///browser/components/pagedata/PageDataService.sys.mjs", +}); + +/** + * Get the current local time in ISO format with timezone offset. + * + * @returns {string} + */ +export function getLocalIsoTime() { + try { + const date = new Date(); + const tzOffsetMinutes = date.getTimezoneOffset(); + const adjusted = new Date(date.getTime() - tzOffsetMinutes * 60000) + .toISOString() + .slice(0, 19); // Keep up to seconds + const sign = tzOffsetMinutes <= 0 ? "+" : "-"; + const hours = String(Math.floor(Math.abs(tzOffsetMinutes) / 60)).padStart( + 2, + "0" + ); + const minutes = String(Math.abs(tzOffsetMinutes) % 60).padStart(2, "0"); + return `${adjusted}${sign}${hours}:${minutes}`; + } catch { + return null; + } +} + +function resolveTabMetadataDependencies(overrides = {}) { + return { + BrowserWindowTracker: + overrides.BrowserWindowTracker ?? lazy.BrowserWindowTracker, + PageDataService: overrides.PageDataService ?? lazy.PageDataService, + }; +} + +/** + * Get current tab metadata: url, title, description if available. + * + * @param {object} [depsOverride] + * @returns {Promise<{url: string, title: string, description: string}>} + */ +export async function getCurrentTabMetadata(depsOverride) { + const { BrowserWindowTracker, PageDataService } = + resolveTabMetadataDependencies(depsOverride); + const win = BrowserWindowTracker.getTopWindow(); + const browser = win?.gBrowser?.selectedBrowser; + if (!browser) { + return { url: "", title: "", description: "" }; + } + + const url = browser.currentURI?.spec || ""; + const title = browser.contentTitle || browser.documentTitle || ""; + + let description = ""; + if (url) { + description = + PageDataService.getCached(url)?.description || + (await PageDataService.fetchPageData(url))?.description || + ""; + } + + return { url, title, description }; +} + +/** + * Construct real time information injection message, to be inserted before + * the insights injection message and the user message in the conversation + * messages list. + * + * @param {object} [depsOverride] + * @returns {Promise<{role: string, content: string}>} + */ +export async function constructRealTimeInfoInjectionMessage(depsOverride) { + const { url, title, description } = await getCurrentTabMetadata(depsOverride); + const isoTimestamp = getLocalIsoTime(); + const datePart = isoTimestamp?.split("T")[0] ?? ""; + const locale = Services.locale.appLocaleAsBCP47; + const hasTabInfo = Boolean(url || title || description); + const tabSection = hasTabInfo + ? [ + `Current active browser tab details:`, + `- URL: ${url}`, + `- Title: ${title}`, + `- Description: ${description}`, + ] + : [`No active browser tab.`]; + + const content = [ + `Below are some real-time context details you can use to inform your response:`, + `Locale: ${locale}`, + `Current date & time in ISO format: ${isoTimestamp}`, + `Today's date: ${datePart || "Unavailable"}`, + ``, + ...tabSection, + ].join("\n"); + + return { + role: "system", + content, + }; +} diff --git a/browser/components/aiwindow/models/moz.build b/browser/components/aiwindow/models/moz.build @@ -5,7 +5,11 @@ with Files("**"): BUG_COMPONENT = ("Core", "Machine Learning: General") -MOZ_SRC_FILES += ["InsightsHistorySource.sys.mjs", "IntentClassifier.sys.mjs"] +MOZ_SRC_FILES += [ + "ChatUtils.mjs", + "InsightsHistorySource.sys.mjs", + "IntentClassifier.sys.mjs", +] XPCSHELL_TESTS_MANIFESTS += [ "tests/xpcshell/xpcshell.toml", diff --git a/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js b/browser/components/aiwindow/models/tests/xpcshell/test_ChatUtils.js @@ -0,0 +1,170 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const { + constructRealTimeInfoInjectionMessage, + getLocalIsoTime, + getCurrentTabMetadata, +} = ChromeUtils.importESModule( + "moz-src:///browser/components/aiwindow/models/ChatUtils.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +add_task(function test_getLocalIsoTime_returns_offset_timestamp() { + const sb = sinon.createSandbox(); + const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) }); + try { + const iso = getLocalIsoTime(); + Assert.ok( + typeof iso === "string" && !!iso.length, + "Should return a non-empty string" + ); + Assert.ok( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(iso), + "Should include date, time (up to seconds), and timezone offset" + ); + } finally { + clock.restore(); + sb.restore(); + } +}); + +add_task(async function test_getCurrentTabMetadata_fetch_fallback() { + const sb = sinon.createSandbox(); + const tracker = { getTopWindow: sb.stub() }; + const pageData = { + getCached: sb.stub(), + fetchPageData: sb.stub(), + }; + const fakeBrowser = { + currentURI: { spec: "https://example.com/article" }, + contentTitle: "", + documentTitle: "Example Article", + }; + + tracker.getTopWindow.returns({ + gBrowser: { selectedBrowser: fakeBrowser }, + }); + pageData.getCached.returns(null); + const fetchStub = pageData.fetchPageData.resolves({ + description: "Fetched description", + }); + + try { + const result = await getCurrentTabMetadata({ + BrowserWindowTracker: tracker, + PageDataService: pageData, + }); + Assert.deepEqual(result, { + url: "https://example.com/article", + title: "Example Article", + description: "Fetched description", + }); + Assert.ok(fetchStub.calledOnce, "Should fetch description when not cached"); + } finally { + sb.restore(); + } +}); + +add_task( + async function test_constructRealTimeInfoInjectionMessage_with_tab_info() { + const sb = sinon.createSandbox(); + const tracker = { getTopWindow: sb.stub() }; + const pageData = { + getCached: sb.stub(), + fetchPageData: sb.stub(), + }; + const locale = Services.locale.appLocaleAsBCP47; + const fakeBrowser = { + currentURI: { spec: "https://mozilla.org" }, + contentTitle: "Mozilla", + documentTitle: "Mozilla", + }; + + tracker.getTopWindow.returns({ + gBrowser: { selectedBrowser: fakeBrowser }, + }); + pageData.getCached.returns({ + description: "Internet for people", + }); + const fetchStub = pageData.fetchPageData; + const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) }); + + try { + const message = await constructRealTimeInfoInjectionMessage({ + BrowserWindowTracker: tracker, + PageDataService: pageData, + }); + Assert.equal(message.role, "system", "Should return system role"); + Assert.ok( + message.content.includes(`Locale: ${locale}`), + "Should include locale" + ); + Assert.ok( + message.content.includes("Current active browser tab details:"), + "Should include tab details heading" + ); + Assert.ok( + message.content.includes("- URL: https://mozilla.org"), + "Should include tab URL" + ); + Assert.ok( + message.content.includes("- Title: Mozilla"), + "Should include tab title" + ); + Assert.ok( + message.content.includes("- Description: Internet for people"), + "Should include tab description" + ); + Assert.ok( + fetchStub.notCalled, + "Should not fetch when cached data exists" + ); + } finally { + clock.restore(); + sb.restore(); + } + } +); + +add_task( + async function test_constructRealTimeInfoInjectionMessage_without_tab_info() { + const sb = sinon.createSandbox(); + const tracker = { getTopWindow: sb.stub() }; + const pageData = { + getCached: sb.stub(), + fetchPageData: sb.stub(), + }; + const locale = Services.locale.appLocaleAsBCP47; + + tracker.getTopWindow.returns(null); + const clock = sb.useFakeTimers({ now: Date.UTC(2025, 11, 27, 14, 0, 0) }); + + try { + const message = await constructRealTimeInfoInjectionMessage({ + BrowserWindowTracker: tracker, + PageDataService: pageData, + }); + Assert.ok( + message.content.includes("No active browser tab."), + "Should mention missing tab info" + ); + Assert.ok( + !message.content.includes("- URL:"), + "Should not include empty tab fields" + ); + Assert.ok( + message.content.includes(`Locale: ${locale}`), + "Should include system locale" + ); + } finally { + clock.restore(); + sb.restore(); + } + } +); diff --git a/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml b/browser/components/aiwindow/models/tests/xpcshell/xpcshell.toml @@ -4,6 +4,8 @@ head = "head.js" firefox-appdir = "browser" support-files = [] +["test_ChatUtils.js"] + ["test_InsightsHistorySource.js"] ["test_intent_classifier.js"]