cerberus-trust-score / tree / test/score.test.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// Table driven tests named after the acceptance items: A1 every tier boundary, A2 an unproven agent is never
// refused, A3 the outside score moves the tier by at most one step. A4 covers determinism and bad input.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { scoreAgent, components, tierForScore, gateFor, TIERS, WEIGHTS, type AgentRecord, type Tier } from '../src/score.js';

// Records built so their own score sits exactly on each side of each boundary.
const GOLD_75: AgentRecord = { accepted: 3, rejected: 0, admission: 'passed', aiax: 'ready', listing_age_days: 180, acp_score: 60 };
const SILVER_74: AgentRecord = { ...GOLD_75, acp_score: 53 };
const SILVER_50: AgentRecord = { accepted: 1, rejected: 0, admission: 'passed', aiax: 'ready', listing_age_days: 0, acp_score: 27 };
const BRONZE_49: AgentRecord = { ...SILVER_50, acp_score: 20 };
const BRONZE_25: AgentRecord = { accepted: 0, admission: 'passed', aiax: 'ready' };
const UNPROVEN_24: AgentRecord = { accepted: 0, admission: 'passed', aiax: 'partial', acp_score: 27 };

test('A1 every tier boundary, table driven', () => {
  const rows: Array<[string, AgentRecord, number, Tier]> = [
    ['gold at 75', GOLD_75, 75, 'gold'],
    ['silver at 74', SILVER_74, 74, 'silver'],
    ['silver at 50', SILVER_50, 50, 'silver'],
    ['bronze at 49', BRONZE_49, 49, 'bronze'],
    ['bronze at 25', BRONZE_25, 25, 'bronze'],
    ['unproven at 24', UNPROVEN_24, 24, 'unproven'],
    ['unproven at 0', {}, 0, 'unproven'],
    ['gold at 100', { accepted: 10, rejected: 0, admission: 'passed', aiax: 'ready', listing_age_days: 400, acp_score: 100 }, 100, 'gold'],
  ];
  for (const [name, rec, score, tier] of rows) {
    const r = scoreAgent(rec);
    assert.equal(r.score, score, name);
    assert.equal(r.tier, tier, name);
  }
  const pure: Array<[number, Tier]> = [[0, 'unproven'], [24, 'unproven'], [25, 'bronze'], [49, 'bronze'], [50, 'silver'], [74, 'silver'], [75, 'gold'], [100, 'gold']];
  for (const [s, t] of pure) assert.equal(tierForScore(s), t, String(s));
  // proof gates: a high score without the proof stays at the tier the proof allows
  const gates: Array<[string, AgentRecord, Tier]> = [
    ['no delivery and no admission', { aiax: 'ready', listing_age_days: 180, acp_score: 100 }, 'unproven'],
    ['admission only', { admission: 'passed', aiax: 'ready', listing_age_days: 180, acp_score: 100 }, 'bronze'],
    ['two accepted', { accepted: 2, admission: 'passed', aiax: 'ready', listing_age_days: 180, acp_score: 100 }, 'silver'],
    ['three accepted', { accepted: 3, admission: 'passed', aiax: 'ready', listing_age_days: 180, acp_score: 100 }, 'gold'],
  ];
  for (const [name, rec, tier] of gates) assert.equal(scoreAgent(rec).tier, tier, name);
  assert.equal(scoreAgent({ aiax: 'ready', listing_age_days: 180, acp_score: 100 }).score, 40);
  assert.equal(Object.values(WEIGHTS).reduce((a, b) => a + b, 0), 100);
});

test('A2 an unproven agent is never refused: it gets the lowest tier, a score and reasons', () => {
  const inputs: unknown[] = [undefined, null, {}, { accepted: -3 }, { accepted: Number.NaN, rejected: 'x' }, { acp_score: 400 }, 'garbage', { admission: 'failed', rejected: 9 }];
  for (const input of inputs) {
    const r = scoreAgent(input as AgentRecord);
    assert.equal(r.tier, 'unproven', JSON.stringify(input));
    assert.ok(Number.isInteger(r.score) && r.score >= 0 && r.score <= 100);
    assert.ok(r.reasons.length > 0);
    assert.ok(r.reasons.some((x) => /not a refusal/.test(x)));
    assert.ok(TIERS.includes(r.tier));
  }
  assert.ok(scoreAgent({ accepted: -3 }).reasons.some((x) => /accepted is not a number/.test(x)));
  assert.ok(scoreAgent({ acp_score: 400 }).reasons.some((x) => /acp_score is not between 0 and 100/.test(x)));
});

test('A3 the outside score moves the tier by at most one step', () => {
  // default 20 percent blend
  const down = scoreAgent(GOLD_75, 0);
  assert.equal(down.score, 60);
  assert.equal(down.tier, 'silver');
  const up = scoreAgent(BRONZE_49, 100);
  assert.equal(up.score, 59);
  assert.equal(up.tier, 'silver');
  assert.ok(up.reasons.some((x) => /moved the tier from bronze to silver/.test(x)));
  // with a heavier blend the move is still capped at one step
  const rows: Array<[AgentRecord, number, Tier, Tier]> = [
    [GOLD_75, 0, 'gold', 'silver'],
    [BRONZE_25, 100, 'bronze', 'bronze'], // admission only: the proof gate holds it at bronze
    [{ accepted: 3, rejected: 9, admission: 'passed', aiax: 'ready' }, 100, 'bronze', 'silver'], // gold by proof, capped at one step
    [SILVER_50, 100, 'silver', 'silver'], // one accepted delivery: the proof gate holds it at silver
    [{ ...GOLD_75 }, 100, 'gold', 'gold'],
  ];
  for (const [rec, outside, before, after] of rows) {
    assert.equal(scoreAgent(rec).tier, before);
    const r = scoreAgent(rec, outside, { outsideWeight: 1 });
    assert.equal(r.tier, after, `${before} with outside ${outside}`);
    assert.ok(Math.abs(TIERS.indexOf(r.tier) - TIERS.indexOf(before)) <= 1);
  }
  assert.ok(scoreAgent(GOLD_75, 0, { outsideWeight: 1 }).reasons.some((x) => /by one step only, from gold to silver/.test(x)));
  // an exhaustive sweep: no record and no outside score ever moves the tier more than one step
  for (const rec of [GOLD_75, SILVER_74, SILVER_50, BRONZE_49, BRONZE_25, UNPROVEN_24, {}]) {
    const own = scoreAgent(rec).tier;
    for (let o = 0; o <= 100; o += 5) for (const w of [0, 0.2, 0.5, 1]) {
      const t = scoreAgent(rec, o, { outsideWeight: w }).tier;
      assert.ok(Math.abs(TIERS.indexOf(t) - TIERS.indexOf(own)) <= 1, `${JSON.stringify(rec)} ${o} ${w}`);
    }
  }
  // a bad outside score is ignored with a reason
  const bad = scoreAgent(GOLD_75, 250);
  assert.equal(bad.score, 75);
  assert.ok(bad.reasons.some((x) => /outside score is not between 0 and 100/.test(x)));
});

test('A4 deterministic, pure and explained', () => {
  const rec = { ...SILVER_74 };
  const a = scoreAgent(rec, 40);
  const b = scoreAgent(rec, 40);
  assert.deepEqual(a, b);
  assert.deepEqual(rec, SILVER_74); // the input is not changed
  assert.equal(a.reasons.length >= 5, true);
  for (const r of a.reasons) assert.doesNotMatch(r, /[\u2013\u2014]/);
  assert.equal(components(GOLD_75).record, 26);
  assert.equal(gateFor({ accepted: 0, admissionResult: 'failed' }), 'unproven');
});

test('A1 listing age is capped at 180 days, and age alone never lifts an agent out of unproven', () => {
  assert.equal(components({ listing_age_days: 180 }).ageScore, WEIGHTS.age);
  assert.equal(components({ listing_age_days: 4000 }).ageScore, WEIGHTS.age);
  const old = scoreAgent({ listing_age_days: 4000, aiax: 'ready', acp_score: 100 });
  assert.equal(old.score, WEIGHTS.age + WEIGHTS.aiax + WEIGHTS.acp);
  assert.equal(old.tier, 'unproven');
  assert.ok(old.reasons.some((x) => /tier held at unproven/.test(x)));
});

agentgate-source: code mirror on Cloudflare Pages. All repos