/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { LinkPreviewModel } = ChromeUtils.importESModule( "moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs" ); const { SentencePostProcessor } = ChromeUtils.importESModule( "moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs" ); const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); const { BlockListManager } = ChromeUtils.importESModule( "chrome://global/content/ml/Utils.sys.mjs" ); /** * Test that text is processed for ai. */ add_task(function test_text_processing() { const text = "This is a test sentence. "; Assert.equal( LinkPreviewModel.preprocessText(text), "This is a test sentence.", "basic sentence should be trimmed" ); Assert.equal( LinkPreviewModel.preprocessText("Too short."), "", "short sentence should be removed" ); Assert.equal( LinkPreviewModel.preprocessText("Today is Mar. 12, 2025."), "Today is Mar. 12, 2025.", "abbreviations don't break sentences" ); Assert.equal( LinkPreviewModel.preprocessText("Thisisatestsentence but fewwords."), "", "needs enough words" ); Assert.equal( LinkPreviewModel.preprocessText( "This is a test sentence without punctuation" ), "", "needs punctuation" ); Assert.equal( LinkPreviewModel.preprocessText(`${text} Too short. Today is Mar. 12, 2025. Thisisatestsentence but fewwords. This is a test sentence without punctuation`), "This is a test sentence. Today is Mar. 12, 2025.", "multiple sentences should be processed" ); Assert.equal( LinkPreviewModel.preprocessText(`${text} Short \t\t\t\t\t\t\t\t\t\t without spaces. It has \t \t \t multiple consecutive spaces in between words. They should all go away `), "This is a test sentence. It has multiple consecutive spaces in between words.", "Remove consecutive spaces by single." ); Assert.equal( LinkPreviewModel.preprocessText(text.repeat(100)), text.repeat(6).trim(), "restrict to 6 sentences" ); Assert.equal( LinkPreviewModel.preprocessText( "This is a test sentence with an emoji \ud83e\udd14." ), "This is a test sentence with an emoji .", "emoji should be removed from the sentence" ); }); /** * Test that prefs affect text processing. */ add_task(function test_text_processing_prefs() { const text = "This is a test sentence. "; Services.prefs.setIntPref("browser.ml.linkPreview.inputSentences", 3); Assert.equal( LinkPreviewModel.preprocessText(text.repeat(100)), text.repeat(3).trim(), "restrict to 3 sentences" ); Services.prefs.setIntPref( "browser.ml.linkPreview.minWordsPerOutputSentences", 3 ); let processor = new SentencePostProcessor(); let output = ""; output += processor.put("I am writing unit test.").sentence; output += processor.put(" So great.").sentence; output += processor.put(" I like it.").sentence; output += processor.put(" Done. ").sentence; Assert.equal( output, "I am writing unit test. I like it. ", "ignore too short sentences" ); Services.prefs.setIntPref( "browser.ml.linkPreview.minWordsPerOutputSentences", 4 ); processor = new SentencePostProcessor(); output = ""; output += processor.put("I am writing unit test.").sentence; output += processor.put(" So great.").sentence; output += processor.put(" I like it.").sentence; output += processor.put(" Done. ").sentence; Assert.equal( output, "I am writing unit test. ", "ignore too short sentences" ); Services.prefs.clearUserPref("browser.ml.linkPreview.inputSentences"); Services.prefs.clearUserPref( "browser.ml.linkPreview.minWordsPerOutputSentences" ); }); /** * Test that post processor provides sentences. */ add_task(function test_sentence_post_processor() { const processor = new SentencePostProcessor(); Assert.deepEqual( processor.put("Hello "), { sentence: "", abort: false }, "no sentence yet" ); Assert.deepEqual( processor.put("world. "), { sentence: "", abort: false }, "sentence complete but not next" ); Assert.deepEqual( processor.put("How "), { sentence: "Hello world. ", abort: false }, "previous sentence complete" ); Assert.deepEqual( processor.put("are you today? I'm"), { sentence: "How are you today? ", abort: false }, "question complete" ); Assert.deepEqual( processor.put(" fine. And"), { sentence: "I'm fine. ", abort: true }, "response complete" ); Assert.deepEqual( processor.put("you? Good"), { sentence: "", abort: true }, "hit limit" ); Assert.equal(processor.flush(), "", "still nothing at limit"); }); /** * Test that generateTextAI works properly with the blocklist */ add_task(async function test_generateAI_with_blocklist() { // Mocked ML Engine const engine = { async *runWithGenerator() { const preview = "Hello world, I am here and would like to make a sentence. Now, more sentences are coming. Even more are raining here. Bye"; for (const text of preview) { yield { text }; } }, terminate() {}, }; // Mocked Blocked List Manager let manager = new BlockListManager({ blockNgrams: [BlockListManager.encodeBase64("hello")], language: "en", }); // Disable block list Services.prefs.setBoolPref("browser.ml.linkPreview.blockListEnabled", false); let createEngineStub = sinon .stub(LinkPreviewModel, "createEngine") .returns(engine); let managerStub = sinon .stub(BlockListManager, "initializeFromRemoteSettings") .returns(manager); let numOutputs = 0; await LinkPreviewModel.generateTextAI( "This is the big article. Now give me its preview. Please.", { onText: () => { numOutputs += 1; }, } ); Assert.equal( numOutputs, 3, "should output all sentences when block list is disabled" ); // Enable block list Services.prefs.setBoolPref("browser.ml.linkPreview.blockListEnabled", true); numOutputs = 0; await LinkPreviewModel.generateTextAI( "This is the big article. Now give me its preview. Please.", { onText: () => { numOutputs += 1; }, } ); Assert.equal( numOutputs, 0, "Should output no sentences when 1st sentence contains block word and block list enabled." ); managerStub.restore(); manager = new BlockListManager({ blockNgrams: [BlockListManager.encodeBase64("coming")], language: "en", }); managerStub = sinon .stub(BlockListManager, "initializeFromRemoteSettings") .returns(manager); // Force link preview to reload blockList manager Services.prefs.setBoolPref("browser.ml.linkPreview.blockListEnabled", false); await LinkPreviewModel.generateTextAI( "This is the big article. Now give me its preview. Please." ); Assert.equal(LinkPreviewModel.blockListManager, null); // Now re-enable block list Services.prefs.setBoolPref("browser.ml.linkPreview.blockListEnabled", true); numOutputs = 0; await LinkPreviewModel.generateTextAI( "This is the big article. Now give me its preview. Please.", { onText: () => { numOutputs += 1; }, } ); Assert.equal( numOutputs, 1, "Should output 1 sentence when blocked word found 1st time in 2nd sentence and block list enabled." ); Services.prefs.setBoolPref("browser.ml.linkPreview.blockListEnabled", false); numOutputs = 0; await LinkPreviewModel.generateTextAI( "This is the big article. Now give me its preview. Please.", { onText: () => { numOutputs += 1; }, } ); Assert.equal( numOutputs, 3, "all sentences should be outputted when block list disabled" ); Services.prefs.clearUserPref("browser.ml.linkPreview.blockListEnabled"); createEngineStub.restore(); managerStub.restore(); }); /** * Test post processor respects limits. */ add_task(function test_sentence_post_processor_limits() { const processor = new SentencePostProcessor({ maxNumOutputSentences: 1, }); Assert.deepEqual( processor.put("Hi. There. "), { sentence: "Hi. ", abort: true }, "first sentence" ); Assert.deepEqual( processor.put("Nope."), { sentence: "", abort: true }, "no more sentences" ); Services.prefs.setIntPref("browser.ml.linkPreview.outputSentences", 2); const viaPref = new SentencePostProcessor(); Assert.deepEqual( viaPref.put("Hi. There. "), { sentence: "Hi. ", abort: false }, "first sentence" ); Assert.deepEqual( viaPref.put("Yup. "), { sentence: "There. ", abort: true }, "second sentence" ); Assert.deepEqual( viaPref.put("Nope."), { sentence: "", abort: true }, "no more sentences" ); Services.prefs.clearUserPref("browser.ml.linkPreview.outputSentences"); });