// Phase 3 tests — ACT-R scoring and re-ranking. // // Run: node test/test_actr.js // Pass criteria: every assertion line must print PASS. Exits 1 if any FAIL. import { activation, minMaxNormalize, rerank, ACTR_DEFAULTS } from "../src/actr.js"; let passed = 0; let failed = 0; function assert(name, ok, extra = "") { if (ok) { passed++; console.log(`PASS ${name}`); } else { failed++; console.log(`FAIL ${name}${extra ? ` (${extra})` : ""}`); } } function approx(a, b, tol = 1e-3) { return Math.abs(a - b) <= tol; } // ---- activation() formula tests ---- { // age=1 → base = -d·ln(1) = 0 const a = activation({ ageDays: 1, frequency: 0, confidence: 1, d: 0.5, beta: 1, epsilon: 1 }); assert("activation: age=1, no freq/conf bonus → 1.0", approx(a, 1.0), `got ${a}`); } { // age=30 semantic-like: base = -0.5*ln(30) ≈ -1.701 // freq=0 → 0; conf=1.0 → 1.0 // total ≈ -1.701 + 0 + 1.0 = -0.701 const a = activation({ ageDays: 30, frequency: 0, confidence: 1, d: 0.5, beta: 1, epsilon: 1 }); assert("activation: age=30 conf=1 → ≈-0.701", approx(a, -0.701, 0.01), `got ${a}`); } { // Higher freq → higher activation const a0 = activation({ ageDays: 10, frequency: 0, confidence: 1 }); const a10 = activation({ ageDays: 10, frequency: 10, confidence: 1 }); const a100 = activation({ ageDays: 10, frequency: 100, confidence: 1 }); assert("activation: freq is monotonic", a10 > a0 && a100 > a10, `a0=${a0} a10=${a10} a100=${a100}`); } { // Higher confidence → higher activation const low = activation({ ageDays: 10, frequency: 5, confidence: 0.3 }); const mid = activation({ ageDays: 10, frequency: 5, confidence: 0.6 }); const high = activation({ ageDays: 10, frequency: 5, confidence: 1.0 }); assert("activation: confidence is monotonic", low < mid && mid < high, `${low} < ${mid} < ${high}`); } { // Older → lower activation (forgetting) const fresh = activation({ ageDays: 1, frequency: 5, confidence: 1 }); const old = activation({ ageDays: 365, frequency: 5, confidence: 1 }); assert("activation: older is lower (forgetting)", fresh > old, `fresh=${fresh} old=${old}`); } { // Edge case: ageDays=0 (just-modified record) → falls back to 1 const a = activation({ ageDays: 0, frequency: 0, confidence: 1 }); assert("activation: age=0 falls back to 1 (no -Infinity)", Number.isFinite(a), `got ${a}`); } { // Edge case: missing fields use defaults const a = activation({}); assert("activation: empty args → defaults are sane", Number.isFinite(a), `got ${a}`); } { // Edge case: negative confidence clamped to 0 (so eTerm=0; base term // can still be negative for old records, which is correct ACT-R behavior) const a = activation({ ageDays: 10, frequency: 0, confidence: -0.5 }); const baseline = activation({ ageDays: 10, frequency: 0, confidence: 0 }); assert("activation: negative confidence clamps (matches confidence=0)", approx(a, baseline), `a=${a} baseline=${baseline}`); } { // Edge case: huge frequency still works const a = activation({ ageDays: 10, frequency: 1000000, confidence: 0.5 }); assert("activation: huge freq is finite", Number.isFinite(a), `got ${a}`); } // ---- minMaxNormalize() tests ---- { const out = minMaxNormalize([1, 2, 3, 4, 5]); assert("minMaxNormalize: 5 values → [0, 0.25, 0.5, 0.75, 1]", approx(out[0], 0) && approx(out[1], 0.25) && approx(out[2], 0.5) && approx(out[3], 0.75) && approx(out[4], 1), `got ${JSON.stringify(out)}`); } { const out = minMaxNormalize([5, 5, 5, 5]); assert("minMaxNormalize: all-equal → all 0.5", out.every((v) => approx(v, 0.5)), `got ${JSON.stringify(out)}`); } { assert("minMaxNormalize: empty → empty", minMaxNormalize([]).length === 0); } { assert("minMaxNormalize: non-array → empty", minMaxNormalize(null).length === 0 && minMaxNormalize(undefined).length === 0); } // ---- rerank() tests ---- { // 3 candidates with different ages/freqs. Use a stub lookupMeta. const candidates = [ { urn: "u1", score: 0.5, byChannel: { ump: { rank: 1 } } }, { urn: "u2", score: 0.4, byChannel: { ump: { rank: 2 } } }, { urn: "u3", score: 0.3, byChannel: { ump: { rank: 3 } } }, ]; const metas = { u1: { ageDays: 100, frequency: 0, confidence: 0.3 }, // old, no freq, low conf u2: { ageDays: 1, frequency: 50, confidence: 1.0 }, // fresh, popular, fully encoded u3: { ageDays: 5, frequency: 5, confidence: 0.7 }, }; const lookupMeta = async (urn) => metas[urn] || { ageDays: 1, frequency: 0, confidence: 1 }; const ranked = await rerank(candidates, lookupMeta, { alpha: 0.5 }); assert("rerank: returns same count", ranked.length === 3, `got ${ranked.length}`); assert("rerank: assigns final_rank 1..N", ranked[0].final_rank === 1 && ranked[1].final_rank === 2 && ranked[2].final_rank === 3); assert("rerank: fresh+popular+confident beats old+rare (with alpha=0.5)", ranked[0].urn === "u2", `top is ${ranked[0].urn}`); assert("rerank: includes actr_score and final_score", typeof ranked[0].actr_score === "number" && typeof ranked[0].final_score === "number"); } { // alpha=0 → pure RRF (no reordering) const candidates = [ { urn: "u1", score: 0.5 }, { urn: "u2", score: 0.4 }, ]; const lookupMeta = async () => ({ ageDays: 1000, frequency: 0, confidence: 0 }); const ranked = await rerank(candidates, lookupMeta, { alpha: 0 }); assert("rerank: alpha=0 keeps RRF order", ranked[0].urn === "u1" && ranked[1].urn === "u2", `got ${ranked.map((r) => r.urn).join(",")}`); } { // alpha=1 → pure ACT-R (order may flip) const candidates = [ { urn: "u1", score: 0.5 }, // will have low ACT-R (old, no freq, low conf) { urn: "u2", score: 0.4 }, // will have high ACT-R (fresh, popular, confident) ]; const lookupMeta = async (urn) => { if (urn === "u1") return { ageDays: 1000, frequency: 0, confidence: 0.1 }; return { ageDays: 1, frequency: 100, confidence: 1 }; }; const ranked = await rerank(candidates, lookupMeta, { alpha: 1 }); assert("rerank: alpha=1 reverses based on ACT-R alone", ranked[0].urn === "u2", `got ${ranked.map((r) => r.urn).join(",")}`); } { // Empty input const ranked = await rerank([], async () => ({})); assert("rerank: empty input → empty output", ranked.length === 0); } { // lookupMeta throws → gracefully skipped (uses defaults) const candidates = [{ urn: "u1", score: 0.5 }]; const ranked = await rerank(candidates, async () => { throw new Error("mock failure"); }); assert("rerank: lookupMeta throw → still returns candidates", ranked.length === 1); assert("rerank: lookupMeta throw → uses default metadata", typeof ranked[0].actr_score === "number"); } { // RRF tie-break: when final_score ties, higher RRF wins const candidates = [ { urn: "high_rrf_low_actr", score: 0.9 }, { urn: "low_rrf_high_actr", score: 0.5 }, ]; // Both have same ACT-R → final_score from rrf norm = [1.0, 0.0] // With alpha=0, final_score == rrf_norm → high_rrf wins const lookupMeta = async () => ({ ageDays: 10, frequency: 10, confidence: 0.5 }); const ranked = await rerank(candidates, lookupMeta, { alpha: 0 }); assert("rerank: ties broken by RRF score desc", ranked[0].urn === "high_rrf_low_actr", `got ${ranked.map((r) => r.urn).join(",")}`); } // ---- defaults ---- { assert("ACTR_DEFAULTS: alpha=0.3", ACTR_DEFAULTS.alpha === 0.3); assert("ACTR_DEFAULTS: d=0.5", ACTR_DEFAULTS.d === 0.5); assert("ACTR_DEFAULTS: beta=1.0", ACTR_DEFAULTS.beta === 1.0); assert("ACTR_DEFAULTS: epsilon=1.0", ACTR_DEFAULTS.epsilon === 1.0); } console.log(`\n${passed}/${passed + failed} passed`); process.exit(failed > 0 ? 1 : 0);