import { describe, expect, it } from "vitest"; import { defaultProfile, rateSource } from "./learning"; import { defaultState, defaultTaste } from "./state"; import { buildTraces } from "./traces"; import type { FeedItem } from "./types"; const item = (over: Partial): FeedItem => ({ uid: "u1", id: "claude_done", source: "Claude", say: "The goblin rests.", tier: "playful", tags: ["useful", "warm"], decision: "notify", time: "12:00", rating: null, ...over, }); describe("buildTraces", () => { const fs = defaultState(); const taste = defaultTaste(); const now = new Date("2026-06-06T12:00:00Z"); it("snapshots event, fairy dials, taste, and per-source bias", () => { let profile = defaultProfile(); profile = rateSource(profile, "Discord", "annoying"); // seed -8 → -20 const [t] = buildTraces([item({ source: "Discord", id: "discord_noise" })], fs, taste, profile, now); expect(t.v).toBe(1); expect(t.ts).toBe("2026-06-06T12:00:00.000Z"); expect(t.event).toMatchObject({ id: "discord_noise", source: "Discord", decision: "notify" }); expect(t.sourceBias).toBe(-20); expect(t.fairy.mischief).toBe(fs.mischief); expect(t.taste).toEqual(taste); }); it("carries channel and outcome when present, null when absent", () => { const outcome = { type: "dismissed" as const, rating: null, latencyMs: 420, visible: true }; const [a, b] = buildTraces( [item({ channel: "toast", outcome }), item({ uid: "u2" })], fs, taste, defaultProfile(), now, ); expect(a.event.channel).toBe("toast"); expect(a.outcome).toEqual(outcome); expect(b.event.channel).toBeNull(); expect(b.outcome).toBeNull(); }); it("includes silent decisions too — ignore/scroll distributions matter for the notify head", () => { const [t] = buildTraces( [item({ decision: "ignore", channel: "feed" })], fs, taste, defaultProfile(), now, ); expect(t.event.decision).toBe("ignore"); expect(t.outcome).toBeNull(); }); });