PersonalityProviderWorkerClass.test.js (14952B)
1 import { GlobalOverrider } from "test/unit/utils"; 2 import { PersonalityProviderWorker } from "lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs"; 3 import { 4 tokenize, 5 toksToTfIdfVector, 6 } from "lib/PersonalityProvider/Tokenize.mjs"; 7 import { RecipeExecutor } from "lib/PersonalityProvider/RecipeExecutor.mjs"; 8 import { NmfTextTagger } from "lib/PersonalityProvider/NmfTextTagger.mjs"; 9 import { NaiveBayesTextTagger } from "lib/PersonalityProvider/NaiveBayesTextTagger.mjs"; 10 11 describe("Personality Provider Worker Class", () => { 12 let instance; 13 let globals; 14 let sandbox; 15 16 beforeEach(() => { 17 sandbox = sinon.createSandbox(); 18 globals = new GlobalOverrider(); 19 globals.set("tokenize", tokenize); 20 globals.set("toksToTfIdfVector", toksToTfIdfVector); 21 globals.set("NaiveBayesTextTagger", NaiveBayesTextTagger); 22 globals.set("NmfTextTagger", NmfTextTagger); 23 globals.set("RecipeExecutor", RecipeExecutor); 24 instance = new PersonalityProviderWorker(); 25 26 // mock the RecipeExecutor 27 instance.recipeExecutor = { 28 executeRecipe: (item, recipe) => { 29 if (recipe === "history_item_builder") { 30 if (item.title === "fail") { 31 return null; 32 } 33 return { 34 title: item.title, 35 score: item.frecency, 36 type: "history_item", 37 }; 38 } else if (recipe === "interest_finalizer") { 39 return { 40 title: item.title, 41 score: item.score * 100, 42 type: "interest_vector", 43 }; 44 } else if (recipe === "item_to_rank_builder") { 45 if (item.title === "fail") { 46 return null; 47 } 48 return { 49 item_title: item.title, 50 item_score: item.score, 51 type: "item_to_rank", 52 }; 53 } else if (recipe === "item_ranker") { 54 if (item.title === "fail" || item.item_title === "fail") { 55 return null; 56 } 57 return { 58 title: item.title, 59 score: item.item_score * item.score, 60 type: "ranked_item", 61 }; 62 } 63 return null; 64 }, 65 executeCombinerRecipe: (item1, item2, recipe) => { 66 if (recipe === "interest_combiner") { 67 if ( 68 item1.title === "combiner_fail" || 69 item2.title === "combiner_fail" 70 ) { 71 return null; 72 } 73 if (item1.type === undefined) { 74 item1.type = "combined_iv"; 75 } 76 if (item1.score === undefined) { 77 item1.score = 0; 78 } 79 return { type: item1.type, score: item1.score + item2.score }; 80 } 81 return null; 82 }, 83 }; 84 85 instance.interestConfig = { 86 history_item_builder: "history_item_builder", 87 history_required_fields: ["a", "b", "c"], 88 interest_finalizer: "interest_finalizer", 89 item_to_rank_builder: "item_to_rank_builder", 90 item_ranker: "item_ranker", 91 interest_combiner: "interest_combiner", 92 }; 93 }); 94 afterEach(() => { 95 sinon.restore(); 96 sandbox.restore(); 97 globals.restore(); 98 }); 99 describe("#setBaseAttachmentsURL", () => { 100 it("should set baseAttachmentsURL", () => { 101 instance.setBaseAttachmentsURL("url"); 102 assert.equal(instance.baseAttachmentsURL, "url"); 103 }); 104 }); 105 describe("#setInterestConfig", () => { 106 it("should set interestConfig", () => { 107 instance.setInterestConfig("config"); 108 assert.equal(instance.interestConfig, "config"); 109 }); 110 }); 111 describe("#setInterestVector", () => { 112 it("should set interestVector", () => { 113 instance.setInterestVector("vector"); 114 assert.equal(instance.interestVector, "vector"); 115 }); 116 }); 117 describe("#onSync", async () => { 118 it("should sync remote settings collection from onSync", async () => { 119 sinon.stub(instance, "deleteAttachment").resolves(); 120 sinon.stub(instance, "maybeDownloadAttachment").resolves(); 121 122 instance.onSync({ 123 data: { 124 created: ["create-1", "create-2"], 125 updated: [ 126 { old: "update-old-1", new: "update-new-1" }, 127 { old: "update-old-2", new: "update-new-2" }, 128 ], 129 deleted: ["delete-2", "delete-1"], 130 }, 131 }); 132 133 assert(instance.maybeDownloadAttachment.withArgs("create-1").calledOnce); 134 assert(instance.maybeDownloadAttachment.withArgs("create-2").calledOnce); 135 assert( 136 instance.maybeDownloadAttachment.withArgs("update-new-1").calledOnce 137 ); 138 assert( 139 instance.maybeDownloadAttachment.withArgs("update-new-2").calledOnce 140 ); 141 142 assert(instance.deleteAttachment.withArgs("delete-1").calledOnce); 143 assert(instance.deleteAttachment.withArgs("delete-2").calledOnce); 144 assert(instance.deleteAttachment.withArgs("update-old-1").calledOnce); 145 assert(instance.deleteAttachment.withArgs("update-old-2").calledOnce); 146 }); 147 }); 148 describe("#maybeDownloadAttachment", () => { 149 it("should attempt _downloadAttachment three times for maybeDownloadAttachment", async () => { 150 let existsStub; 151 let statStub; 152 let attachmentStub; 153 sinon.stub(instance, "_downloadAttachment").resolves(); 154 const makeDirStub = globals.sandbox 155 .stub(global.IOUtils, "makeDirectory") 156 .resolves(); 157 158 existsStub = globals.sandbox 159 .stub(global.IOUtils, "exists") 160 .resolves(true); 161 162 statStub = globals.sandbox 163 .stub(global.IOUtils, "stat") 164 .resolves({ size: "1" }); 165 166 attachmentStub = { 167 attachment: { 168 filename: "file", 169 size: "1", 170 // This hash matches the hash generated from the empty Uint8Array returned by the IOUtils.read stub. 171 hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 172 }, 173 }; 174 175 await instance.maybeDownloadAttachment(attachmentStub); 176 assert.calledWith(makeDirStub, "personality-provider"); 177 assert.calledOnce(existsStub); 178 assert.calledOnce(statStub); 179 assert.notCalled(instance._downloadAttachment); 180 181 existsStub.resetHistory(); 182 statStub.resetHistory(); 183 instance._downloadAttachment.resetHistory(); 184 185 attachmentStub = { 186 attachment: { 187 filename: "file", 188 size: "2", 189 }, 190 }; 191 192 await instance.maybeDownloadAttachment(attachmentStub); 193 assert.calledThrice(existsStub); 194 assert.calledThrice(statStub); 195 assert.calledThrice(instance._downloadAttachment); 196 197 existsStub.resetHistory(); 198 statStub.resetHistory(); 199 instance._downloadAttachment.resetHistory(); 200 201 attachmentStub = { 202 attachment: { 203 filename: "file", 204 size: "1", 205 // Bogus hash to trigger an update. 206 hash: "1234", 207 }, 208 }; 209 210 await instance.maybeDownloadAttachment(attachmentStub); 211 assert.calledThrice(existsStub); 212 assert.calledThrice(statStub); 213 assert.calledThrice(instance._downloadAttachment); 214 }); 215 }); 216 describe("#_downloadAttachment", () => { 217 beforeEach(() => { 218 globals.set("Uint8Array", class Uint8Array {}); 219 }); 220 it("should write a file from _downloadAttachment", async () => { 221 globals.set( 222 "XMLHttpRequest", 223 class { 224 constructor() { 225 this.status = 200; 226 this.response = "response!"; 227 } 228 open() {} 229 setRequestHeader() {} 230 send() {} 231 } 232 ); 233 234 const ioutilsWriteStub = globals.sandbox 235 .stub(global.IOUtils, "write") 236 .resolves(); 237 238 await instance._downloadAttachment({ 239 attachment: { location: "location", filename: "filename" }, 240 }); 241 242 const writeArgs = ioutilsWriteStub.firstCall.args; 243 assert.equal(writeArgs[0], "filename"); 244 assert.equal(writeArgs[2].tmpPath, "filename.tmp"); 245 }); 246 it("should call console.error from _downloadAttachment if not valid response", async () => { 247 globals.set( 248 "XMLHttpRequest", 249 class { 250 constructor() { 251 this.status = 0; 252 this.response = "response!"; 253 } 254 open() {} 255 setRequestHeader() {} 256 send() {} 257 } 258 ); 259 260 const consoleErrorStub = globals.sandbox 261 .stub(console, "error") 262 .resolves(); 263 264 await instance._downloadAttachment({ 265 attachment: { location: "location", filename: "filename" }, 266 }); 267 268 assert.calledOnce(consoleErrorStub); 269 }); 270 }); 271 describe("#deleteAttachment", () => { 272 it("should remove attachments when calling deleteAttachment", async () => { 273 const makeDirStub = globals.sandbox 274 .stub(global.IOUtils, "makeDirectory") 275 .resolves(); 276 const removeStub = globals.sandbox 277 .stub(global.IOUtils, "remove") 278 .resolves(); 279 await instance.deleteAttachment({ attachment: { filename: "filename" } }); 280 assert.calledOnce(makeDirStub); 281 assert.calledTwice(removeStub); 282 assert.calledWith(removeStub.firstCall, "filename", { 283 ignoreAbsent: true, 284 }); 285 assert.calledWith(removeStub.secondCall, "personality-provider", { 286 ignoreAbsent: true, 287 }); 288 }); 289 }); 290 describe("#getAttachment", () => { 291 it("should return JSON when calling getAttachment", async () => { 292 sinon.stub(instance, "maybeDownloadAttachment").resolves(); 293 const readJSONStub = globals.sandbox 294 .stub(global.IOUtils, "readJSON") 295 .resolves({}); 296 const record = { attachment: { filename: "filename" } }; 297 let returnValue = await instance.getAttachment(record); 298 299 assert.calledOnce(readJSONStub); 300 assert.calledWith(readJSONStub, "filename"); 301 assert.calledOnce(instance.maybeDownloadAttachment); 302 assert.calledWith(instance.maybeDownloadAttachment, record); 303 assert.deepEqual(returnValue, {}); 304 305 readJSONStub.restore(); 306 globals.sandbox.stub(global.IOUtils, "readJSON").throws("foo"); 307 const consoleErrorStub = globals.sandbox 308 .stub(console, "error") 309 .resolves(); 310 returnValue = await instance.getAttachment(record); 311 312 assert.calledOnce(consoleErrorStub); 313 assert.deepEqual(returnValue, {}); 314 }); 315 }); 316 describe("#fetchModels", () => { 317 it("should return ok true", async () => { 318 sinon.stub(instance, "getAttachment").resolves(); 319 const result = await instance.fetchModels([{ key: 1234 }]); 320 assert.isTrue(result.ok); 321 assert.deepEqual(instance.models, [{ recordKey: 1234 }]); 322 }); 323 it("should return ok false", async () => { 324 sinon.stub(instance, "getAttachment").resolves(); 325 const result = await instance.fetchModels([]); 326 assert.isTrue(!result.ok); 327 }); 328 }); 329 describe("#generateTaggers", () => { 330 it("should generate taggers from modelKeys", () => { 331 const modelKeys = ["nb_model_sports", "nmf_model_sports"]; 332 333 instance.models = [ 334 { recordKey: "nb_model_sports", model_type: "nb" }, 335 { 336 recordKey: "nmf_model_sports", 337 model_type: "nmf", 338 parent_tag: "nmf_sports_parent_tag", 339 }, 340 ]; 341 342 instance.generateTaggers(modelKeys); 343 assert.equal(instance.taggers.nbTaggers.length, 1); 344 assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 1); 345 }); 346 it("should skip any models not in modelKeys", () => { 347 const modelKeys = ["nb_model_sports"]; 348 349 instance.models = [ 350 { recordKey: "nb_model_sports", model_type: "nb" }, 351 { 352 recordKey: "nmf_model_sports", 353 model_type: "nmf", 354 parent_tag: "nmf_sports_parent_tag", 355 }, 356 ]; 357 358 instance.generateTaggers(modelKeys); 359 assert.equal(instance.taggers.nbTaggers.length, 1); 360 assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0); 361 }); 362 it("should skip any models not defined", () => { 363 const modelKeys = ["nb_model_sports", "nmf_model_sports"]; 364 365 instance.models = [{ recordKey: "nb_model_sports", model_type: "nb" }]; 366 instance.generateTaggers(modelKeys); 367 assert.equal(instance.taggers.nbTaggers.length, 1); 368 assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0); 369 }); 370 }); 371 describe("#generateRecipeExecutor", () => { 372 it("should generate a recipeExecutor", () => { 373 instance.recipeExecutor = null; 374 instance.taggers = {}; 375 instance.generateRecipeExecutor(); 376 assert.isNotNull(instance.recipeExecutor); 377 }); 378 }); 379 describe("#createInterestVector", () => { 380 let mockHistory = []; 381 beforeEach(() => { 382 mockHistory = [ 383 { 384 title: "automotive", 385 description: "something about automotive", 386 url: "http://example.com/automotive", 387 frecency: 10, 388 }, 389 { 390 title: "fashion", 391 description: "something about fashion", 392 url: "http://example.com/fashion", 393 frecency: 5, 394 }, 395 { 396 title: "tech", 397 description: "something about tech", 398 url: "http://example.com/tech", 399 frecency: 1, 400 }, 401 ]; 402 }); 403 it("should gracefully handle history entries that fail", () => { 404 mockHistory.push({ title: "fail" }); 405 assert.isNotNull(instance.createInterestVector(mockHistory)); 406 }); 407 408 it("should fail if the combiner fails", () => { 409 mockHistory.push({ title: "combiner_fail", frecency: 111 }); 410 let actual = instance.createInterestVector(mockHistory); 411 assert.isNull(actual); 412 }); 413 414 it("should process history, combine, and finalize", () => { 415 let actual = instance.createInterestVector(mockHistory); 416 assert.equal(actual.interestVector.score, 1600); 417 }); 418 }); 419 describe("#calculateItemRelevanceScore", () => { 420 it("should return null for busted item", () => { 421 assert.equal( 422 instance.calculateItemRelevanceScore({ title: "fail" }), 423 null 424 ); 425 }); 426 it("should return null for a busted ranking", () => { 427 instance.interestVector = { title: "fail", score: 10 }; 428 assert.equal( 429 instance.calculateItemRelevanceScore({ title: "some item", score: 6 }), 430 null 431 ); 432 }); 433 it("should return a score, and not change with interestVector", () => { 434 instance.interestVector = { score: 10 }; 435 assert.equal( 436 instance.calculateItemRelevanceScore({ score: 2 }).rankingVector.score, 437 20 438 ); 439 assert.deepEqual(instance.interestVector, { score: 10 }); 440 }); 441 it("should use defined personalization_models if available", () => { 442 instance.interestVector = { score: 10 }; 443 const item = { 444 score: 2, 445 personalization_models: { 446 entertainment: 1, 447 }, 448 }; 449 assert.equal( 450 instance.calculateItemRelevanceScore(item).scorableItem.item_tags 451 .entertainment, 452 1 453 ); 454 }); 455 }); 456 });