tor-browser

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

RecipeExecutor.test.js (50057B)


      1 import { RecipeExecutor } from "lib/PersonalityProvider/RecipeExecutor.mjs";
      2 import { tokenize } from "lib/PersonalityProvider/Tokenize.mjs";
      3 
      4 class MockTagger {
      5  constructor(mode, tagScoreMap) {
      6    this.mode = mode;
      7    this.tagScoreMap = tagScoreMap;
      8  }
      9  tagTokens() {
     10    if (this.mode === "nb") {
     11      // eslint-disable-next-line prefer-destructuring
     12      let tag = Object.keys(this.tagScoreMap)[0];
     13      // eslint-disable-next-line prefer-destructuring
     14      let prob = this.tagScoreMap[tag];
     15      let conf = prob >= 0.85;
     16      return {
     17        label: tag,
     18        logProb: Math.log(prob),
     19        confident: conf,
     20      };
     21    }
     22    return this.tagScoreMap;
     23  }
     24  tag(text) {
     25    return this.tagTokens([text]);
     26  }
     27 }
     28 
     29 describe("RecipeExecutor", () => {
     30  let makeItem = () => {
     31    let x = {
     32      lhs: 2,
     33      one: 1,
     34      two: 2,
     35      three: 3,
     36      foo: "FOO",
     37      bar: "BAR",
     38      baz: ["one", "two", "three"],
     39      qux: 42,
     40      text: "This Is A_sentence.",
     41      url: "http://www.wonder.example.com/dir1/dir2a-dir2b/dir3+4?key1&key2=val2&key3&%26amp=%3D3+4",
     42      url2: "http://wonder.example.com/dir1/dir2a-dir2b/dir3+4?key1&key2=val2&key3&%26amp=%3D3+4",
     43      map: {
     44        c: 3,
     45        a: 1,
     46        b: 2,
     47      },
     48      map2: {
     49        b: 2,
     50        c: 3,
     51        d: 4,
     52      },
     53      arr1: [2, 3, 4],
     54      arr2: [3, 4, 5],
     55      long: [3, 4, 5, 6, 7],
     56      tags: {
     57        a: {
     58          aa: 0.1,
     59          ab: 0.2,
     60          ac: 0.3,
     61        },
     62        b: {
     63          ba: 4,
     64          bb: 5,
     65          bc: 6,
     66        },
     67      },
     68      bogus: {
     69        a: {
     70          aa: "0.1",
     71          ab: "0.2",
     72          ac: "0.3",
     73        },
     74        b: {
     75          ba: "4",
     76          bb: "5",
     77          bc: "6",
     78        },
     79      },
     80      zero: {
     81        a: 0,
     82        b: 0,
     83      },
     84      zaro: [0, 0],
     85    };
     86    return x;
     87  };
     88 
     89  let EPSILON = 0.00001;
     90 
     91  let instance = new RecipeExecutor(
     92    [
     93      new MockTagger("nb", { tag1: 0.7 }),
     94      new MockTagger("nb", { tag2: 0.86 }),
     95      new MockTagger("nb", { tag3: 0.9 }),
     96      new MockTagger("nb", { tag5: 0.9 }),
     97    ],
     98    {
     99      tag1: new MockTagger("nmf", {
    100        tag11: 0.9,
    101        tag12: 0.8,
    102        tag13: 0.7,
    103      }),
    104      tag2: new MockTagger("nmf", {
    105        tag21: 0.8,
    106        tag22: 0.7,
    107        tag23: 0.6,
    108      }),
    109      tag3: new MockTagger("nmf", {
    110        tag31: 0.7,
    111        tag32: 0.6,
    112        tag33: 0.5,
    113      }),
    114      tag4: new MockTagger("nmf", { tag41: 0.99 }),
    115    },
    116    tokenize
    117  );
    118  let item = null;
    119 
    120  beforeEach(() => {
    121    item = makeItem();
    122  });
    123 
    124  describe("#_assembleText", () => {
    125    it("should simply copy a single string", () => {
    126      assert.equal(instance._assembleText(item, ["foo"]), "FOO");
    127    });
    128    it("should append some strings with a space", () => {
    129      assert.equal(instance._assembleText(item, ["foo", "bar"]), "FOO BAR");
    130    });
    131    it("should give an empty string for a missing field", () => {
    132      assert.equal(instance._assembleText(item, ["missing"]), "");
    133    });
    134    it("should not double space an interior missing field", () => {
    135      assert.equal(
    136        instance._assembleText(item, ["foo", "missing", "bar"]),
    137        "FOO BAR"
    138      );
    139    });
    140    it("should splice in an array of strings", () => {
    141      assert.equal(
    142        instance._assembleText(item, ["foo", "baz", "bar"]),
    143        "FOO one two three BAR"
    144      );
    145    });
    146    it("should handle numbers", () => {
    147      assert.equal(
    148        instance._assembleText(item, ["foo", "qux", "bar"]),
    149        "FOO 42 BAR"
    150      );
    151    });
    152  });
    153 
    154  describe("#naiveBayesTag", () => {
    155    it("should understand NaiveBayesTextTagger", () => {
    156      item = instance.naiveBayesTag(item, { fields: ["text"] });
    157      assert.isTrue("nb_tags" in item);
    158      assert.isTrue(!("tag1" in item.nb_tags));
    159      assert.equal(item.nb_tags.tag2, 0.86);
    160      assert.equal(item.nb_tags.tag3, 0.9);
    161      assert.equal(item.nb_tags.tag5, 0.9);
    162      assert.isTrue("nb_tokens" in item);
    163      assert.deepEqual(item.nb_tokens, ["this", "is", "a", "sentence"]);
    164      assert.isTrue("nb_tags_extended" in item);
    165      assert.isTrue(!("tag1" in item.nb_tags_extended));
    166      assert.deepEqual(item.nb_tags_extended.tag2, {
    167        label: "tag2",
    168        logProb: Math.log(0.86),
    169        confident: true,
    170      });
    171      assert.deepEqual(item.nb_tags_extended.tag3, {
    172        label: "tag3",
    173        logProb: Math.log(0.9),
    174        confident: true,
    175      });
    176      assert.deepEqual(item.nb_tags_extended.tag5, {
    177        label: "tag5",
    178        logProb: Math.log(0.9),
    179        confident: true,
    180      });
    181      assert.isTrue("nb_tokens" in item);
    182      assert.deepEqual(item.nb_tokens, ["this", "is", "a", "sentence"]);
    183    });
    184  });
    185 
    186  describe("#conditionallyNmfTag", () => {
    187    it("should do nothing if it's not nb tagged", () => {
    188      item = instance.conditionallyNmfTag(item, {});
    189      assert.equal(item, null);
    190    });
    191    it("should populate nmf tags for the nb tags", () => {
    192      item = instance.naiveBayesTag(item, { fields: ["text"] });
    193      item = instance.conditionallyNmfTag(item, {});
    194      assert.isTrue("nb_tags" in item);
    195      assert.deepEqual(item.nmf_tags, {
    196        tag2: {
    197          tag21: 0.8,
    198          tag22: 0.7,
    199          tag23: 0.6,
    200        },
    201        tag3: {
    202          tag31: 0.7,
    203          tag32: 0.6,
    204          tag33: 0.5,
    205        },
    206      });
    207      assert.deepEqual(item.nmf_tags_parent, {
    208        tag21: "tag2",
    209        tag22: "tag2",
    210        tag23: "tag2",
    211        tag31: "tag3",
    212        tag32: "tag3",
    213        tag33: "tag3",
    214      });
    215    });
    216    it("should not populate nmf tags for things that were not nb tagged", () => {
    217      item = instance.naiveBayesTag(item, { fields: ["text"] });
    218      item = instance.conditionallyNmfTag(item, {});
    219      assert.isTrue("nmf_tags" in item);
    220      assert.isTrue(!("tag4" in item.nmf_tags));
    221      assert.isTrue("nmf_tags_parent" in item);
    222      assert.isTrue(!("tag4" in item.nmf_tags_parent));
    223    });
    224  });
    225 
    226  describe("#acceptItemByFieldValue", () => {
    227    it("should implement ==", () => {
    228      assert.isTrue(
    229        instance.acceptItemByFieldValue(item, {
    230          field: "lhs",
    231          op: "==",
    232          rhsValue: 2,
    233        }) !== null
    234      );
    235      assert.isTrue(
    236        instance.acceptItemByFieldValue(item, {
    237          field: "lhs",
    238          op: "==",
    239          rhsValue: 3,
    240        }) === null
    241      );
    242      assert.isTrue(
    243        instance.acceptItemByFieldValue(item, {
    244          field: "lhs",
    245          op: "==",
    246          rhsField: "two",
    247        }) !== null
    248      );
    249      assert.isTrue(
    250        instance.acceptItemByFieldValue(item, {
    251          field: "lhs",
    252          op: "==",
    253          rhsField: "three",
    254        }) === null
    255      );
    256    });
    257    it("should implement !=", () => {
    258      assert.isTrue(
    259        instance.acceptItemByFieldValue(item, {
    260          field: "lhs",
    261          op: "!=",
    262          rhsValue: 2,
    263        }) === null
    264      );
    265      assert.isTrue(
    266        instance.acceptItemByFieldValue(item, {
    267          field: "lhs",
    268          op: "!=",
    269          rhsValue: 3,
    270        }) !== null
    271      );
    272    });
    273    it("should implement < ", () => {
    274      assert.isTrue(
    275        instance.acceptItemByFieldValue(item, {
    276          field: "lhs",
    277          op: "<",
    278          rhsValue: 1,
    279        }) === null
    280      );
    281      assert.isTrue(
    282        instance.acceptItemByFieldValue(item, {
    283          field: "lhs",
    284          op: "<",
    285          rhsValue: 2,
    286        }) === null
    287      );
    288      assert.isTrue(
    289        instance.acceptItemByFieldValue(item, {
    290          field: "lhs",
    291          op: "<",
    292          rhsValue: 3,
    293        }) !== null
    294      );
    295    });
    296    it("should implement <= ", () => {
    297      assert.isTrue(
    298        instance.acceptItemByFieldValue(item, {
    299          field: "lhs",
    300          op: "<=",
    301          rhsValue: 1,
    302        }) === null
    303      );
    304      assert.isTrue(
    305        instance.acceptItemByFieldValue(item, {
    306          field: "lhs",
    307          op: "<=",
    308          rhsValue: 2,
    309        }) !== null
    310      );
    311      assert.isTrue(
    312        instance.acceptItemByFieldValue(item, {
    313          field: "lhs",
    314          op: "<=",
    315          rhsValue: 3,
    316        }) !== null
    317      );
    318    });
    319    it("should implement > ", () => {
    320      assert.isTrue(
    321        instance.acceptItemByFieldValue(item, {
    322          field: "lhs",
    323          op: ">",
    324          rhsValue: 1,
    325        }) !== null
    326      );
    327      assert.isTrue(
    328        instance.acceptItemByFieldValue(item, {
    329          field: "lhs",
    330          op: ">",
    331          rhsValue: 2,
    332        }) === null
    333      );
    334      assert.isTrue(
    335        instance.acceptItemByFieldValue(item, {
    336          field: "lhs",
    337          op: ">",
    338          rhsValue: 3,
    339        }) === null
    340      );
    341    });
    342    it("should implement >= ", () => {
    343      assert.isTrue(
    344        instance.acceptItemByFieldValue(item, {
    345          field: "lhs",
    346          op: ">=",
    347          rhsValue: 1,
    348        }) !== null
    349      );
    350      assert.isTrue(
    351        instance.acceptItemByFieldValue(item, {
    352          field: "lhs",
    353          op: ">=",
    354          rhsValue: 2,
    355        }) !== null
    356      );
    357      assert.isTrue(
    358        instance.acceptItemByFieldValue(item, {
    359          field: "lhs",
    360          op: ">=",
    361          rhsValue: 3,
    362        }) === null
    363      );
    364    });
    365    it("should skip items with missing fields", () => {
    366      assert.isTrue(
    367        instance.acceptItemByFieldValue(item, {
    368          field: "no-left",
    369          op: "==",
    370          rhsValue: 1,
    371        }) === null
    372      );
    373      assert.isTrue(
    374        instance.acceptItemByFieldValue(item, {
    375          field: "lhs",
    376          op: "==",
    377          rhsField: "no-right",
    378        }) === null
    379      );
    380      assert.isTrue(
    381        instance.acceptItemByFieldValue(item, { field: "lhs", op: "==" }) ===
    382          null
    383      );
    384    });
    385    it("should skip items with bogus operators", () => {
    386      assert.isTrue(
    387        instance.acceptItemByFieldValue(item, {
    388          field: "lhs",
    389          op: "bogus",
    390          rhsField: "two",
    391        }) === null
    392      );
    393    });
    394  });
    395 
    396  describe("#tokenizeUrl", () => {
    397    it("should strip the leading www from a url", () => {
    398      item = instance.tokenizeUrl(item, { field: "url", dest: "url_toks" });
    399      assert.deepEqual(
    400        [
    401          "wonder",
    402          "example",
    403          "com",
    404          "dir1",
    405          "dir2a",
    406          "dir2b",
    407          "dir3",
    408          "4",
    409          "key1",
    410          "key2",
    411          "val2",
    412          "key3",
    413          "amp",
    414          "3",
    415          "4",
    416        ],
    417        item.url_toks
    418      );
    419    });
    420    it("should tokenize the not strip the leading non-wwww token from a url", () => {
    421      item = instance.tokenizeUrl(item, { field: "url2", dest: "url_toks" });
    422      assert.deepEqual(
    423        [
    424          "wonder",
    425          "example",
    426          "com",
    427          "dir1",
    428          "dir2a",
    429          "dir2b",
    430          "dir3",
    431          "4",
    432          "key1",
    433          "key2",
    434          "val2",
    435          "key3",
    436          "amp",
    437          "3",
    438          "4",
    439        ],
    440        item.url_toks
    441      );
    442    });
    443    it("should error for a missing url", () => {
    444      item = instance.tokenizeUrl(item, { field: "missing", dest: "url_toks" });
    445      assert.equal(item, null);
    446    });
    447  });
    448 
    449  describe("#getUrlDomain", () => {
    450    it("should get only the hostname skipping the www", () => {
    451      item = instance.getUrlDomain(item, { field: "url", dest: "url_domain" });
    452      assert.isTrue("url_domain" in item);
    453      assert.deepEqual("wonder.example.com", item.url_domain);
    454    });
    455    it("should get only the hostname", () => {
    456      item = instance.getUrlDomain(item, { field: "url2", dest: "url_domain" });
    457      assert.isTrue("url_domain" in item);
    458      assert.deepEqual("wonder.example.com", item.url_domain);
    459    });
    460    it("should get the hostname and 2 levels of directories", () => {
    461      item = instance.getUrlDomain(item, {
    462        field: "url",
    463        path_length: 2,
    464        dest: "url_plus_2",
    465      });
    466      assert.isTrue("url_plus_2" in item);
    467      assert.deepEqual("wonder.example.com/dir1/dir2a-dir2b", item.url_plus_2);
    468    });
    469    it("should error for a missing url", () => {
    470      item = instance.getUrlDomain(item, {
    471        field: "missing",
    472        dest: "url_domain",
    473      });
    474      assert.equal(item, null);
    475    });
    476  });
    477 
    478  describe("#tokenizeField", () => {
    479    it("should tokenize the field", () => {
    480      item = instance.tokenizeField(item, { field: "text", dest: "toks" });
    481      assert.isTrue("toks" in item);
    482      assert.deepEqual(["this", "is", "a", "sentence"], item.toks);
    483    });
    484    it("should error for a missing field", () => {
    485      item = instance.tokenizeField(item, { field: "missing", dest: "toks" });
    486      assert.equal(item, null);
    487    });
    488    it("should error for a broken config", () => {
    489      item = instance.tokenizeField(item, {});
    490      assert.equal(item, null);
    491    });
    492  });
    493 
    494  describe("#_typeOf", () => {
    495    it("should know this is a map", () => {
    496      assert.equal(instance._typeOf({}), "map");
    497    });
    498    it("should know this is an array", () => {
    499      assert.equal(instance._typeOf([]), "array");
    500    });
    501    it("should know this is a string", () => {
    502      assert.equal(instance._typeOf("blah"), "string");
    503    });
    504    it("should know this is a boolean", () => {
    505      assert.equal(instance._typeOf(true), "boolean");
    506    });
    507 
    508    it("should know this is a null", () => {
    509      assert.equal(instance._typeOf(null), "null");
    510    });
    511  });
    512 
    513  describe("#_lookupScalar", () => {
    514    it("should return the constant", () => {
    515      assert.equal(instance._lookupScalar({}, 1, 0), 1);
    516    });
    517    it("should return the default", () => {
    518      assert.equal(instance._lookupScalar({}, "blah", 42), 42);
    519    });
    520    it("should return the field's value", () => {
    521      assert.equal(instance._lookupScalar({ blah: 11 }, "blah", 42), 11);
    522    });
    523  });
    524 
    525  describe("#copyValue", () => {
    526    it("should copy values", () => {
    527      item = instance.copyValue(item, { src: "one", dest: "again" });
    528      assert.isTrue("again" in item);
    529      assert.equal(item.again, 1);
    530      item.one = 100;
    531      assert.equal(item.one, 100);
    532      assert.equal(item.again, 1);
    533    });
    534    it("should handle maps corrects", () => {
    535      item = instance.copyValue(item, { src: "map", dest: "again" });
    536      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });
    537      item.map.c = 100;
    538      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });
    539      item.map = 342;
    540      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });
    541    });
    542    it("should error for a missing field", () => {
    543      item = instance.copyValue(item, { src: "missing", dest: "toks" });
    544      assert.equal(item, null);
    545    });
    546  });
    547 
    548  describe("#keepTopK", () => {
    549    it("should keep the 2 smallest", () => {
    550      item = instance.keepTopK(item, { field: "map", k: 2, descending: false });
    551      assert.equal(Object.keys(item.map).length, 2);
    552      assert.isTrue("a" in item.map);
    553      assert.equal(item.map.a, 1);
    554      assert.isTrue("b" in item.map);
    555      assert.equal(item.map.b, 2);
    556      assert.isTrue(!("c" in item.map));
    557    });
    558    it("should keep the 2 largest", () => {
    559      item = instance.keepTopK(item, { field: "map", k: 2, descending: true });
    560      assert.equal(Object.keys(item.map).length, 2);
    561      assert.isTrue(!("a" in item.map));
    562      assert.isTrue("b" in item.map);
    563      assert.equal(item.map.b, 2);
    564      assert.isTrue("c" in item.map);
    565      assert.equal(item.map.c, 3);
    566    });
    567    it("should still keep the 2 largest", () => {
    568      item = instance.keepTopK(item, { field: "map", k: 2 });
    569      assert.equal(Object.keys(item.map).length, 2);
    570      assert.isTrue(!("a" in item.map));
    571      assert.isTrue("b" in item.map);
    572      assert.equal(item.map.b, 2);
    573      assert.isTrue("c" in item.map);
    574      assert.equal(item.map.c, 3);
    575    });
    576    it("should promote up nested fields", () => {
    577      item = instance.keepTopK(item, { field: "tags", k: 2 });
    578      assert.equal(Object.keys(item.tags).length, 2);
    579      assert.deepEqual(item.tags, { bb: 5, bc: 6 });
    580    });
    581    it("should error for a missing field", () => {
    582      item = instance.keepTopK(item, { field: "missing", k: 3 });
    583      assert.equal(item, null);
    584    });
    585  });
    586 
    587  describe("#scalarMultiply", () => {
    588    it("should use constants", () => {
    589      item = instance.scalarMultiply(item, { field: "map", k: 2 });
    590      assert.equal(item.map.a, 2);
    591      assert.equal(item.map.b, 4);
    592      assert.equal(item.map.c, 6);
    593    });
    594    it("should use fields", () => {
    595      item = instance.scalarMultiply(item, { field: "map", k: "three" });
    596      assert.equal(item.map.a, 3);
    597      assert.equal(item.map.b, 6);
    598      assert.equal(item.map.c, 9);
    599    });
    600    it("should use default", () => {
    601      item = instance.scalarMultiply(item, {
    602        field: "map",
    603        k: "missing",
    604        dfault: 4,
    605      });
    606      assert.equal(item.map.a, 4);
    607      assert.equal(item.map.b, 8);
    608      assert.equal(item.map.c, 12);
    609    });
    610    it("should error for a missing field", () => {
    611      item = instance.scalarMultiply(item, { field: "missing", k: 3 });
    612      assert.equal(item, null);
    613    });
    614    it("should multiply numbers", () => {
    615      item = instance.scalarMultiply(item, { field: "lhs", k: 2 });
    616      assert.equal(item.lhs, 4);
    617    });
    618    it("should multiply arrays", () => {
    619      item = instance.scalarMultiply(item, { field: "arr1", k: 2 });
    620      assert.deepEqual(item.arr1, [4, 6, 8]);
    621    });
    622    it("should should error on strings", () => {
    623      item = instance.scalarMultiply(item, { field: "foo", k: 2 });
    624      assert.equal(item, null);
    625    });
    626  });
    627 
    628  describe("#elementwiseMultiply", () => {
    629    it("should handle maps", () => {
    630      item = instance.elementwiseMultiply(item, {
    631        left: "tags",
    632        right: "map2",
    633      });
    634      assert.deepEqual(item.tags, {
    635        a: { aa: 0, ab: 0, ac: 0 },
    636        b: { ba: 8, bb: 10, bc: 12 },
    637      });
    638    });
    639    it("should handle arrays of same length", () => {
    640      item = instance.elementwiseMultiply(item, {
    641        left: "arr1",
    642        right: "arr2",
    643      });
    644      assert.deepEqual(item.arr1, [6, 12, 20]);
    645    });
    646    it("should error for arrays of different lengths", () => {
    647      item = instance.elementwiseMultiply(item, {
    648        left: "arr1",
    649        right: "long",
    650      });
    651      assert.equal(item, null);
    652    });
    653    it("should error for a missing left", () => {
    654      item = instance.elementwiseMultiply(item, {
    655        left: "missing",
    656        right: "arr2",
    657      });
    658      assert.equal(item, null);
    659    });
    660    it("should error for a missing right", () => {
    661      item = instance.elementwiseMultiply(item, {
    662        left: "arr1",
    663        right: "missing",
    664      });
    665      assert.equal(item, null);
    666    });
    667    it("should handle numbers", () => {
    668      item = instance.elementwiseMultiply(item, {
    669        left: "three",
    670        right: "two",
    671      });
    672      assert.equal(item.three, 6);
    673    });
    674    it("should error for mismatched types", () => {
    675      item = instance.elementwiseMultiply(item, { left: "arr1", right: "two" });
    676      assert.equal(item, null);
    677    });
    678    it("should error for strings", () => {
    679      item = instance.elementwiseMultiply(item, { left: "foo", right: "bar" });
    680      assert.equal(item, null);
    681    });
    682  });
    683 
    684  describe("#vectorMultiply", () => {
    685    it("should calculate dot products from maps", () => {
    686      item = instance.vectorMultiply(item, {
    687        left: "map",
    688        right: "map2",
    689        dest: "dot",
    690      });
    691      assert.equal(item.dot, 13);
    692    });
    693    it("should calculate dot products from arrays", () => {
    694      item = instance.vectorMultiply(item, {
    695        left: "arr1",
    696        right: "arr2",
    697        dest: "dot",
    698      });
    699      assert.equal(item.dot, 38);
    700    });
    701    it("should error for arrays of different lengths", () => {
    702      item = instance.vectorMultiply(item, { left: "arr1", right: "long" });
    703      assert.equal(item, null);
    704    });
    705    it("should error for a missing left", () => {
    706      item = instance.vectorMultiply(item, { left: "missing", right: "arr2" });
    707      assert.equal(item, null);
    708    });
    709    it("should error for a missing right", () => {
    710      item = instance.vectorMultiply(item, { left: "arr1", right: "missing" });
    711      assert.equal(item, null);
    712    });
    713    it("should error for mismatched types", () => {
    714      item = instance.vectorMultiply(item, { left: "arr1", right: "two" });
    715      assert.equal(item, null);
    716    });
    717    it("should error for strings", () => {
    718      item = instance.vectorMultiply(item, { left: "foo", right: "bar" });
    719      assert.equal(item, null);
    720    });
    721  });
    722 
    723  describe("#scalarAdd", () => {
    724    it("should error for a missing field", () => {
    725      item = instance.scalarAdd(item, { field: "missing", k: 10 });
    726      assert.equal(item, null);
    727    });
    728    it("should error for strings", () => {
    729      item = instance.scalarAdd(item, { field: "foo", k: 10 });
    730      assert.equal(item, null);
    731    });
    732    it("should work for numbers", () => {
    733      item = instance.scalarAdd(item, { field: "one", k: 10 });
    734      assert.equal(item.one, 11);
    735    });
    736    it("should add a constant to every cell on a map", () => {
    737      item = instance.scalarAdd(item, { field: "map", k: 10 });
    738      assert.deepEqual(item.map, { a: 11, b: 12, c: 13 });
    739    });
    740    it("should add a value from a field to every cell on a map", () => {
    741      item = instance.scalarAdd(item, { field: "map", k: "qux" });
    742      assert.deepEqual(item.map, { a: 43, b: 44, c: 45 });
    743    });
    744    it("should add a constant to every cell on an array", () => {
    745      item = instance.scalarAdd(item, { field: "arr1", k: 10 });
    746      assert.deepEqual(item.arr1, [12, 13, 14]);
    747    });
    748  });
    749 
    750  describe("#vectorAdd", () => {
    751    it("should calculate add vectors from maps", () => {
    752      item = instance.vectorAdd(item, { left: "map", right: "map2" });
    753      assert.equal(Object.keys(item.map).length, 4);
    754      assert.isTrue("a" in item.map);
    755      assert.equal(item.map.a, 1);
    756      assert.isTrue("b" in item.map);
    757      assert.equal(item.map.b, 4);
    758      assert.isTrue("c" in item.map);
    759      assert.equal(item.map.c, 6);
    760      assert.isTrue("d" in item.map);
    761      assert.equal(item.map.d, 4);
    762    });
    763    it("should work for missing left", () => {
    764      item = instance.vectorAdd(item, { left: "missing", right: "arr2" });
    765      assert.deepEqual(item.missing, [3, 4, 5]);
    766    });
    767    it("should error for missing right", () => {
    768      item = instance.vectorAdd(item, { left: "arr2", right: "missing" });
    769      assert.equal(item, null);
    770    });
    771    it("should error error for strings", () => {
    772      item = instance.vectorAdd(item, { left: "foo", right: "bar" });
    773      assert.equal(item, null);
    774    });
    775    it("should error for different types", () => {
    776      item = instance.vectorAdd(item, { left: "arr2", right: "map" });
    777      assert.equal(item, null);
    778    });
    779    it("should calculate add vectors from arrays", () => {
    780      item = instance.vectorAdd(item, { left: "arr1", right: "arr2" });
    781      assert.deepEqual(item.arr1, [5, 7, 9]);
    782    });
    783    it("should abort on different sized arrays", () => {
    784      item = instance.vectorAdd(item, { left: "arr1", right: "long" });
    785      assert.equal(item, null);
    786    });
    787    it("should calculate add vectors from arrays", () => {
    788      item = instance.vectorAdd(item, { left: "arr1", right: "arr2" });
    789      assert.deepEqual(item.arr1, [5, 7, 9]);
    790    });
    791  });
    792 
    793  describe("#makeBoolean", () => {
    794    it("should error for missing field", () => {
    795      item = instance.makeBoolean(item, { field: "missing", threshold: 2 });
    796      assert.equal(item, null);
    797    });
    798    it("should 0/1 a map", () => {
    799      item = instance.makeBoolean(item, { field: "map", threshold: 2 });
    800      assert.deepEqual(item.map, { a: 0, b: 0, c: 1 });
    801    });
    802    it("should a map of all 1s", () => {
    803      item = instance.makeBoolean(item, { field: "map" });
    804      assert.deepEqual(item.map, { a: 1, b: 1, c: 1 });
    805    });
    806    it("should -1/1 a map", () => {
    807      item = instance.makeBoolean(item, {
    808        field: "map",
    809        threshold: 2,
    810        keep_negative: true,
    811      });
    812      assert.deepEqual(item.map, { a: -1, b: -1, c: 1 });
    813    });
    814    it("should work an array", () => {
    815      item = instance.makeBoolean(item, { field: "arr1", threshold: 3 });
    816      assert.deepEqual(item.arr1, [0, 0, 1]);
    817    });
    818    it("should -1/1 an array", () => {
    819      item = instance.makeBoolean(item, {
    820        field: "arr1",
    821        threshold: 3,
    822        keep_negative: true,
    823      });
    824      assert.deepEqual(item.arr1, [-1, -1, 1]);
    825    });
    826    it("should 1 a high number", () => {
    827      item = instance.makeBoolean(item, { field: "qux", threshold: 3 });
    828      assert.equal(item.qux, 1);
    829    });
    830    it("should 0 a low number", () => {
    831      item = instance.makeBoolean(item, { field: "qux", threshold: 70 });
    832      assert.equal(item.qux, 0);
    833    });
    834    it("should -1 a low number", () => {
    835      item = instance.makeBoolean(item, {
    836        field: "qux",
    837        threshold: 83,
    838        keep_negative: true,
    839      });
    840      assert.equal(item.qux, -1);
    841    });
    842    it("should fail a string", () => {
    843      item = instance.makeBoolean(item, { field: "foo", threshold: 3 });
    844      assert.equal(item, null);
    845    });
    846  });
    847 
    848  describe("#allowFields", () => {
    849    it("should filter the keys out of a map", () => {
    850      item = instance.allowFields(item, {
    851        fields: ["foo", "missing", "bar"],
    852      });
    853      assert.deepEqual(item, { foo: "FOO", bar: "BAR" });
    854    });
    855  });
    856 
    857  describe("#filterByValue", () => {
    858    it("should fail on missing field", () => {
    859      item = instance.filterByValue(item, { field: "missing", threshold: 2 });
    860      assert.equal(item, null);
    861    });
    862    it("should filter the keys out of a map", () => {
    863      item = instance.filterByValue(item, { field: "map", threshold: 2 });
    864      assert.deepEqual(item.map, { c: 3 });
    865    });
    866  });
    867 
    868  describe("#l2Normalize", () => {
    869    it("should fail on missing field", () => {
    870      item = instance.l2Normalize(item, { field: "missing" });
    871      assert.equal(item, null);
    872    });
    873    it("should L2 normalize an array", () => {
    874      item = instance.l2Normalize(item, { field: "arr1" });
    875      assert.deepEqual(
    876        item.arr1,
    877        [0.3713906763541037, 0.5570860145311556, 0.7427813527082074]
    878      );
    879    });
    880    it("should L2 normalize a map", () => {
    881      item = instance.l2Normalize(item, { field: "map" });
    882      assert.deepEqual(item.map, {
    883        a: 0.2672612419124244,
    884        b: 0.5345224838248488,
    885        c: 0.8017837257372732,
    886      });
    887    });
    888    it("should fail a string", () => {
    889      item = instance.l2Normalize(item, { field: "foo" });
    890      assert.equal(item, null);
    891    });
    892    it("should not bomb on a zero vector", () => {
    893      item = instance.l2Normalize(item, { field: "zero" });
    894      assert.deepEqual(item.zero, { a: 0, b: 0 });
    895      item = instance.l2Normalize(item, { field: "zaro" });
    896      assert.deepEqual(item.zaro, [0, 0]);
    897    });
    898  });
    899 
    900  describe("#probNormalize", () => {
    901    it("should fail on missing field", () => {
    902      item = instance.probNormalize(item, { field: "missing" });
    903      assert.equal(item, null);
    904    });
    905    it("should normalize an array to sum to 1", () => {
    906      item = instance.probNormalize(item, { field: "arr1" });
    907      assert.deepEqual(
    908        item.arr1,
    909        [0.2222222222222222, 0.3333333333333333, 0.4444444444444444]
    910      );
    911    });
    912    it("should normalize a map to sum to 1", () => {
    913      item = instance.probNormalize(item, { field: "map" });
    914      assert.equal(Object.keys(item.map).length, 3);
    915      assert.isTrue("a" in item.map);
    916      assert.isTrue(Math.abs(item.map.a - 0.16667) <= EPSILON);
    917      assert.isTrue("b" in item.map);
    918      assert.isTrue(Math.abs(item.map.b - 0.33333) <= EPSILON);
    919      assert.isTrue("c" in item.map);
    920      assert.isTrue(Math.abs(item.map.c - 0.5) <= EPSILON);
    921    });
    922    it("should fail a string", () => {
    923      item = instance.probNormalize(item, { field: "foo" });
    924      assert.equal(item, null);
    925    });
    926    it("should not bomb on a zero vector", () => {
    927      item = instance.probNormalize(item, { field: "zero" });
    928      assert.deepEqual(item.zero, { a: 0, b: 0 });
    929      item = instance.probNormalize(item, { field: "zaro" });
    930      assert.deepEqual(item.zaro, [0, 0]);
    931    });
    932  });
    933 
    934  describe("#scalarMultiplyTag", () => {
    935    it("should fail on missing field", () => {
    936      item = instance.scalarMultiplyTag(item, { field: "missing", k: 3 });
    937      assert.equal(item, null);
    938    });
    939    it("should scalar multiply a nested map", () => {
    940      item = instance.scalarMultiplyTag(item, {
    941        field: "tags",
    942        k: 3,
    943        log_scale: false,
    944      });
    945      assert.isTrue(Math.abs(item.tags.a.aa - 0.3) <= EPSILON);
    946      assert.isTrue(Math.abs(item.tags.a.ab - 0.6) <= EPSILON);
    947      assert.isTrue(Math.abs(item.tags.a.ac - 0.9) <= EPSILON);
    948      assert.isTrue(Math.abs(item.tags.b.ba - 12) <= EPSILON);
    949      assert.isTrue(Math.abs(item.tags.b.bb - 15) <= EPSILON);
    950      assert.isTrue(Math.abs(item.tags.b.bc - 18) <= EPSILON);
    951    });
    952    it("should scalar multiply a nested map with logrithms", () => {
    953      item = instance.scalarMultiplyTag(item, {
    954        field: "tags",
    955        k: 3,
    956        log_scale: true,
    957      });
    958      assert.isTrue(
    959        Math.abs(item.tags.a.aa - Math.log(0.1 + 0.000001) * 3) <= EPSILON
    960      );
    961      assert.isTrue(
    962        Math.abs(item.tags.a.ab - Math.log(0.2 + 0.000001) * 3) <= EPSILON
    963      );
    964      assert.isTrue(
    965        Math.abs(item.tags.a.ac - Math.log(0.3 + 0.000001) * 3) <= EPSILON
    966      );
    967      assert.isTrue(
    968        Math.abs(item.tags.b.ba - Math.log(4.0 + 0.000001) * 3) <= EPSILON
    969      );
    970      assert.isTrue(
    971        Math.abs(item.tags.b.bb - Math.log(5.0 + 0.000001) * 3) <= EPSILON
    972      );
    973      assert.isTrue(
    974        Math.abs(item.tags.b.bc - Math.log(6.0 + 0.000001) * 3) <= EPSILON
    975      );
    976    });
    977    it("should fail a string", () => {
    978      item = instance.scalarMultiplyTag(item, { field: "foo", k: 3 });
    979      assert.equal(item, null);
    980    });
    981  });
    982 
    983  describe("#setDefault", () => {
    984    it("should store a missing value", () => {
    985      item = instance.setDefault(item, { field: "missing", value: 1111 });
    986      assert.equal(item.missing, 1111);
    987    });
    988    it("should not overwrite an existing value", () => {
    989      item = instance.setDefault(item, { field: "lhs", value: 1111 });
    990      assert.equal(item.lhs, 2);
    991    });
    992    it("should store a complex value", () => {
    993      item = instance.setDefault(item, { field: "missing", value: { a: 1 } });
    994      assert.deepEqual(item.missing, { a: 1 });
    995    });
    996  });
    997 
    998  describe("#lookupValue", () => {
    999    it("should promote a value", () => {
   1000      item = instance.lookupValue(item, {
   1001        haystack: "map",
   1002        needle: "c",
   1003        dest: "ccc",
   1004      });
   1005      assert.equal(item.ccc, 3);
   1006    });
   1007    it("should handle a missing haystack", () => {
   1008      item = instance.lookupValue(item, {
   1009        haystack: "missing",
   1010        needle: "c",
   1011        dest: "ccc",
   1012      });
   1013      assert.isTrue(!("ccc" in item));
   1014    });
   1015    it("should handle a missing needle", () => {
   1016      item = instance.lookupValue(item, {
   1017        haystack: "map",
   1018        needle: "missing",
   1019        dest: "ccc",
   1020      });
   1021      assert.isTrue(!("ccc" in item));
   1022    });
   1023  });
   1024 
   1025  describe("#copyToMap", () => {
   1026    it("should copy a value to a map", () => {
   1027      item = instance.copyToMap(item, {
   1028        src: "qux",
   1029        dest_map: "map",
   1030        dest_key: "zzz",
   1031      });
   1032      assert.isTrue("zzz" in item.map);
   1033      assert.equal(item.map.zzz, item.qux);
   1034    });
   1035    it("should create a new map to hold the key", () => {
   1036      item = instance.copyToMap(item, {
   1037        src: "qux",
   1038        dest_map: "missing",
   1039        dest_key: "zzz",
   1040      });
   1041      assert.equal(Object.keys(item.missing).length, 1);
   1042      assert.equal(item.missing.zzz, item.qux);
   1043    });
   1044    it("should not create an empty map if the src is missing", () => {
   1045      item = instance.copyToMap(item, {
   1046        src: "missing",
   1047        dest_map: "no_map",
   1048        dest_key: "zzz",
   1049      });
   1050      assert.isTrue(!("no_map" in item));
   1051    });
   1052  });
   1053 
   1054  describe("#applySoftmaxTags", () => {
   1055    it("should error on missing field", () => {
   1056      item = instance.applySoftmaxTags(item, { field: "missing" });
   1057      assert.equal(item, null);
   1058    });
   1059    it("should error on nonmaps", () => {
   1060      item = instance.applySoftmaxTags(item, { field: "arr1" });
   1061      assert.equal(item, null);
   1062    });
   1063    it("should error on unnested maps", () => {
   1064      item = instance.applySoftmaxTags(item, { field: "map" });
   1065      assert.equal(item, null);
   1066    });
   1067    it("should error on wrong nested maps", () => {
   1068      item = instance.applySoftmaxTags(item, { field: "bogus" });
   1069      assert.equal(item, null);
   1070    });
   1071    it("should apply softmax across the subtags", () => {
   1072      item = instance.applySoftmaxTags(item, { field: "tags" });
   1073      assert.isTrue("a" in item.tags);
   1074      assert.isTrue("aa" in item.tags.a);
   1075      assert.isTrue("ab" in item.tags.a);
   1076      assert.isTrue("ac" in item.tags.a);
   1077      assert.isTrue(Math.abs(item.tags.a.aa - 0.30061) <= EPSILON);
   1078      assert.isTrue(Math.abs(item.tags.a.ab - 0.33222) <= EPSILON);
   1079      assert.isTrue(Math.abs(item.tags.a.ac - 0.36717) <= EPSILON);
   1080 
   1081      assert.isTrue("b" in item.tags);
   1082      assert.isTrue("ba" in item.tags.b);
   1083      assert.isTrue("bb" in item.tags.b);
   1084      assert.isTrue("bc" in item.tags.b);
   1085      assert.isTrue(Math.abs(item.tags.b.ba - 0.09003) <= EPSILON);
   1086      assert.isTrue(Math.abs(item.tags.b.bb - 0.24473) <= EPSILON);
   1087      assert.isTrue(Math.abs(item.tags.b.bc - 0.66524) <= EPSILON);
   1088    });
   1089  });
   1090 
   1091  describe("#combinerAdd", () => {
   1092    it("should do nothing when right field is missing", () => {
   1093      let right = makeItem();
   1094      let combined = instance.combinerAdd(item, right, { field: "missing" });
   1095      assert.deepEqual(combined, item);
   1096    });
   1097    it("should handle missing left maps", () => {
   1098      let right = makeItem();
   1099      right.missingmap = { a: 5, b: -1, c: 3 };
   1100      let combined = instance.combinerAdd(item, right, { field: "missingmap" });
   1101      assert.deepEqual(combined.missingmap, { a: 5, b: -1, c: 3 });
   1102    });
   1103    it("should add equal sized maps", () => {
   1104      let right = makeItem();
   1105      let combined = instance.combinerAdd(item, right, { field: "map" });
   1106      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6 });
   1107    });
   1108    it("should add long map to short map", () => {
   1109      let right = makeItem();
   1110      right.map.d = 999;
   1111      let combined = instance.combinerAdd(item, right, { field: "map" });
   1112      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6, d: 999 });
   1113    });
   1114    it("should add short map to long map", () => {
   1115      let right = makeItem();
   1116      item.map.d = 999;
   1117      let combined = instance.combinerAdd(item, right, { field: "map" });
   1118      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6, d: 999 });
   1119    });
   1120    it("should add equal sized arrays", () => {
   1121      let right = makeItem();
   1122      let combined = instance.combinerAdd(item, right, { field: "arr1" });
   1123      assert.deepEqual(combined.arr1, [4, 6, 8]);
   1124    });
   1125    it("should handle missing left arrays", () => {
   1126      let right = makeItem();
   1127      right.missingarray = [5, 1, 4];
   1128      let combined = instance.combinerAdd(item, right, {
   1129        field: "missingarray",
   1130      });
   1131      assert.deepEqual(combined.missingarray, [5, 1, 4]);
   1132    });
   1133    it("should add long array to short array", () => {
   1134      let right = makeItem();
   1135      right.arr1 = [2, 3, 4, 12];
   1136      let combined = instance.combinerAdd(item, right, { field: "arr1" });
   1137      assert.deepEqual(combined.arr1, [4, 6, 8, 12]);
   1138    });
   1139    it("should add short array to long array", () => {
   1140      let right = makeItem();
   1141      item.arr1 = [2, 3, 4, 12];
   1142      let combined = instance.combinerAdd(item, right, { field: "arr1" });
   1143      assert.deepEqual(combined.arr1, [4, 6, 8, 12]);
   1144    });
   1145    it("should handle missing left number", () => {
   1146      let right = makeItem();
   1147      right.missingnumber = 999;
   1148      let combined = instance.combinerAdd(item, right, {
   1149        field: "missingnumber",
   1150      });
   1151      assert.deepEqual(combined.missingnumber, 999);
   1152    });
   1153    it("should add numbers", () => {
   1154      let right = makeItem();
   1155      let combined = instance.combinerAdd(item, right, { field: "lhs" });
   1156      assert.equal(combined.lhs, 4);
   1157    });
   1158    it("should error on missing left, and right is a string", () => {
   1159      let right = makeItem();
   1160      right.error = "error";
   1161      let combined = instance.combinerAdd(item, right, { field: "error" });
   1162      assert.equal(combined, null);
   1163    });
   1164    it("should error on left string", () => {
   1165      let right = makeItem();
   1166      let combined = instance.combinerAdd(item, right, { field: "foo" });
   1167      assert.equal(combined, null);
   1168    });
   1169    it("should error on mismatch types", () => {
   1170      let right = makeItem();
   1171      right.lhs = [1, 2, 3];
   1172      let combined = instance.combinerAdd(item, right, { field: "lhs" });
   1173      assert.equal(combined, null);
   1174    });
   1175  });
   1176 
   1177  describe("#combinerMax", () => {
   1178    it("should do nothing when right field is missing", () => {
   1179      let right = makeItem();
   1180      let combined = instance.combinerMax(item, right, { field: "missing" });
   1181      assert.deepEqual(combined, item);
   1182    });
   1183    it("should handle missing left maps", () => {
   1184      let right = makeItem();
   1185      right.missingmap = { a: 5, b: -1, c: 3 };
   1186      let combined = instance.combinerMax(item, right, { field: "missingmap" });
   1187      assert.deepEqual(combined.missingmap, { a: 5, b: -1, c: 3 });
   1188    });
   1189    it("should handle equal sized maps", () => {
   1190      let right = makeItem();
   1191      right.map = { a: 5, b: -1, c: 3 };
   1192      let combined = instance.combinerMax(item, right, { field: "map" });
   1193      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3 });
   1194    });
   1195    it("should handle short map to long map", () => {
   1196      let right = makeItem();
   1197      right.map = { a: 5, b: -1, c: 3, d: 999 };
   1198      let combined = instance.combinerMax(item, right, { field: "map" });
   1199      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3, d: 999 });
   1200    });
   1201    it("should handle long map to short map", () => {
   1202      let right = makeItem();
   1203      right.map = { a: 5, b: -1, c: 3 };
   1204      item.map.d = 999;
   1205      let combined = instance.combinerMax(item, right, { field: "map" });
   1206      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3, d: 999 });
   1207    });
   1208    it("should handle equal sized arrays", () => {
   1209      let right = makeItem();
   1210      right.arr1 = [5, 1, 4];
   1211      let combined = instance.combinerMax(item, right, { field: "arr1" });
   1212      assert.deepEqual(combined.arr1, [5, 3, 4]);
   1213    });
   1214    it("should handle missing left arrays", () => {
   1215      let right = makeItem();
   1216      right.missingarray = [5, 1, 4];
   1217      let combined = instance.combinerMax(item, right, {
   1218        field: "missingarray",
   1219      });
   1220      assert.deepEqual(combined.missingarray, [5, 1, 4]);
   1221    });
   1222    it("should handle short array to long array", () => {
   1223      let right = makeItem();
   1224      right.arr1 = [5, 1, 4, 7];
   1225      let combined = instance.combinerMax(item, right, { field: "arr1" });
   1226      assert.deepEqual(combined.arr1, [5, 3, 4, 7]);
   1227    });
   1228    it("should handle long array to short array", () => {
   1229      let right = makeItem();
   1230      right.arr1 = [5, 1, 4];
   1231      item.arr1.push(7);
   1232      let combined = instance.combinerMax(item, right, { field: "arr1" });
   1233      assert.deepEqual(combined.arr1, [5, 3, 4, 7]);
   1234    });
   1235    it("should handle missing left number", () => {
   1236      let right = makeItem();
   1237      right.missingnumber = 999;
   1238      let combined = instance.combinerMax(item, right, {
   1239        field: "missingnumber",
   1240      });
   1241      assert.deepEqual(combined.missingnumber, 999);
   1242    });
   1243    it("should handle big number", () => {
   1244      let right = makeItem();
   1245      right.lhs = 99;
   1246      let combined = instance.combinerMax(item, right, { field: "lhs" });
   1247      assert.equal(combined.lhs, 99);
   1248    });
   1249    it("should handle small number", () => {
   1250      let right = makeItem();
   1251      item.lhs = 99;
   1252      let combined = instance.combinerMax(item, right, { field: "lhs" });
   1253      assert.equal(combined.lhs, 99);
   1254    });
   1255    it("should error on missing left, and right is a string", () => {
   1256      let right = makeItem();
   1257      right.error = "error";
   1258      let combined = instance.combinerMax(item, right, { field: "error" });
   1259      assert.equal(combined, null);
   1260    });
   1261    it("should error on left string", () => {
   1262      let right = makeItem();
   1263      let combined = instance.combinerMax(item, right, { field: "foo" });
   1264      assert.equal(combined, null);
   1265    });
   1266    it("should error on mismatch types", () => {
   1267      let right = makeItem();
   1268      right.lhs = [1, 2, 3];
   1269      let combined = instance.combinerMax(item, right, { field: "lhs" });
   1270      assert.equal(combined, null);
   1271    });
   1272  });
   1273 
   1274  describe("#combinerCollectValues", () => {
   1275    it("should error on bogus operation", () => {
   1276      let right = makeItem();
   1277      right.url_domain = "maseratiusa.com/maserati";
   1278      right.time = 41;
   1279      let combined = instance.combinerCollectValues(item, right, {
   1280        left_field: "combined_map",
   1281        right_key_field: "url_domain",
   1282        right_value_field: "time",
   1283        operation: "missing",
   1284      });
   1285      assert.equal(combined, null);
   1286    });
   1287    it("should sum when missing left", () => {
   1288      let right = makeItem();
   1289      right.url_domain = "maseratiusa.com/maserati";
   1290      right.time = 41;
   1291      let combined = instance.combinerCollectValues(item, right, {
   1292        left_field: "combined_map",
   1293        right_key_field: "url_domain",
   1294        right_value_field: "time",
   1295        operation: "sum",
   1296      });
   1297      assert.deepEqual(combined.combined_map, {
   1298        "maseratiusa.com/maserati": 41,
   1299      });
   1300    });
   1301    it("should sum when missing right", () => {
   1302      let right = makeItem();
   1303      item.combined_map = { fake: 42 };
   1304      let combined = instance.combinerCollectValues(item, right, {
   1305        left_field: "combined_map",
   1306        right_key_field: "url_domain",
   1307        right_value_field: "time",
   1308        operation: "sum",
   1309      });
   1310      assert.deepEqual(combined.combined_map, { fake: 42 });
   1311    });
   1312    it("should sum when both", () => {
   1313      let right = makeItem();
   1314      right.url_domain = "maseratiusa.com/maserati";
   1315      right.time = 41;
   1316      item.combined_map = { fake: 42, "maseratiusa.com/maserati": 41 };
   1317      let combined = instance.combinerCollectValues(item, right, {
   1318        left_field: "combined_map",
   1319        right_key_field: "url_domain",
   1320        right_value_field: "time",
   1321        operation: "sum",
   1322      });
   1323      assert.deepEqual(combined.combined_map, {
   1324        fake: 42,
   1325        "maseratiusa.com/maserati": 82,
   1326      });
   1327    });
   1328 
   1329    it("should max when missing left", () => {
   1330      let right = makeItem();
   1331      right.url_domain = "maseratiusa.com/maserati";
   1332      right.time = 41;
   1333      let combined = instance.combinerCollectValues(item, right, {
   1334        left_field: "combined_map",
   1335        right_key_field: "url_domain",
   1336        right_value_field: "time",
   1337        operation: "max",
   1338      });
   1339      assert.deepEqual(combined.combined_map, {
   1340        "maseratiusa.com/maserati": 41,
   1341      });
   1342    });
   1343    it("should max when missing right", () => {
   1344      let right = makeItem();
   1345      item.combined_map = { fake: 42 };
   1346      let combined = instance.combinerCollectValues(item, right, {
   1347        left_field: "combined_map",
   1348        right_key_field: "url_domain",
   1349        right_value_field: "time",
   1350        operation: "max",
   1351      });
   1352      assert.deepEqual(combined.combined_map, { fake: 42 });
   1353    });
   1354    it("should max when both (right)", () => {
   1355      let right = makeItem();
   1356      right.url_domain = "maseratiusa.com/maserati";
   1357      right.time = 99;
   1358      item.combined_map = { fake: 42, "maseratiusa.com/maserati": 41 };
   1359      let combined = instance.combinerCollectValues(item, right, {
   1360        left_field: "combined_map",
   1361        right_key_field: "url_domain",
   1362        right_value_field: "time",
   1363        operation: "max",
   1364      });
   1365      assert.deepEqual(combined.combined_map, {
   1366        fake: 42,
   1367        "maseratiusa.com/maserati": 99,
   1368      });
   1369    });
   1370    it("should max when both (left)", () => {
   1371      let right = makeItem();
   1372      right.url_domain = "maseratiusa.com/maserati";
   1373      right.time = -99;
   1374      item.combined_map = { fake: 42, "maseratiusa.com/maserati": 41 };
   1375      let combined = instance.combinerCollectValues(item, right, {
   1376        left_field: "combined_map",
   1377        right_key_field: "url_domain",
   1378        right_value_field: "time",
   1379        operation: "max",
   1380      });
   1381      assert.deepEqual(combined.combined_map, {
   1382        fake: 42,
   1383        "maseratiusa.com/maserati": 41,
   1384      });
   1385    });
   1386 
   1387    it("should overwrite when missing left", () => {
   1388      let right = makeItem();
   1389      right.url_domain = "maseratiusa.com/maserati";
   1390      right.time = 41;
   1391      let combined = instance.combinerCollectValues(item, right, {
   1392        left_field: "combined_map",
   1393        right_key_field: "url_domain",
   1394        right_value_field: "time",
   1395        operation: "overwrite",
   1396      });
   1397      assert.deepEqual(combined.combined_map, {
   1398        "maseratiusa.com/maserati": 41,
   1399      });
   1400    });
   1401    it("should overwrite when missing right", () => {
   1402      let right = makeItem();
   1403      item.combined_map = { fake: 42 };
   1404      let combined = instance.combinerCollectValues(item, right, {
   1405        left_field: "combined_map",
   1406        right_key_field: "url_domain",
   1407        right_value_field: "time",
   1408        operation: "overwrite",
   1409      });
   1410      assert.deepEqual(combined.combined_map, { fake: 42 });
   1411    });
   1412    it("should overwrite when both", () => {
   1413      let right = makeItem();
   1414      right.url_domain = "maseratiusa.com/maserati";
   1415      right.time = 41;
   1416      item.combined_map = { fake: 42, "maseratiusa.com/maserati": 77 };
   1417      let combined = instance.combinerCollectValues(item, right, {
   1418        left_field: "combined_map",
   1419        right_key_field: "url_domain",
   1420        right_value_field: "time",
   1421        operation: "overwrite",
   1422      });
   1423      assert.deepEqual(combined.combined_map, {
   1424        fake: 42,
   1425        "maseratiusa.com/maserati": 41,
   1426      });
   1427    });
   1428 
   1429    it("should count when missing left", () => {
   1430      let right = makeItem();
   1431      right.url_domain = "maseratiusa.com/maserati";
   1432      right.time = 41;
   1433      let combined = instance.combinerCollectValues(item, right, {
   1434        left_field: "combined_map",
   1435        right_key_field: "url_domain",
   1436        right_value_field: "time",
   1437        operation: "count",
   1438      });
   1439      assert.deepEqual(combined.combined_map, {
   1440        "maseratiusa.com/maserati": 1,
   1441      });
   1442    });
   1443    it("should count when missing right", () => {
   1444      let right = makeItem();
   1445      item.combined_map = { fake: 42 };
   1446      let combined = instance.combinerCollectValues(item, right, {
   1447        left_field: "combined_map",
   1448        right_key_field: "url_domain",
   1449        right_value_field: "time",
   1450        operation: "count",
   1451      });
   1452      assert.deepEqual(combined.combined_map, { fake: 42 });
   1453    });
   1454    it("should count when both", () => {
   1455      let right = makeItem();
   1456      right.url_domain = "maseratiusa.com/maserati";
   1457      right.time = 41;
   1458      item.combined_map = { fake: 42, "maseratiusa.com/maserati": 1 };
   1459      let combined = instance.combinerCollectValues(item, right, {
   1460        left_field: "combined_map",
   1461        right_key_field: "url_domain",
   1462        right_value_field: "time",
   1463        operation: "count",
   1464      });
   1465      assert.deepEqual(combined.combined_map, {
   1466        fake: 42,
   1467        "maseratiusa.com/maserati": 2,
   1468      });
   1469    });
   1470  });
   1471 
   1472  describe("#executeRecipe", () => {
   1473    it("should handle working steps", () => {
   1474      let final = instance.executeRecipe({}, [
   1475        { function: "set_default", field: "foo", value: 1 },
   1476        { function: "set_default", field: "bar", value: 10 },
   1477      ]);
   1478      assert.equal(final.foo, 1);
   1479      assert.equal(final.bar, 10);
   1480    });
   1481    it("should handle unknown steps", () => {
   1482      let final = instance.executeRecipe({}, [
   1483        { function: "set_default", field: "foo", value: 1 },
   1484        { function: "missing" },
   1485        { function: "set_default", field: "bar", value: 10 },
   1486      ]);
   1487      assert.equal(final, null);
   1488    });
   1489    it("should handle erroring steps", () => {
   1490      let final = instance.executeRecipe({}, [
   1491        { function: "set_default", field: "foo", value: 1 },
   1492        {
   1493          function: "accept_item_by_field_value",
   1494          field: "missing",
   1495          op: "invalid",
   1496          rhsField: "moot",
   1497          rhsValue: "m00t",
   1498        },
   1499        { function: "set_default", field: "bar", value: 10 },
   1500      ]);
   1501      assert.equal(final, null);
   1502    });
   1503  });
   1504 
   1505  describe("#executeCombinerRecipe", () => {
   1506    it("should handle working steps", () => {
   1507      let final = instance.executeCombinerRecipe(
   1508        { foo: 1, bar: 10 },
   1509        { foo: 1, bar: 10 },
   1510        [
   1511          { function: "combiner_add", field: "foo" },
   1512          { function: "combiner_add", field: "bar" },
   1513        ]
   1514      );
   1515      assert.equal(final.foo, 2);
   1516      assert.equal(final.bar, 20);
   1517    });
   1518    it("should handle unknown steps", () => {
   1519      let final = instance.executeCombinerRecipe(
   1520        { foo: 1, bar: 10 },
   1521        { foo: 1, bar: 10 },
   1522        [
   1523          { function: "combiner_add", field: "foo" },
   1524          { function: "missing" },
   1525          { function: "combiner_add", field: "bar" },
   1526        ]
   1527      );
   1528      assert.equal(final, null);
   1529    });
   1530    it("should handle erroring steps", () => {
   1531      let final = instance.executeCombinerRecipe(
   1532        { foo: 1, bar: 10, baz: 0 },
   1533        { foo: 1, bar: 10, baz: "hundred" },
   1534        [
   1535          { function: "combiner_add", field: "foo" },
   1536          { function: "combiner_add", field: "baz" },
   1537          { function: "combiner_add", field: "bar" },
   1538        ]
   1539      );
   1540      assert.equal(final, null);
   1541    });
   1542  });
   1543 });