test_TitleGeneration.js (11609B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const { generateChatTitle } = ChromeUtils.importESModule( 6 "moz-src:///browser/components/aiwindow/models/TitleGeneration.sys.mjs" 7 ); 8 9 const { openAIEngine } = ChromeUtils.importESModule( 10 "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs" 11 ); 12 13 const { sinon } = ChromeUtils.importESModule( 14 "resource://testing-common/Sinon.sys.mjs" 15 ); 16 17 /** 18 * Constants for preference keys and test values 19 */ 20 const PREF_API_KEY = "browser.aiwindow.apiKey"; 21 const PREF_ENDPOINT = "browser.aiwindow.endpoint"; 22 const PREF_MODEL = "browser.aiwindow.model"; 23 24 const API_KEY = "test-api-key"; 25 const ENDPOINT = "https://api.test-endpoint.com/v1"; 26 const MODEL = "test-model"; 27 28 /** 29 * Cleans up preferences after testing 30 */ 31 registerCleanupFunction(() => { 32 for (let pref of [PREF_API_KEY, PREF_ENDPOINT, PREF_MODEL]) { 33 if (Services.prefs.prefHasUserValue(pref)) { 34 Services.prefs.clearUserPref(pref); 35 } 36 } 37 }); 38 39 /** 40 * Test that generateChatTitle successfully generates a title 41 */ 42 add_task(async function test_generateChatTitle_success() { 43 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 44 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 45 Services.prefs.setStringPref(PREF_MODEL, MODEL); 46 47 const sb = sinon.createSandbox(); 48 try { 49 // Mock the engine response 50 const mockResponse = { 51 choices: [ 52 { 53 message: { 54 content: "Weather Forecast Query", 55 }, 56 }, 57 ], 58 }; 59 60 const fakeEngineInstance = { 61 run: sb.stub().resolves(mockResponse), 62 }; 63 64 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 65 66 const message = "What's the weather like today?"; 67 const currentTab = { 68 url: "https://weather.example.com", 69 title: "Weather Forecast", 70 description: "Get current weather conditions", 71 }; 72 73 const title = await generateChatTitle(message, currentTab); 74 75 Assert.equal( 76 title, 77 "Weather Forecast Query", 78 "Should return the generated title from the LLM" 79 ); 80 81 Assert.ok( 82 fakeEngineInstance.run.calledOnce, 83 "Engine run should be called once" 84 ); 85 86 // Verify the messages structure passed to the engine 87 const callArgs = fakeEngineInstance.run.firstCall.args[0]; 88 Assert.ok(callArgs.messages, "Should pass messages to the engine"); 89 Assert.equal( 90 callArgs.messages.length, 91 2, 92 "Should have system and user messages" 93 ); 94 Assert.equal( 95 callArgs.messages[0].role, 96 "system", 97 "First message should be system" 98 ); 99 Assert.equal( 100 callArgs.messages[1].role, 101 "user", 102 "Second message should be user" 103 ); 104 Assert.equal( 105 callArgs.messages[1].content, 106 message, 107 "User message should contain the input message" 108 ); 109 110 // Verify the system prompt contains the tab information 111 const systemContent = callArgs.messages[0].content; 112 Assert.ok( 113 systemContent.includes(currentTab.url), 114 "System prompt should include tab URL" 115 ); 116 Assert.ok( 117 systemContent.includes(currentTab.title), 118 "System prompt should include tab title" 119 ); 120 Assert.ok( 121 systemContent.includes(currentTab.description), 122 "System prompt should include tab description" 123 ); 124 } finally { 125 sb.restore(); 126 } 127 }); 128 129 /** 130 * Test that generateChatTitle handles missing tab information 131 */ 132 add_task(async function test_generateChatTitle_no_tab_info() { 133 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 134 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 135 Services.prefs.setStringPref(PREF_MODEL, MODEL); 136 137 const sb = sinon.createSandbox(); 138 try { 139 const mockResponse = { 140 choices: [ 141 { 142 message: { 143 content: "General Question", 144 }, 145 }, 146 ], 147 }; 148 149 const fakeEngineInstance = { 150 run: sb.stub().resolves(mockResponse), 151 }; 152 153 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 154 155 const message = "Tell me about AI"; 156 const currentTab = null; 157 158 const title = await generateChatTitle(message, currentTab); 159 160 Assert.equal( 161 title, 162 "General Question", 163 "Should return the generated title even without tab info" 164 ); 165 166 // Verify the system prompt handles null tab 167 const callArgs = fakeEngineInstance.run.firstCall.args[0]; 168 Assert.ok(callArgs.messages, "Should pass messages even with null tab"); 169 } finally { 170 sb.restore(); 171 } 172 }); 173 174 /** 175 * Test that generateChatTitle handles empty tab fields 176 */ 177 add_task(async function test_generateChatTitle_empty_tab_fields() { 178 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 179 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 180 Services.prefs.setStringPref(PREF_MODEL, MODEL); 181 182 const sb = sinon.createSandbox(); 183 try { 184 const mockResponse = { 185 choices: [ 186 { 187 message: { 188 content: "Untitled Chat", 189 }, 190 }, 191 ], 192 }; 193 194 const fakeEngineInstance = { 195 run: sb.stub().resolves(mockResponse), 196 }; 197 198 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 199 200 const message = "Hello"; 201 const currentTab = { 202 url: "", 203 title: "", 204 description: "", 205 }; 206 207 const title = await generateChatTitle(message, currentTab); 208 209 Assert.equal(title, "Untitled Chat", "Should handle empty tab fields"); 210 211 // Verify the system prompt includes the empty tab object 212 const callArgs = fakeEngineInstance.run.firstCall.args[0]; 213 Assert.ok( 214 callArgs.messages, 215 "Should pass messages even with empty tab fields" 216 ); 217 } finally { 218 sb.restore(); 219 } 220 }); 221 222 /** 223 * Test that generateChatTitle handles engine errors gracefully 224 */ 225 add_task(async function test_generateChatTitle_engine_error() { 226 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 227 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 228 Services.prefs.setStringPref(PREF_MODEL, MODEL); 229 230 const sb = sinon.createSandbox(); 231 try { 232 const fakeEngineInstance = { 233 run: sb.stub().rejects(new Error("Engine failed")), 234 }; 235 236 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 237 238 const message = "Test message for error handling"; 239 const currentTab = { 240 url: "https://example.com", 241 title: "Example", 242 description: "Test", 243 }; 244 245 const title = await generateChatTitle(message, currentTab); 246 247 Assert.equal( 248 title, 249 "Test message for error...", 250 "Should return first four words when engine fails" 251 ); 252 } finally { 253 sb.restore(); 254 } 255 }); 256 257 /** 258 * Test that generateChatTitle handles malformed engine responses 259 */ 260 add_task(async function test_generateChatTitle_malformed_response() { 261 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 262 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 263 Services.prefs.setStringPref(PREF_MODEL, MODEL); 264 265 const sb = sinon.createSandbox(); 266 try { 267 // Test with missing choices 268 const mockResponse1 = {}; 269 let fakeEngineInstance = { 270 run: sb.stub().resolves(mockResponse1), 271 }; 272 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 273 274 let title = await generateChatTitle("test message one two", null); 275 Assert.equal( 276 title, 277 "test message one two...", 278 "Should return first four words for missing choices" 279 ); 280 281 // Test with empty choices array 282 sb.restore(); 283 const sb2 = sinon.createSandbox(); 284 const mockResponse2 = { choices: [] }; 285 fakeEngineInstance = { 286 run: sb2.stub().resolves(mockResponse2), 287 }; 288 sb2.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 289 290 title = await generateChatTitle("another test message here", null); 291 Assert.equal( 292 title, 293 "another test message here...", 294 "Should return first four words for empty choices" 295 ); 296 297 // Test with null content 298 sb2.restore(); 299 const sb3 = sinon.createSandbox(); 300 const mockResponse3 = { 301 choices: [{ message: { content: null } }], 302 }; 303 fakeEngineInstance = { 304 run: sb3.stub().resolves(mockResponse3), 305 }; 306 sb3.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 307 308 title = await generateChatTitle("short test here", null); 309 Assert.equal( 310 title, 311 "short test here...", 312 "Should return first four words for null content" 313 ); 314 315 sb3.restore(); 316 } finally { 317 sb.restore(); 318 } 319 }); 320 321 /** 322 * Test that generateChatTitle trims whitespace from response 323 */ 324 add_task(async function test_generateChatTitle_trim_whitespace() { 325 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 326 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 327 Services.prefs.setStringPref(PREF_MODEL, MODEL); 328 329 const sb = sinon.createSandbox(); 330 try { 331 const mockResponse = { 332 choices: [ 333 { 334 message: { 335 content: " Title With Spaces \n\n", 336 }, 337 }, 338 ], 339 }; 340 341 const fakeEngineInstance = { 342 run: sb.stub().resolves(mockResponse), 343 }; 344 345 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 346 347 const title = await generateChatTitle("test", null); 348 349 Assert.equal( 350 title, 351 "Title With Spaces", 352 "Should trim whitespace from generated title" 353 ); 354 } finally { 355 sb.restore(); 356 } 357 }); 358 359 /** 360 * Test default title generation with fewer than four words 361 */ 362 add_task(async function test_generateChatTitle_short_message() { 363 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 364 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 365 Services.prefs.setStringPref(PREF_MODEL, MODEL); 366 367 const sb = sinon.createSandbox(); 368 try { 369 const fakeEngineInstance = { 370 run: sb.stub().rejects(new Error("Engine failed")), 371 }; 372 373 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 374 375 // Test with three words 376 let title = await generateChatTitle("Hello there friend", null); 377 Assert.equal( 378 title, 379 "Hello there friend...", 380 "Should return three words with ellipsis" 381 ); 382 383 // Test with one word 384 title = await generateChatTitle("Hello", null); 385 Assert.equal(title, "Hello...", "Should return one word with ellipsis"); 386 387 // Test with empty message 388 title = await generateChatTitle("", null); 389 Assert.equal( 390 title, 391 "New Chat", 392 "Should return 'New Chat' for empty message" 393 ); 394 395 // Test with whitespace only 396 title = await generateChatTitle(" ", null); 397 Assert.equal( 398 title, 399 "New Chat", 400 "Should return 'New Chat' for whitespace-only message" 401 ); 402 } finally { 403 sb.restore(); 404 } 405 }); 406 407 /** 408 * Test default title generation with more than four words 409 */ 410 add_task(async function test_generateChatTitle_long_message() { 411 Services.prefs.setStringPref(PREF_API_KEY, API_KEY); 412 Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT); 413 Services.prefs.setStringPref(PREF_MODEL, MODEL); 414 415 const sb = sinon.createSandbox(); 416 try { 417 const fakeEngineInstance = { 418 run: sb.stub().rejects(new Error("Engine failed")), 419 }; 420 421 sb.stub(openAIEngine, "_createEngine").resolves(fakeEngineInstance); 422 423 const message = "This is a very long message with many words"; 424 const title = await generateChatTitle(message, null); 425 426 Assert.equal( 427 title, 428 "This is a very...", 429 "Should return only first four words with ellipsis" 430 ); 431 } finally { 432 sb.restore(); 433 } 434 });