197 lines
5.2 KiB
TypeScript
197 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { applyDecay, computeTrust, householdTrustScore, worstTrustState } from "../src/trust.js";
|
|
|
|
const PINNED = new Date("2026-08-06T12:00:00.000Z");
|
|
|
|
describe("applyDecay", () => {
|
|
it("returns original confidence when elapsed days is zero", () => {
|
|
expect(applyDecay(0.8, 0)).toBe(0.8);
|
|
});
|
|
|
|
it("halves confidence after halfLifeDays", () => {
|
|
expect(applyDecay(1.0, 7, { halfLifeDays: 7, staleAfterDays: 30 })).toBeCloseTo(0.5, 3);
|
|
});
|
|
|
|
it("returns zero when elapsed days reaches staleAfterDays", () => {
|
|
expect(applyDecay(0.8, 30, { halfLifeDays: 7, staleAfterDays: 30 })).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("computeTrust", () => {
|
|
it("returns trusted for a fresh high-confidence item", () => {
|
|
const result = computeTrust(
|
|
{
|
|
confidence: 0.8,
|
|
verifiedByUser: false,
|
|
lastVerifiedAt: null,
|
|
quantity: 2,
|
|
updatedAt: PINNED,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.state).toBe("trusted");
|
|
expect(result.score).toBeGreaterThanOrEqual(60);
|
|
});
|
|
|
|
it("decays when last verification is old", () => {
|
|
const sevenDaysAgo = new Date(PINNED.getTime() - 7 * 86_400_000);
|
|
const result = computeTrust(
|
|
{
|
|
confidence: 0.8,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: sevenDaysAgo,
|
|
quantity: 1,
|
|
updatedAt: sevenDaysAgo,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.state).toBe("decaying");
|
|
});
|
|
|
|
it("is stale for very old unverified low-confidence items", () => {
|
|
const sixtyDaysAgo = new Date(PINNED.getTime() - 60 * 86_400_000);
|
|
const result = computeTrust(
|
|
{
|
|
confidence: 0.2,
|
|
verifiedByUser: false,
|
|
lastVerifiedAt: null,
|
|
quantity: 1,
|
|
updatedAt: sixtyDaysAgo,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.state).toBe("stale");
|
|
});
|
|
|
|
it("is trusted when user verified recently", () => {
|
|
const result = computeTrust(
|
|
{
|
|
confidence: 0.7,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: PINNED,
|
|
quantity: 1,
|
|
updatedAt: PINNED,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.state).toBe("trusted");
|
|
expect(result.score).toBeGreaterThanOrEqual(70);
|
|
});
|
|
|
|
it("uses custom decay profile to accelerate staleness", () => {
|
|
const tenDaysAgo = new Date(PINNED.getTime() - 10 * 86_400_000);
|
|
const fast = { halfLifeDays: 3, staleAfterDays: 10 };
|
|
const result = computeTrust(
|
|
{
|
|
confidence: 0.9,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: tenDaysAgo,
|
|
quantity: 1,
|
|
updatedAt: tenDaysAgo,
|
|
},
|
|
PINNED,
|
|
fast,
|
|
);
|
|
expect(result.state).toBe("stale");
|
|
});
|
|
});
|
|
|
|
describe("householdTrustScore", () => {
|
|
it("returns 100 / up_to_date for empty inventory", () => {
|
|
const result = householdTrustScore(
|
|
{ items: [], correctionCount30d: 0, transactionCount30d: 0 },
|
|
PINNED,
|
|
);
|
|
expect(result.score).toBe(100);
|
|
expect(result.status).toBe("up_to_date");
|
|
});
|
|
|
|
it("scores high when all items are verified and recent", () => {
|
|
const result = householdTrustScore(
|
|
{
|
|
items: [
|
|
{
|
|
confidence: 1,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: PINNED,
|
|
quantity: 2,
|
|
updatedAt: PINNED,
|
|
},
|
|
{
|
|
confidence: 1,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: PINNED,
|
|
quantity: 3,
|
|
updatedAt: PINNED,
|
|
},
|
|
],
|
|
correctionCount30d: 0,
|
|
transactionCount30d: 0,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.score).toBeGreaterThanOrEqual(80);
|
|
expect(result.status).toBe("up_to_date");
|
|
});
|
|
|
|
it("drops to uncertain when items are old and unverified", () => {
|
|
const old = new Date(PINNED.getTime() - 60 * 86_400_000);
|
|
const result = householdTrustScore(
|
|
{
|
|
items: [
|
|
{
|
|
confidence: 0.3,
|
|
verifiedByUser: false,
|
|
lastVerifiedAt: null,
|
|
quantity: 0.1,
|
|
updatedAt: old,
|
|
},
|
|
],
|
|
correctionCount30d: 5,
|
|
transactionCount30d: 10,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.status).toBe("uncertain");
|
|
expect(result.score).toBeLessThan(50);
|
|
});
|
|
|
|
it("flags needs_check for partly verified household", () => {
|
|
const weekAgo = new Date(PINNED.getTime() - 7 * 86_400_000);
|
|
const result = householdTrustScore(
|
|
{
|
|
items: [
|
|
{
|
|
confidence: 0.8,
|
|
verifiedByUser: true,
|
|
lastVerifiedAt: weekAgo,
|
|
quantity: 1,
|
|
updatedAt: weekAgo,
|
|
},
|
|
{
|
|
confidence: 0.5,
|
|
verifiedByUser: false,
|
|
lastVerifiedAt: null,
|
|
quantity: 1,
|
|
updatedAt: weekAgo,
|
|
},
|
|
],
|
|
correctionCount30d: 1,
|
|
transactionCount30d: 5,
|
|
},
|
|
PINNED,
|
|
);
|
|
expect(result.status).toBe("needs_check");
|
|
});
|
|
});
|
|
|
|
describe("worstTrustState", () => {
|
|
it("returns stale if present", () => {
|
|
expect(worstTrustState(["trusted", "stale", "decaying"])).toBe("stale");
|
|
});
|
|
|
|
it("falls back to unverified", () => {
|
|
expect(worstTrustState(["trusted", "unverified"])).toBe("unverified");
|
|
});
|
|
});
|