// 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