94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
// Trust model v2 — pure math tests (no DB required)
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
import { describe, expect, it } from "vitest";
|
|||
|
|
import {
|
|||
|
|
computeCleanTrustGain,
|
|||
|
|
computeInfractionPenalty,
|
|||
|
|
INFRACTION_FLOORS,
|
|||
|
|
INFRACTION_PENALTIES,
|
|||
|
|
TRUST_DEFAULTS,
|
|||
|
|
} from "../src/modules/ai-moderation/userReputationStore.js";
|
|||
|
|
|
|||
|
|
describe("computeCleanTrustGain — trust CAN rise", () => {
|
|||
|
|
it("grants +1 every CLEAN_MESSAGES_PER_POINT clean messages", () => {
|
|||
|
|
const before = computeCleanTrustGain(14);
|
|||
|
|
expect(before.newStreak).toBe(15);
|
|||
|
|
expect(before.trustGain).toBe(1);
|
|||
|
|
|
|||
|
|
const after = computeCleanTrustGain(15);
|
|||
|
|
expect(after.newStreak).toBe(16);
|
|||
|
|
expect(after.trustGain).toBe(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("keeps compounding past the threshold (no wasted progress)", () => {
|
|||
|
|
expect(computeCleanTrustGain(29).trustGain).toBe(1);
|
|||
|
|
expect(computeCleanTrustGain(44).trustGain).toBe(1);
|
|||
|
|
// 45 clean messages from a fresh start → 3 points of recovery
|
|||
|
|
let gain = 0;
|
|||
|
|
let streak = 0;
|
|||
|
|
for (let i = 0; i < 45; i++) {
|
|||
|
|
const r = computeCleanTrustGain(streak);
|
|||
|
|
streak = r.newStreak;
|
|||
|
|
gain += r.trustGain;
|
|||
|
|
}
|
|||
|
|
expect(gain).toBe(3);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("computeInfractionPenalty — fair and escalating", () => {
|
|||
|
|
const NOW = Date.now();
|
|||
|
|
|
|||
|
|
it("applies base penalty for a repeat offender outside the window", () => {
|
|||
|
|
const r = computeInfractionPenalty({
|
|||
|
|
totalInfractions: 3,
|
|||
|
|
lastInfractionAt: NOW - TRUST_DEFAULTS.REPEAT_OFFENSE_WINDOW_MS - 1000,
|
|||
|
|
severity: "medium",
|
|||
|
|
now: NOW,
|
|||
|
|
});
|
|||
|
|
expect(r.penalty).toBe(INFRACTION_PENALTIES.medium); // 6
|
|||
|
|
expect(r.appliedRules.firstOffense).toBe(false);
|
|||
|
|
expect(r.appliedRules.repeatEscalation).toBe(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("halves the penalty for a first offense (leniency)", () => {
|
|||
|
|
const r = computeInfractionPenalty({
|
|||
|
|
totalInfractions: 0,
|
|||
|
|
lastInfractionAt: null,
|
|||
|
|
severity: "high",
|
|||
|
|
now: NOW,
|
|||
|
|
});
|
|||
|
|
expect(r.penalty).toBe(Math.ceil(INFRACTION_PENALTIES.high / 2)); // 6
|
|||
|
|
expect(r.appliedRules.firstOffense).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("escalates ×1.5 for a repeat offense within 7 days", () => {
|
|||
|
|
const r = computeInfractionPenalty({
|
|||
|
|
totalInfractions: 2,
|
|||
|
|
lastInfractionAt: NOW - 60 * 60 * 1000, // 1h ago
|
|||
|
|
severity: "medium",
|
|||
|
|
now: NOW,
|
|||
|
|
});
|
|||
|
|
expect(r.penalty).toBe(Math.ceil(INFRACTION_PENALTIES.medium * 1.5)); // 9
|
|||
|
|
expect(r.appliedRules.repeatEscalation).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("critical first offense still hurts but is halved", () => {
|
|||
|
|
const r = computeInfractionPenalty({
|
|||
|
|
totalInfractions: 0,
|
|||
|
|
lastInfractionAt: null,
|
|||
|
|
severity: "critical",
|
|||
|
|
now: NOW,
|
|||
|
|
});
|
|||
|
|
expect(r.penalty).toBe(Math.ceil(INFRACTION_PENALTIES.critical / 2)); // 13
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("severity floors prevent minor offenses from zeroing a user", () => {
|
|||
|
|
expect(INFRACTION_FLOORS.low).toBeGreaterThan(0);
|
|||
|
|
expect(INFRACTION_FLOORS.medium).toBeGreaterThan(0);
|
|||
|
|
// high/critical can still reach zero — severe behavior has consequences
|
|||
|
|
expect(INFRACTION_FLOORS.high).toBe(0);
|
|||
|
|
expect(INFRACTION_FLOORS.critical).toBe(0);
|
|||
|
|
});
|
|||
|
|
});
|