← back to Rentv Adintel
test/scoring.test.js
128 lines
'use strict';
/**
* Tests for the Advertising Opportunity Score (spec §13).
* Pure functions only — no DB, no network. Runs under the built-in
* `node --test` runner (no jest/vitest/mocha).
*/
const test = require('node:test');
const assert = require('node:assert');
const {
calculateAdvertiserOpportunityScore,
explainScore,
DEFAULT_WEIGHTS,
clampScore,
FACTOR_KEYS,
} = require('../lib/scoring');
// The ten factors the §13 formula weights.
const FULL_INPUT = {
verifiedAdvertising: 100,
verifiedConferenceSpendSignal: 100,
recency: 100,
repeatActivity: 100,
californiaFit: 100,
arizonaFit: 100,
categoryFit: 100,
rentvAudienceFit: 100,
contactCompleteness: 100,
evidenceQuality: 100,
};
test('all-100 input scores exactly 100 (weights sum to 1.0)', () => {
assert.strictEqual(calculateAdvertiserOpportunityScore(FULL_INPUT), 100);
});
test('all-zero input scores exactly 0', () => {
const zeroed = Object.fromEntries(FACTOR_KEYS.map((k) => [k, 0]));
assert.strictEqual(calculateAdvertiserOpportunityScore(zeroed), 0);
});
test('known input maps to the known §13 output', () => {
// Verify the exact §13 spec formula by computing it independently.
const input = {
verifiedAdvertising: 80, // *0.22 = 17.6
verifiedConferenceSpendSignal: 60, // *0.15 = 9.0
recency: 90, // *0.12 = 10.8
repeatActivity: 50, // *0.10 = 5.0
californiaFit: 100, // *0.10 = 10.0
arizonaFit: 0, // *0.05 = 0.0
categoryFit: 70, // *0.08 = 5.6
rentvAudienceFit: 40, // *0.08 = 3.2
contactCompleteness: 100, // *0.04 = 4.0
evidenceQuality: 90, // *0.06 = 5.4
};
// sum = 70.6 → round → 71
assert.strictEqual(calculateAdvertiserOpportunityScore(input), 71);
});
test('DEFAULT_WEIGHTS sum to ~1.0', () => {
const sum = Object.values(DEFAULT_WEIGHTS).reduce((a, b) => a + b, 0);
assert.ok(Math.abs(sum - 1.0) < 1e-9, `weights sum to ${sum}, expected 1.0`);
});
test('FACTOR_KEYS covers every weighted factor (and nothing else)', () => {
assert.strictEqual(FACTOR_KEYS.length, 10);
assert.deepStrictEqual([...FACTOR_KEYS].sort(), Object.keys(DEFAULT_WEIGHTS).sort());
});
test('clampScore bounds values into [0,100]', () => {
assert.strictEqual(clampScore(-50), 0);
assert.strictEqual(clampScore(0), 0);
assert.strictEqual(clampScore(50), 50);
assert.strictEqual(clampScore(100), 100);
assert.strictEqual(clampScore(150), 100);
assert.strictEqual(clampScore(NaN), 0);
assert.strictEqual(clampScore(undefined), 0);
});
test('out-of-range factor inputs are clamped before weighting', () => {
const over = Object.fromEntries(FACTOR_KEYS.map((k) => [k, 999]));
assert.strictEqual(calculateAdvertiserOpportunityScore(over), 100);
const under = Object.fromEntries(FACTOR_KEYS.map((k) => [k, -999]));
assert.strictEqual(calculateAdvertiserOpportunityScore(under), 0);
});
test('admin weight overrides are applied', () => {
// Put ALL weight on one factor; only that factor should matter.
const weights = Object.fromEntries(FACTOR_KEYS.map((k) => [k, 0]));
weights.recency = 1.0;
const input = Object.fromEntries(FACTOR_KEYS.map((k) => [k, 0]));
input.recency = 77;
assert.strictEqual(calculateAdvertiserOpportunityScore(input, weights), 77);
});
test('explainScore returns a per-factor contribution for "Show Me Why"', () => {
const out = explainScore(FULL_INPUT);
assert.strictEqual(out.score, 100);
assert.strictEqual(out.factors.length, 10);
for (const f of out.factors) {
assert.ok(FACTOR_KEYS.includes(f.factor));
assert.strictEqual(typeof f.value, 'number');
assert.strictEqual(typeof f.weight, 'number');
assert.strictEqual(typeof f.contribution, 'number');
}
});
test('explainScore contributions sum to the score (±1 for rounding)', () => {
const input = {
verifiedAdvertising: 83,
verifiedConferenceSpendSignal: 41,
recency: 77,
repeatActivity: 12,
californiaFit: 95,
arizonaFit: 8,
categoryFit: 66,
rentvAudienceFit: 54,
contactCompleteness: 30,
evidenceQuality: 71,
};
const out = explainScore(input);
const sum = out.factors.reduce((a, f) => a + f.contribution, 0);
assert.ok(
Math.abs(sum - out.score) <= 1,
`contributions ${sum} should be within 1 of score ${out.score}`
);
});