Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
ChromeUtils.defineESModuleGetters(this, {
});
add_task(async function test_weightedSampleTopSites_no_guid_last() {
// Ranker are utilities we are testing
const Ranker = ChromeUtils.importESModule(
);
const provider = new Ranker.RankShortcutsProvider();
// We are going to stub a database call
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
// Stub DB call
sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.resolves([
["a", 5, 10],
["b", 2, 10],
]);
// First item here intentially has no guid
const input = [
{ url: "no-guid.com" },
{ guid: "a", url: "a.com" },
{ guid: "b", url: "b.com" },
];
const prefValues = {
trainhopConfig: {
smartShortcuts: {
// fset: "custom", // uncomment iff your build defines a "custom" fset you want to use
eta: 0,
click_bonus: 10,
positive_prior: 1,
negative_prior: 1,
fset: 1,
},
},
};
const result = await provider.rankTopSites(input, prefValues, {
isStartup: false,
});
Assert.ok(Array.isArray(result), "returns an array");
Assert.equal(
result[result.length - 1].url,
"no-guid.com",
"top-site without GUID is last"
);
sandbox.restore();
});
add_task(async function test_sumNorm() {
const Ranker = ChromeUtils.importESModule(
);
let vec = [1, 1];
let result = Ranker.sumNorm(vec);
Assert.ok(
result.every((v, i) => Math.abs(v - [0.5, 0.5][i]) < 1e-6),
"sum norm works as expected for dense array"
);
vec = [0, 0];
result = Ranker.sumNorm(vec);
Assert.ok(
result.every((v, i) => Math.abs(v - [0.0, 0.0][i]) < 1e-6),
"if sum is 0.0, it should return the original vector, input is zeros"
);
vec = [1, -1];
result = Ranker.sumNorm(vec);
Assert.ok(
result.every((v, i) => Math.abs(v - [1.0, -1.0][i]) < 1e-6),
"if sum is 0.0, it should return the original vector, input contains negatives"
);
});
add_task(async function test_computeLinearScore() {
const Ranker = ChromeUtils.importESModule(
);
let entry = { a: 1, b: 0, bias: 1 };
let weights = { a: 1, b: 0, bias: 0 };
let result = Ranker.computeLinearScore(entry, weights);
Assert.equal(result, 1, "check linear score with one non-zero weight");
entry = { a: 1, b: 1, bias: 1 };
weights = { a: 1, b: 1, bias: 1 };
result = Ranker.computeLinearScore(entry, weights);
Assert.equal(result, 3, "check linear score with 1 everywhere");
entry = { bias: 1 };
weights = { a: 1, b: 1, bias: 1 };
result = Ranker.computeLinearScore(entry, weights);
Assert.equal(result, 1, "check linear score with empty entry, get bias");
entry = { a: 1, b: 1, bias: 1 };
weights = {};
result = Ranker.computeLinearScore(entry, weights);
Assert.equal(result, 0, "check linear score with empty weights");
entry = { a: 1, b: 1, bias: 1 };
weights = { a: 3 };
result = Ranker.computeLinearScore(entry, weights);
Assert.equal(result, 3, "check linear score with a missing weight");
});
add_task(async function test_interpolateWrappedHistogram() {
const Ranker = ChromeUtils.importESModule(
);
let hist = [1, 2];
let t = 0.5;
let result = Ranker.interpolateWrappedHistogram(hist, t);
Assert.equal(result, 1.5, "test linear interpolation square in the middle");
hist = [1, 2, 5];
t = 2.5;
result = Ranker.interpolateWrappedHistogram(hist, t);
Assert.equal(
result,
(hist[0] + hist[2]) / 2,
"test linear interpolation correctly wraps around"
);
hist = [1, 2, 5];
t = 5;
result = Ranker.interpolateWrappedHistogram(hist, t);
Assert.equal(
result,
hist[2],
"linear interpolation will wrap around to the last index"
);
});
add_task(async function test_bayesHist() {
const Ranker = ChromeUtils.importESModule(
);
let vec = [1, 2];
let pvec = [0.5, 0.5];
let tau = 2;
let result = Ranker.bayesHist(vec, pvec, tau);
// checking the math
// (1+2*.5)/(3+2)........... 2/5
// (2+2*.5)/(3+2)........... 3/5
Assert.ok(
result.every((v, i) => Math.abs(v - [0.4, 0.6][i]) < 1e-6),
"bayes histogram is expected for typical input"
);
vec = [1, 2];
pvec = [0.5, 0.5];
tau = 0.0;
result = Ranker.bayesHist(vec, pvec, tau);
Assert.ok(
result.every((v, i) => Math.abs(v - vec[i] / 3) < 1e-6), // 3 is sum of vec
"bayes histogram is a sum norming function if tau is 0"
);
});
add_task(async function test_normSites() {
const Ranker = ChromeUtils.importESModule(
);
let Y = {
a: [2, 2],
b: [3, 3],
};
let result = Ranker.normHistDict(Y);
Assert.ok(
result.a.every(
(v, i) => Math.abs(v - [4 / (4 + 9), 4 / (4 + 9)][i]) < 1e-6
),
"normSites, basic input, first array"
);
Assert.ok(
result.b.every(
(v, i) => Math.abs(v - [9 / (4 + 9), 9 / (4 + 9)][i]) < 1e-6
),
"normSites, basic input, second array"
);
Y = [];
result = Ranker.normHistDict(Y);
Assert.deepEqual(result, [], "normSites handles empty array");
});
add_task(async function test_clampWeights() {
const Ranker = ChromeUtils.importESModule(
);
let weights = { a: 0, b: 1000, bias: 0 };
let result = Ranker.clampWeights(weights, 100);
info("clampWeights clamps a big weight vector");
Assert.equal(result.a, 0);
Assert.equal(result.b, 100);
Assert.equal(result.bias, 0);
weights = { a: 1, b: 1, bias: 1 };
result = Ranker.clampWeights(weights, 100);
info("clampWeights ignores a small weight vector");
Assert.equal(result.a, 1);
Assert.equal(result.b, 1);
Assert.equal(result.bias, 1);
});
add_task(async function test_updateWeights_batch() {
// Import the module that exports updateWeights
const Ranker = ChromeUtils.importESModule(
);
const eta = 1;
const click_bonus = 1;
const features = ["a", "b", "bias"];
function sigmoid(x) {
return 1 / (1 + Math.exp(-x));
}
function approxEqual(actual, expected, eps = 1e-12) {
Assert.lessOrEqual(
Math.abs(actual - expected),
eps,
`expected ${actual} ≈ ${expected}`
);
}
// single click on guid_A updates all weights
const initial1 = { a: 0, b: 1, bias: 0.1 };
const scores1 = {
guid_A: { final: 1.1, a: 1, b: 1, bias: 1 },
};
let updated = await Ranker.updateWeights(
{
data: { guid_A: { clicks: 1, impressions: 0 } },
scores: scores1,
features,
weights: { ...initial1 },
eta,
click_bonus,
},
false
);
const delta = sigmoid(1.1) - 1; // gradient term for a positive click
approxEqual(updated.a, 0 - Number(delta) * 1);
approxEqual(updated.b, 1 - Number(delta) * 1);
approxEqual(updated.bias, 0.1 - Number(delta) * 1);
// missing guid -> no-op (weights unchanged)
const initial2 = { a: 0, b: 1000, bias: 0 };
const scores2 = {
guid_A: { final: 1, a: 1, b: 1e-3, bias: 1 },
};
updated = await Ranker.updateWeights(
{
data: { guid_B: { clicks: 1, impressions: 0 } }, // guid_B not in scores
scores: scores2,
features,
weights: { ...initial2 },
eta,
click_bonus,
},
false
);
Assert.equal(updated.a, 0);
Assert.equal(updated.b, 1000);
Assert.equal(updated.bias, 0);
});
add_task(async function test_buildFrecencyFeatures_shape_and_empty() {
const Ranker = ChromeUtils.importESModule(
);
// empty inputs → empty feature maps
let out = await Ranker.buildFrecencyFeatures({}, {});
Assert.ok(
out && "refre" in out && "rece" in out && "freq" in out,
"returns transposed object with feature keys"
);
Assert.deepEqual(out.refre, {}, "refre empty");
Assert.deepEqual(out.rece, {}, "rece empty");
Assert.deepEqual(out.freq, {}, "freq empty");
// single guid with no visits → zeros
out = await Ranker.buildFrecencyFeatures({ guidA: [] }, { guidA: 42 });
Assert.equal(out.refre.guidA, 0, "refre zero with no visits");
Assert.equal(out.freq.guidA, 0, "freq zero with no visits");
Assert.equal(out.rece.guidA, 0, "rece zero with no visits");
});
add_task(async function test_buildFrecencyFeatures_recency_monotonic() {
const Ranker = ChromeUtils.importESModule(
);
const sandbox = sinon.createSandbox();
// Fix time so the decay is deterministic
const nowMs = Date.UTC(2025, 0, 1); // Jan 1, 2025
const clock = sandbox.useFakeTimers({ now: nowMs });
const dayMs = 864e5;
// visits use microseconds since epoch; function computes age with visit_date_us / 1000
const us = v => Math.round(v * 1000);
const visitsByGuid = {
// one very recent visit (age ~0d)
recent: [{ visit_date_us: us(nowMs), visit_type: 2 /* any value */ }],
// one older visit (age 10d)
old: [{ visit_date_us: us(nowMs - 10 * dayMs), visit_type: 2 }],
// both recent + old → should have larger rece (sum of decays)
both: [
{ visit_date_us: us(nowMs), visit_type: 2 },
{ visit_date_us: us(nowMs - 10 * dayMs), visit_type: 2 },
],
};
const visitCounts = { recent: 10, old: 10, both: 10 };
const out = await Ranker.buildFrecencyFeatures(visitsByGuid, visitCounts, {
halfLifeDays: 28, // default
});
Assert.greater(
out.rece.recent,
out.rece.old,
"more recent visit → larger recency score"
);
Assert.greater(
out.rece.both,
out.rece.recent,
"two visits (recent+old) → larger recency sum than just recent"
);
clock.restore();
sandbox.restore();
});
add_task(
async function test_buildFrecencyFeatures_log_scaling_and_interaction() {
const Ranker = ChromeUtils.importESModule(
);
const sandbox = sinon.createSandbox();
const clock = sandbox.useFakeTimers({ now: Date.UTC(2025, 0, 1) });
const nowMs = Date.now();
const us = v => Math.round(v * 1000);
// Same single visit (type held constant) for both GUIDs → same rece and same type sum.
// Only visit_count differs → freq/refre should scale with log1p(visit_count).
const visitsByGuid = {
A: [{ visit_date_us: us(nowMs), visit_type: 2 /* 'typed' typically */ }],
B: [{ visit_date_us: us(nowMs), visit_type: 2 }],
};
const visitCounts = { A: 9, B: 99 }; // different lifetime counts
const out = await Ranker.buildFrecencyFeatures(visitsByGuid, visitCounts);
// rece should be identical (same timestamps)
Assert.equal(
out.rece.A,
out.rece.B,
"recency is independent of visit_count"
);
// freq and refre should scale with log1p(total)
const ratio = Math.log1p(visitCounts.B) / Math.log1p(visitCounts.A);
const approxEqual = (a, b, eps = 1e-9) =>
Assert.lessOrEqual(Math.abs(a - b), eps);
approxEqual(out.freq.B / out.freq.A, ratio, 1e-9);
approxEqual(out.refre.B / out.refre.A, ratio, 1e-9);
clock.restore();
sandbox.restore();
}
);
add_task(async function test_buildFrecencyFeatures_halfLife_effect() {
const Ranker = ChromeUtils.importESModule(
);
const sandbox = sinon.createSandbox();
const nowMs = Date.UTC(2025, 0, 1);
const clock = sandbox.useFakeTimers({ now: nowMs });
const dayMs = 864e5;
const us = v => Math.round(v * 1000);
// Two visits: one now, one 28 days ago.
const visits = [
{
visit_date_us: us(nowMs),
visit_type: 1,
},
{
visit_date_us: us(nowMs - 28 * dayMs),
visit_type: 1,
},
];
const visitsByGuid = { X: visits };
const visitCounts = { X: 10 };
const outShort = await Ranker.buildFrecencyFeatures(
visitsByGuid,
visitCounts,
{
halfLifeDays: 7, // faster decay
}
);
const outLong = await Ranker.buildFrecencyFeatures(
visitsByGuid,
visitCounts,
{
halfLifeDays: 56, // slower decay
}
);
Assert.greater(
outLong.rece.X,
outShort.rece.X,
"larger half-life → slower decay → bigger recency sum"
);
clock.restore();
sandbox.restore();
});
add_task(
async function test_buildFrecencyFeatures_unknown_types_are_zero_bonus() {
const Ranker = ChromeUtils.importESModule(
);
const sandbox = sinon.createSandbox();
const clock = sandbox.useFakeTimers({ now: Date.UTC(2025, 0, 1) });
const nowMs = Date.now();
const us = v => Math.round(v * 1000);
// Use an unknown visit_type so TYPE_SCORE[visit_type] falls back to 0.
const visitsByGuid = {
U: [{ visit_date_us: us(nowMs), visit_type: 99999 }],
};
const visitCounts = { U: 100 };
const out = await Ranker.buildFrecencyFeatures(visitsByGuid, visitCounts);
Assert.equal(out.freq.U, 0, "unknown visit_type → zero frequency bonus");
Assert.equal(out.refre.U, 0, "unknown visit_type → zero interaction");
Assert.greater(out.rece.U, 0, "recency is still > 0 for a recent visit");
clock.restore();
sandbox.restore();
}
);
add_task(
async function test_weightedSampleTopSites_new_features_scores_and_norms() {
const Ranker = ChromeUtils.importESModule(
);
// 2 GUIDs with simple, distinct raw values so ordering is obvious
const guids = ["g1", "g2"];
const input = {
guid: guids,
features: ["bmark", "rece", "freq", "refre", "bias"],
// raw feature maps keyed by guid
bmark_scores: { g1: 1, g2: 0 }, // bookmark flag/intensity
rece_scores: { g1: 2.0, g2: 1.0 }, // recency-only raw
freq_scores: { g1: 0.5, g2: 0.25 }, // frequency-only raw
refre_scores: { g1: 10, g2: 5 }, // interaction raw
// per-feature normalization state (whatever your normUpdate expects)
norms: { bmark: null, rece: null, freq: null, refre: null },
// weights: only new features (and bias) contribute to final
weights: { bmark: 1, rece: 1, freq: 1, refre: 1, bias: 0 },
// unused here (no "thom", "hour", "daily")
clicks: [],
impressions: [],
alpha: 1,
beta: 1,
tau: 0,
hourly_seasonality: {},
daily_seasonality: {},
};
// Pre-compute what normUpdate will return for each raw vector,
// so we can assert exact equality with weightedSampleTopSites output.
const vecBmark = guids.map(g => input.bmark_scores[g]); // [1, 0]
const vecRece = guids.map(g => input.rece_scores[g]); // [2, 1]
const vecFreq = guids.map(g => input.freq_scores[g]); // [0.5, 0.25]
const vecRefre = guids.map(g => input.refre_scores[g]); // [10, 5]
const [expBmark, expBmarkNorm] = Ranker.normUpdate(
vecBmark,
input.norms.bmark
);
const [expRece, expReceNorm] = Ranker.normUpdate(vecRece, input.norms.rece);
const [expFreq, expFreqNorm] = Ranker.normUpdate(vecFreq, input.norms.freq);
const [expRefre, expRefreNorm] = Ranker.normUpdate(
vecRefre,
input.norms.refre
);
const result = await Ranker.weightedSampleTopSites(input);
// shape
Assert.ok(
result && result.score_map && result.norms,
"returns {score_map, norms}"
);
// per-feature, per-guid scores match normUpdate outputs
Assert.equal(
result.score_map.g1.bmark,
expBmark[0],
"bmark g1 normalized value"
);
Assert.equal(
result.score_map.g2.bmark,
expBmark[1],
"bmark g2 normalized value"
);
Assert.equal(
result.score_map.g1.rece,
expRece[0],
"rece g1 normalized value"
);
Assert.equal(
result.score_map.g2.rece,
expRece[1],
"rece g2 normalized value"
);
Assert.equal(
result.score_map.g1.freq,
expFreq[0],
"freq g1 normalized value"
);
Assert.equal(
result.score_map.g2.freq,
expFreq[1],
"freq g2 normalized value"
);
Assert.equal(
result.score_map.g1.refre,
expRefre[0],
"refre g1 normalized value"
);
Assert.equal(
result.score_map.g2.refre,
expRefre[1],
"refre g2 normalized value"
);
// updated norms propagated back out
Assert.deepEqual(result.norms.bmark, expBmarkNorm, "bmark norms updated");
Assert.deepEqual(result.norms.rece, expReceNorm, "rece norms updated");
Assert.deepEqual(result.norms.freq, expFreqNorm, "freq norms updated");
// THIS WILL FAIL until you fix the bug in the refre block:
// updated_norms.rece = refre_norm;
// should be:
// updated_norms.refre = refre_norm;
Assert.deepEqual(result.norms.refre, expRefreNorm, "refre norms updated");
// final score equals computeLinearScore with provided weights
const expectedFinalG1 = Ranker.computeLinearScore(
result.score_map.g1,
input.weights
);
const expectedFinalG2 = Ranker.computeLinearScore(
result.score_map.g2,
input.weights
);
Assert.equal(
result.score_map.g1.final,
expectedFinalG1,
"final matches linear score (g1)"
);
Assert.equal(
result.score_map.g2.final,
expectedFinalG2,
"final matches linear score (g2)"
);
// sanity: g1 should outrank g2 with strictly larger raw vectors across all features
Assert.greaterOrEqual(
result.score_map.g1.final,
result.score_map.g2.final,
"g1 final ≥ g2 final as all contributing features favor g1"
);
}
);
add_task(async function test_rankshortcuts_queries_returning_nothing() {
const Ranker = ChromeUtils.importESModule(
);
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
const stub = sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.resolves([]); // <— mock: query returns nothing
const topsites = [{ guid: "g1" }, { guid: "g2" }];
const places = "moz_places";
const hist = "moz_historyvisits";
// fetchVisitCountsByGuid: returns {} if no rows come back
{
const out = await Ranker.fetchVisitCountsByGuid(topsites, places);
Assert.deepEqual(out, {}, "visit counts: empty rows → empty map");
}
// fetchLast10VisitsByGuid: pre-seeds all guids → each should be []
{
const out = await Ranker.fetchLast10VisitsByGuid(topsites, hist, places);
Assert.deepEqual(
out,
{ g1: [], g2: [] },
"last 10 visits: empty rows → each guid has []"
);
}
// fetchBookmarkedFlags: ensures every guid present with false
{
const out = await Ranker.fetchBookmarkedFlags(
topsites,
"moz_bookmarks",
places
);
Assert.deepEqual(
out,
{ g1: false, g2: false },
"bookmarks: empty rows → every guid is false"
);
}
// fetchDailyVisitsSpecific: ensures every guid has a 7-bin zero hist
{
const out = await Ranker.fetchDailyVisitsSpecific(topsites, hist, places);
Assert.ok(
Array.isArray(out.g1) && out.g1.length === 7,
"daily specific: 7 bins"
);
Assert.ok(
Array.isArray(out.g2) && out.g2.length === 7,
"daily specific: 7 bins"
);
Assert.ok(
out.g1.every(v => v === 0) && out.g2.every(v => v === 0),
"daily specific: all zeros"
);
}
// fetchDailyVisitsAll: returns a 7-bin zero hist
{
const out = await Ranker.fetchDailyVisitsAll(hist);
Assert.ok(Array.isArray(out) && out.length === 7, "daily all: 7 bins");
Assert.ok(
out.every(v => v === 0),
"daily all: all zeros"
);
}
// fetchHourlyVisitsSpecific: ensures every guid has a 24-bin zero hist
{
const out = await Ranker.fetchHourlyVisitsSpecific(topsites, hist, places);
Assert.ok(
Array.isArray(out.g1) && out.g1.length === 24,
"hourly specific: 24 bins"
);
Assert.ok(
Array.isArray(out.g2) && out.g2.length === 24,
"hourly specific: 24 bins"
);
Assert.ok(
out.g1.every(v => v === 0) && out.g2.every(v => v === 0),
"hourly specific: all zeros"
);
}
// fetchHourlyVisitsAll: returns a 24-bin zero hist
{
const out = await Ranker.fetchHourlyVisitsAll(hist);
Assert.ok(Array.isArray(out) && out.length === 24, "hourly all: 24 bins");
Assert.ok(
out.every(v => v === 0),
"hourly all: all zeros"
);
}
// sanity: we actually exercised the stub at least once
Assert.greater(stub.callCount, 0, "executePlacesQuery was called");
sandbox.restore();
});
// cover the explicit "no topsites" early-return branches
add_task(async function test_rankshortcuts_no_topsites_inputs() {
const Ranker = ChromeUtils.importESModule(
);
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
const stub = sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.resolves([]); // should not matter (early return)
// empty topsites → {}
Assert.deepEqual(
await Ranker.fetchVisitCountsByGuid([], "moz_places"),
{},
"visit counts early return {}"
);
Assert.deepEqual(
await Ranker.fetchLast10VisitsByGuid([], "moz_historyvisits", "moz_places"),
{},
"last10 early return {}"
);
Assert.deepEqual(
await Ranker.fetchBookmarkedFlags([], "moz_bookmarks", "moz_places"),
{},
"bookmarks early return {}"
);
Assert.deepEqual(
await Ranker.fetchDailyVisitsSpecific(
[],
"moz_historyvisits",
"moz_places"
),
{},
"daily specific early return {}"
);
Assert.deepEqual(
await Ranker.fetchHourlyVisitsSpecific(
[],
"moz_historyvisits",
"moz_places"
),
{},
"hourly specific early return {}"
);
// note: the *All* variants don't take topsites and thus aren't part of this early-return set.
// make sure we didn't query DB for early-return cases
Assert.equal(stub.callCount, 0, "no DB calls for early-return functions");
sandbox.restore();
});
add_task(async function test_rankTopSites_sql_pipeline_happy_path() {
const Ranker = ChromeUtils.importESModule(
);
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
// freeze time so recency/seasonality are stable
const nowFixed = Date.UTC(2025, 0, 1, 12, 0, 0); // Jan 1, 2025 @ noon UTC
const clock = sandbox.useFakeTimers({ now: nowFixed });
// inputs: one strong site (g1), one weaker (g2), and one without guid
const topsites = [
{ guid: "g1", url: "https://g1.com", frecency: 1000 },
{ guid: "g2", url: "https://g2.com", frecency: 10 },
{ url: "https://no-guid.com" }, // should end up last
];
// provider under test
const provider = new Ranker.RankShortcutsProvider();
// keep cache interactions in-memory + observable
const initialWeights = {
bmark: 1,
rece: 1,
freq: 1,
refre: 1,
hour: 1,
daily: 1,
bias: 0,
frec: 0,
};
const fakeCache = {
weights: { ...initialWeights },
init_weights: { ...initialWeights },
norms: null,
// minimal score_map so updateWeights can run even if eta > 0 (we set eta=0 below)
score_map: { g1: { final: 0 }, g2: { final: 0 } },
};
sandbox.stub(provider.sc_obj, "get").resolves(fakeCache);
const setSpy = sandbox.stub(provider.sc_obj, "set").resolves();
// stub DB: return realistic rows per SQL shape
const execStub = sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.callsFake(async sql => {
const s = String(sql);
// --- Bookmarks (fetchBookmarkedFlags)
if (s.includes("FROM moz_bookmarks")) {
// rows: [key, bookmark_count]
return [
["g1", 2], // bookmarked
["g2", 0], // not bookmarked
];
}
// --- Visit counts (fetchVisitCountsByGuid)
if (s.includes("COALESCE(p.visit_count")) {
// rows: [guid, visit_count]
return [
["g1", 42],
["g2", 3],
];
}
// --- Last 10 visits (fetchLast10VisitsByGuid)
if (s.includes("LIMIT 10") && s.includes("ORDER BY vv.visit_date DESC")) {
// rows: [guid, visit_date_us, visit_type], ordered by guid/date DESC
const nowUs = nowFixed * 1000;
const dayUs = 864e8;
// g1: two recent visits (typed + link); g2: one older link
return [
["g1", nowUs, 2], // TYPED
["g1", nowUs - 1 * Number(dayUs), 1], // LINK (yesterday)
["g2", nowUs - 10 * Number(dayUs), 1], // LINK (older)
];
}
// --- Daily specific (fetchDailyVisitsSpecific): rows [key, dow, count]
if (
s.includes("strftime('%w'") &&
s.includes("GROUP BY place_ids.guid")
) {
return [
["g1", 1, 3], // Mon
["g1", 2, 2], // Tue
["g2", 1, 1], // Mon
];
}
// --- Daily all (fetchDailyVisitsAll): rows [dow, count]
if (s.includes("strftime('%w'") && !s.includes("place_ids.guid")) {
return [
[0, 10],
[1, 20],
[2, 15],
[3, 12],
[4, 8],
[5, 5],
[6, 4],
];
}
// --- Hourly specific (fetchHourlyVisitsSpecific): rows [key, hour, count]
if (
s.includes("strftime('%H'") &&
s.includes("GROUP BY place_ids.guid")
) {
return [
["g1", 9, 5],
["g1", 10, 3],
["g2", 9, 1],
];
}
// --- Hourly all (fetchHourlyVisitsAll): rows [hour, count]
if (s.includes("strftime('%H'") && !s.includes("place_ids.guid")) {
return Array.from({ length: 24 }, (_, h) => [
h,
h >= 8 && h <= 18 ? 10 : 2,
]);
}
// --- Shortcut interactions (fetchShortcutInteractions)
// The query varies by implementation; if your helper hits SQL, fall back to "empty"
if (
s.toLowerCase().includes("shortcut") ||
s.toLowerCase().includes("smartshortcuts")
) {
return [];
}
// default: empty for any other SQL the provider might issue
return [];
});
// prefs: set eta=0 so updateWeights is a no-op (deterministic)
// NOTE: Feature set comes from the module's SHORTCUT_FSETS.
// If your build's default fset already includes ["bmark","rece","freq","refre","hour","daily","bias","frec"],
// this test will exercise all the SQL above. If not, it still passes the core assertions.
const prefValues = {
trainhopConfig: {
smartShortcuts: {
// fset: "custom", // uncomment iff your build defines a "custom" fset you want to use
eta: 0,
click_bonus: 10,
positive_prior: 1,
negative_prior: 1,
fset: 8,
telem: true,
},
},
};
// run
const out = await provider.rankTopSites(topsites, prefValues, {
isStartup: false,
});
// assertions
Assert.ok(Array.isArray(out), "returns an array");
Assert.ok(
out.every(
site =>
Object.prototype.hasOwnProperty.call(site, "scores") &&
Object.prototype.hasOwnProperty.call(site, "weights")
),
"Every shortcut should expose scores and weights (even when null) because telem=true"
);
Assert.equal(
out[out.length - 1].url,
"no-guid item is last"
);
// If the active fset includes the “new” features, g1 should outrank g2 based on our SQL.
const rankedGuids = out.filter(x => x.guid).map(x => x.guid);
if (rankedGuids.length === 2) {
// This is a soft assertion—kept as ok() instead of equal() so the test still passes
// on builds where the active fset does not include those features.
Assert.strictEqual(
rankedGuids[0],
"g1",
`expected g1 to outrank g2 when feature set includes (bmark/rece/freq/refre/hour/daily); got ${rankedGuids}`
);
}
// cache writes happened (norms + score_map)
Assert.ok(setSpy.calledWith("norms"), "norms written to cache");
Assert.ok(setSpy.calledWith("score_map"), "score_map written to cache");
// sanity: DB stub exercised
Assert.greater(execStub.callCount, 0, "executePlacesQuery was called");
// cleanup
clock.restore();
sandbox.restore();
});
//
// 2) weightedSampleTopSites: "open" feature sets per-guid scores and norms
//
add_task(async function test_weightedSampleTopSites_open_feature_basic() {
const Worker = ChromeUtils.importESModule(
);
// Three guids; g1 and g3 are open, g2 is closed.
const guid = ["g1", "g2", "g3"];
const input = {
features: ["open", "bias"],
guid,
// keep only "open" contributing to final; set bias to 0 for clarity
weights: { open: 1, bias: 0 },
// no prior norms → normUpdate will initialize from first value
norms: { open: null },
// provide the open_scores dict in the format rankTopSites will pass
open_scores: { g1: 1, g2: 0, g3: 1 },
};
const out = await Worker.weightedSampleTopSites(input);
// shape
Assert.ok(out && out.score_map && out.norms, "returns {score_map, norms}");
Assert.ok(out.norms.open, "norms include 'open'");
// The normalized values are opaque, but ordering should reflect inputs:
const s1 = out.score_map.g1.open;
const s2 = out.score_map.g2.open;
const s3 = out.score_map.g3.open;
Assert.greater(s1, s2, "an open guid should score higher than a closed one");
Assert.greater(s3, s2, "another open guid should score higher than closed");
Assert.equal(
Math.abs(s1 - s3) < 1e-12, // normalization treats identical inputs identically
true,
"two open guids with identical inputs get identical normalized scores"
);
// final should reflect the 'open' score since bias has weight 0
Assert.equal(out.score_map.g1.final, s1, "final = open (g1)");
Assert.equal(out.score_map.g2.final, s2, "final = open (g2)");
Assert.equal(out.score_map.g3.final, s3, "final = open (g3)");
});
//
// 3) weightedSampleTopSites: "open" feature honors existing running-norm state
//
add_task(async function test_weightedSampleTopSites_open_with_existing_norms() {
const Worker = ChromeUtils.importESModule(
);
const guid = ["A", "B", "C", "D"];
const open_scores = { A: 0, B: 1, C: 0, D: 1 };
// Seed a prior norm object to ensure running mean/var is updated & used.
const prior = { beta: 1e-3, mean: 0.5, var: 1.0 };
const input = {
features: ["open", "bias"],
guid,
weights: { open: 1, bias: 0 },
norms: { open: prior },
open_scores,
};
const out = await Worker.weightedSampleTopSites(input);
// Norm object should be updated (mean or var shifts minutely due to beta)
Assert.ok(out.norms.open, "open norm returned");
Assert.notEqual(out.norms.open.mean, prior.mean, "running mean updated");
Assert.greater(out.norms.open.var, 0, "variance stays positive");
// Basic ordering still holds
const finals = guid.map(g => out.score_map[g].final);
// open (1) items should outrank closed (0) items after normalization
const maxClosed = Math.max(finals[0], finals[2]); // A,C
const minOpen = Math.min(finals[1], finals[3]); // B,D
Assert.greater(minOpen, maxClosed, "open > closed after normalization");
});
add_task(async function test_buildFrecencyFeatures_unid_counts_unique_days() {
const Ranker = ChromeUtils.importESModule(
);
const sandbox = sinon.createSandbox();
// Fix the clock so "now" is deterministic
const nowMs = Date.UTC(2025, 0, 10); // Jan 10, 2025
const clock = sandbox.useFakeTimers({ now: nowMs });
const us = ms => Math.round(ms * 1000);
const dayMs = 864e5;
const visitsByGuid = {
g1: [
{ visit_date_us: us(nowMs), visit_type: 1 }, // today
{ visit_date_us: us(nowMs - 2 * dayMs), visit_type: 1 }, // 2 days ago
{ visit_date_us: us(nowMs - 2 * dayMs), visit_type: 1 }, // same day as above
],
g2: [
{ visit_date_us: us(nowMs - 7 * dayMs), visit_type: 1 }, // 7 days ago
],
};
const visitCounts = { g1: 3, g2: 1 };
const out = await Ranker.buildFrecencyFeatures(visitsByGuid, visitCounts);
Assert.equal(out.unid.g1, 2, "g1 has visits on 2 unique days");
Assert.equal(out.unid.g2, 1, "g2 has visits on 1 unique day");
clock.restore();
sandbox.restore();
});
add_task(async function test_weightedSampleTopSites_unid_feature() {
const Worker = ChromeUtils.importESModule(
);
const guids = ["a", "b"];
const input = {
features: ["unid", "bias"],
guid: guids,
// explicit scores: a=5 unique days, b=1
unid_scores: { a: 5, b: 1 },
norms: { unid: null },
weights: { unid: 1, bias: 0 },
};
const result = await Worker.weightedSampleTopSites(input);
Assert.ok(result.norms.unid, "norms include 'unid'");
const ua = result.score_map.a.unid;
const ub = result.score_map.b.unid;
Assert.greater(ua, ub, "site with more unique days visited scores higher");
Assert.equal(result.score_map.a.final, ua, "final equals unid score (a)");
Assert.equal(result.score_map.b.final, ub, "final equals unid score (b)");
});
//
// 1) CTR feature: ordering + final uses normalized CTR when weighted
//
add_task(async function test_weightedSampleTopSites_ctr_basic() {
const Worker = ChromeUtils.importESModule(
);
// g1: 9/10 → .909; g2: 1/10 → .1; g3: 0/0 → smoothed to (0+1)/(0+1)=1
// After normalization, g3 should be highest, then g1, then g2.
const input = {
features: ["ctr", "bias"],
guid: ["g1", "g2", "g3"],
clicks: [9, 1, 0],
impressions: [10, 10, 0],
norms: { ctr: null },
weights: { ctr: 1, bias: 0 },
};
const out = await Worker.weightedSampleTopSites(input);
// shape
Assert.ok(out && out.score_map && out.norms, "returns {score_map, norms}");
Assert.ok(out.norms.ctr, "returns ctr norms");
// ordering by normalized ctr (highest to lowest): g3 > g1 > g2
const s1 = out.score_map.g1.ctr;
const s2 = out.score_map.g2.ctr;
const s3 = out.score_map.g3.ctr;
Assert.greater(s3, s1, "g3 (1.0 smoothed) > g1 (~0.909)");
Assert.greater(s1, s2, "g1 (~0.909) > g2 (0.1)");
// finals reflect ctr weight only
Assert.equal(out.score_map.g1.final, s1, "final equals ctr (g1)");
Assert.equal(out.score_map.g2.final, s2, "final equals ctr (g2)");
Assert.equal(out.score_map.g3.final, s3, "final equals ctr (g3)");
});
//
// 2) CTR smoothing edge cases: zero impressions produce finite values
//
add_task(async function test_weightedSampleTopSites_ctr_smoothing_zero_imps() {
const Worker = ChromeUtils.importESModule(
);
// gA: clicks=0, imps=0 → (0+1)/(0+1)=1.0
// gB: clicks=5, imps=0 → (5+1)/(0+1)=6.0 (still finite, larger than 1.0)
const input = {
features: ["ctr"],
guid: ["gA", "gB"],
clicks: [0, 5],
impressions: [0, 0],
norms: { ctr: null },
weights: { ctr: 1 },
};
const out = await Worker.weightedSampleTopSites(input);
const a = out.score_map.gA.ctr;
const b = out.score_map.gB.ctr;
// normalized values are opaque, but ordering must follow raw_ctr (B > A)
Assert.greater(b, a, "higher smoothed CTR should score higher");
// finiteness check
Assert.ok(Number.isFinite(a) && Number.isFinite(b), "scores are finite");
});
//
// 3) CTR running norm: prior norm influences (mean,var) and persists
//
add_task(async function test_weightedSampleTopSites_ctr_with_prior_norm() {
const Worker = ChromeUtils.importESModule(
);
const prior = { beta: 1e-3, mean: 0.3, var: 1.0 }; // seeded running stats
const input = {
features: ["ctr"],
guid: ["x", "y"],
clicks: [3, 0],
impressions: [10, 10], // raw ctr: x=.364, y=.091 → with +1 smoothing: (.3~) but ordering same
norms: { ctr: prior },
weights: { ctr: 1 },
};
const out = await Worker.weightedSampleTopSites(input);
Assert.ok(out.norms.ctr, "ctr norm returned");
Assert.notEqual(out.norms.ctr.mean, prior.mean, "running mean updated");
Assert.greater(out.norms.ctr.var, 0, "variance positive");
const x = out.score_map.x.ctr;
const y = out.score_map.y.ctr;
Assert.greater(x, y, "higher ctr ranks higher under prior normalization");
});
// sticky clicks testing
add_task(async function test_fetchShortcutLastClickPositions_empty() {
const Ranker = ChromeUtils.importESModule(
);
const out = await Ranker.fetchShortcutLastClickPositions(
[],
"smart_shortcuts",
"moz_places"
);
Assert.deepEqual(out, [], "empty guid list → empty result");
});
add_task(
async function test_fetchShortcutLastClickPositions_happy_path_and_numImps() {
const Ranker = ChromeUtils.importESModule(
);
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
const guids = ["g1", "g2", "g3"];
const numImps = 7;
// Stub DB: we just need to return rows in the final SELECT shape:
// [guid, position|null]
const execStub = sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.callsFake(async _sql => {
// Return positions for two guids; one missing (null)
return [
["g1", 3],
["g2", null],
// g3 absent → should default to null in the aligned output
];
});
const out = await Ranker.fetchShortcutLastClickPositions(
guids,
"smart_shortcuts",
"moz_places",
numImps
);
Assert.deepEqual(
out,
[3, null, null],
"aligned array: g1=3, g2=null, g3=null (missing → null)"
);
Assert.greater(execStub.callCount, 0, "DB was queried");
sandbox.restore();
}
);
add_task(async function test_placeGuidsByPositions_basic_and_collisions() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["a", "b", "c", "d"];
// a→1, b→1 (collision), c→3, d→null
const pos = new Map(Object.entries({ a: 1, b: 1, c: 3, d: null }));
const out = Ranker.placeGuidsByPositions(guids, pos);
// Pass #1 (hard place): out[1] = "a", out[3] = "c"
// Pass #2 (fill holes left→right) with [b, d] in original order:
// holes are idx 0 then 2 → place "b" at 0, "d" at 2
Assert.deepEqual(
out,
["b", "a", "d", "c"],
"collision resolved by first-come; others fill holes stably"
);
});
add_task(async function test_placeGuidsByPositions_inferred_size() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["p", "q"];
const pos = new Map(Object.entries({ p: 0, q: null }));
const out = Ranker.placeGuidsByPositions(guids, pos);
Assert.equal(
out.length,
guids.length,
"default size inferred to guids.length"
);
Assert.deepEqual(out, ["p", "q"]);
});
add_task(async function test_fetchShortcutLastClickPositions_empty() {
const Ranker = ChromeUtils.importESModule(
);
// Empty GUID list → empty aligned output; should not query DB.
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
const stub = sandbox
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.resolves([]);
const out = await Ranker.fetchShortcutLastClickPositions(
[],
"smart_shortcuts",
"moz_places",
10
);
Assert.deepEqual(out, [], "empty guid list → empty result");
Assert.equal(stub.callCount, 0, "no DB query for empty input");
sandbox.restore();
});
add_task(async function test_placeGuidsByPositions_empty_inputs() {
const Ranker = ChromeUtils.importESModule(
);
// Empty guids, empty positions
{
const out = Ranker.placeGuidsByPositions([], new Map());
Assert.deepEqual(out, [], "empty → empty");
}
// Empty guids, non-empty positions map (should still produce empty)
{
const pos = new Map(Object.entries({ x: 5, y: 0 }));
const out = Ranker.placeGuidsByPositions([], pos);
Assert.deepEqual(out, [], "no guids to place → empty result");
}
});
add_task(async function test_placeGuidsByPositions_all_null_positions() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["a", "b", "c"];
const pos = new Map(Object.entries({ a: null, b: null, c: null }));
const out = Ranker.placeGuidsByPositions(guids, pos);
Assert.deepEqual(out, guids, "all null → stable original order");
});
add_task(async function test_placeGuidsByPositions_plain_object_positions() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["a", "b", "c"];
// Use a plain object (exercises normalization)
const pos = new Map(Object.entries({ a: 2, b: 0, c: 1 }));
const out = Ranker.placeGuidsByPositions(guids, pos);
Assert.deepEqual(out, ["b", "c", "a"], "plain object map works");
});
add_task(async function test_placeGuidsByPositions_negative_and_noninteger() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["a", "b", "c", "d"];
// a→-1 (ignored), b→1.7 (non-integer → ignored), c→0 (valid), d→null
const pos = new Map(Object.entries({ a: -1, b: 1.7, c: 0, d: null }));
const out = Ranker.placeGuidsByPositions(guids, pos);
// Pass #1 puts c at 0. Others (a,b,d) fill holes in order → [c, a, b, d]
Assert.deepEqual(
out,
["c", "a", "b", "d"],
"negative & non-integer treated as unpositioned"
);
});
add_task(
async function test_placeGuidsByPositions_large_position_goes_lastish() {
const Ranker = ChromeUtils.importESModule(
);
const guids = ["a", "b", "c"];
// If your implementation infers size as max(guids.length, maxPos+1),
// this will create a 6-slot array. The fill pass will leave trailing nulls.
// We at least assert that ordering of real items is stable and includes "a" at its slot.
const pos = new Map(Object.entries({ a: 5, b: null, c: null }));
const out = Ranker.placeGuidsByPositions(guids, pos);
// out length may be > guids.length depending on your impl; assert item order:
const nonNull = out.filter(x => x !== null);
// "a" should still appear once, and b/c follow in original order somewhere after
Assert.ok(nonNull.includes("a"), "contains 'a'");
// The fill order is stable for the unpositioned ones
const idxB = nonNull.indexOf("b");
const idxC = nonNull.indexOf("c");
Assert.greater(
idxC,
idxB,
"unpositioned items stay in input order (b before c)"
);
}
);
add_task(
async function test_applyStickyClicks_preserves_null_and_clamps_negative() {
const Worker = ChromeUtils.importESModule(
);
// positions aligned with guids; pos[1] is null; pos[2] becomes negative after shift
const guids = ["g1", "g2", "g3"];
const positions = [3, null, 1]; // absolute
const numSponsored = 2; // shift by 2 → [1, null, -1] → clamp -1 → 0
const out = Worker.applyStickyClicks(positions, guids, numSponsored);
// After building guid→pos: g1→1, g2→null, g3→0
// Hard place: out[1]=g1, out[0]=g3; fill hole(s) with g2 → [g3, g1, g2]
Assert.deepEqual(
out,
["g3", "g1", "g2"],
"null preserved, negatives clamped to 0"
);
}
);
add_task(
async function test_applyStickyClicks_undefined_numSponsored_defaults_zero() {
const Worker = ChromeUtils.importESModule(
);
const guids = ["g1", "g2"];
const positions = [4, null]; // shift by undefined → treated as 0 in robust version
const out = Worker.applyStickyClicks(positions, guids, undefined);
// guid→pos: g1→4, g2→null → places g1 at 4 (if your placer grows size) or ignores (if not)
// We assert at least the unpositioned stays in order and g1 appears once.
Assert.ok(out.includes("g1") && out.includes("g2"), "both guids present");
}
);
function prefsFor(features, extra = {}) {
// Map every feature in FEATURE_META to a weight; default 0, chosen ones 100.
const baseWeights = {
thom_weight: 0,
frec_weight: 0,
hour_weight: 0,
daily_weight: 0,
bmark_weight: 0,
rece_weight: 0,
freq_weight: 0,
refre_weight: 0,
open_weight: 0,
unid_weight: 0,
ctr_weight: 0,
bias_weight: 100,
};
for (const f of features) {
const key = {
thom: "thom_weight",
frec: "frec_weight",
hour: "hour_weight",
daily: "daily_weight",
bmark: "bmark_weight",
rece: "rece_weight",
freq: "freq_weight",
refre: "refre_weight",
open: "open_weight",
unid: "unid_weight",
ctr: "ctr_weight",
bias: "bias_weight",
}[f];
if (key) {
baseWeights[key] = 100;
}
}
return {
trainhopConfig: {
smartShortcuts: {
features,
// make training a no-op for determinism
eta: 0,
click_bonus: 10,
positive_prior: 1,
negative_prior: 1,
sticky_numimps: 0, // off for matrix tests
...baseWeights,
...extra,
},
},
};
}
function attachLocalWorker(provider, WorkerMod) {
provider._rankShortcutsWorker = {
async post(name, args) {
// name is a string like "weightedSampleTopSites"
// Worker functions are exported with same names.
// Some are standalone, some are exported in the class wrapper too.
const fn = WorkerMod[name] || new WorkerMod.RankShortcutsWorker()[name];
if (typeof fn === "function") {
// if it's a plain function, spread args; if method, pass as method
return Array.isArray(args) ? fn(...args) : fn(args);
}
throw new Error(`No worker function for ${name}`);
},
};
}
function stubDB(sinon, NewTabUtils, nowFixed) {
return sinon
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
.callsFake(async sql => {
const s = String(sql);
// Bookmarks
if (s.includes("FROM moz_bookmarks")) {
return [
["g1", 1],
["g2", 0],
["g3", 0],
];
}
// Visit counts
if (s.includes("COALESCE(p.visit_count")) {
return [
["g1", 20],
["g2", 10],
["g3", 5],
];
}
// Last 10 visits (guid, visit_date_us, visit_type)
if (s.includes("LIMIT 10") && s.includes("ORDER BY vv.visit_date DESC")) {
const us = ms => Math.round(ms * 1000);
const day = 864e5;
return [
["g1", us(nowFixed), 2], // typed now
["g1", us(nowFixed - day), 1], // link
["g2", us(nowFixed - 2 * day), 1],
["g3", us(nowFixed - 10 * day), 1],
];
}
// Daily specific (guid, dow, count)
if (
s.includes("strftime('%w'") &&
s.includes("GROUP BY place_ids.guid")
) {
return [
["g1", 1, 3],
["g2", 1, 1],
];
}
// Daily all (dow, count)
if (s.includes("strftime('%w'") && !s.includes("place_ids.guid")) {
return [
[0, 10],
[1, 20],
[2, 15],
[3, 12],
[4, 8],
[5, 5],
[6, 4],
];
}
// Hourly specific (guid, hour, count)
if (
s.includes("strftime('%H'") &&
s.includes("GROUP BY place_ids.guid")
) {
return [
["g1", 9, 5],
["g2", 10, 1],
];
}
// Hourly all (hour, count)
if (s.includes("strftime('%H'") && !s.includes("place_ids.guid")) {
return Array.from({ length: 24 }, (_, h) => [
h,
h >= 8 && h <= 18 ? 10 : 2,
]);
}
// Shortcut interactions (clicks/imps aggregation over 2 months)
if (
s.includes("moz_newtab_shortcuts_interaction") &&
s.includes("SUM(")
) {
return [
["g1", 5, 10], // clicks, imps
["g2", 1, 10],
["g3", 0, 0],
];
}
// Sticky-clicks helper (only in its dedicated test)
if (s.includes("ROW_NUMBER()") && s.includes("tile_position")) {
// Shape: [guid, position|null]
return [
["g1", 3],
["g2", null],
["g3", null],
];
}
return [];
});
}
add_task(async function test_rankTopSites_feature_matrix() {
const Ranker = ChromeUtils.importESModule(
);
const Worker = ChromeUtils.importESModule(
);
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
);
await NewTabUtils.init();
const sandbox = sinon.createSandbox();
const nowFixed = Date.UTC(2025, 0, 1, 12, 0, 0);
const clock = sandbox.useFakeTimers({ now: nowFixed });
// provider under test
const provider = new Ranker.RankShortcutsProvider();
attachLocalWorker(provider, Worker);
// cache stubs: keep simple but real-ish
const fakeCache = {
weights: {},
init_weights: {},
norms: null,
score_map: { g1: { final: 0 }, g2: { final: 0 }, g3: { final: 0 } },
time_last_update: 0,
};
sandbox.stub(provider.sc_obj, "get").resolves(fakeCache);
sandbox.stub(provider.sc_obj, "set").resolves();
const dbStub = stubDB(sandbox, NewTabUtils, nowFixed);
// input topsites
const topsites = [
{ guid: "g1", url: "https://g1", frecency: 100 },
{ guid: "g2", url: "https://g2", frecency: 10 },
{ guid: "g3", url: "https://g3", frecency: 1 },
{ url: "https://no-guid" }, // should remain last
];
// build feature lists
const ALL = [
"thom",
"frec",
"hour",
"daily",
"bmark",
"rece",
"freq",
"refre",
"unid",
"ctr",
"bias",
];
const singles = ALL.map(f => [f]);
const pairs = [
["frec", "bias"],
["thom", "bias"],
["ctr", "bias"],
["bmark", "bias"],
["rece", "freq"],
];
const triples = [
["frec", "thom", "bias"],
["rece", "freq", "refre"],
];
const cases = [...singles, ...pairs, ...triples];
for (const features of cases) {
const prefs = prefsFor(features, { sticky_numimps: 0 });
const out = await provider.rankTopSites(
topsites,
prefs,
{ isStartup: true },
/*numSponsored*/ 0
);
// basic shape assertions
Assert.ok(Array.isArray(out), `(${features}) returns array`);
Assert.equal(out.length, topsites.length, `(${features}) length stable`);
Assert.equal(
out[out.length - 1].url,
`(${features}) no-guid item is last`
);
// permutation check for guided items
const inGuids = topsites
.filter(x => x.guid)
.map(x => x.guid)
.sort();
const outGuids = out
.filter(x => x.guid)
.map(x => x.guid)
.sort();
Assert.deepEqual(outGuids, inGuids, `(${features}) guids preserved`);
}
Assert.greater(dbStub.callCount, 0, "DB stub used at least once");
clock.restore();
sandbox.restore();
});
add_task(function test_roundNum_precision_and_edge_cases() {
const { roundNum } = ChromeUtils.importESModule(
);
Assert.equal(
roundNum(1.23456789),
1.235,
"roundNum rounds to four significant digits by default"
);
Assert.equal(
roundNum(987.65, 2),
990,
"roundNum respects the requested precision"
);
Assert.equal(
roundNum(5e-7),
0,
"roundNum clamps magnitudes smaller than eps to zero"
);
Assert.equal(
roundNum(-5e-7),
0,
"roundNum clamps small negative magnitudes to zero"
);
Assert.equal(
roundNum(5e-7, 4, 1e-9),
5e-7,
"roundNum uses the provided eps override when keeping small values"
);
Assert.strictEqual(
roundNum(Number.POSITIVE_INFINITY),
Number.POSITIVE_INFINITY,
"roundNum leaves non-finite numbers unchanged"
);
Assert.ok(Number.isNaN(roundNum(NaN)), "roundNum returns NaN for NaN");
const sentinel = { foo: "bar" };
Assert.strictEqual(
roundNum(sentinel),
sentinel,
"roundNum returns non-number inputs unchanged"
);
});