import { describe, expect, it } from 'vitest' import { EVIL_METER_INITIAL_SPREAD, EVIL_METER_INITIAL_VALUE, applyTell, computeEvilMeter, evilMeterLabel, } from './evilMeter' describe('computeEvilMeter — idle / empty history', () => { it('starts at the wide, uncertain midpoint with no tells', () => { const meter = computeEvilMeter([]) expect(meter.value).toBe(EVIL_METER_INITIAL_VALUE) expect(meter.spread).toBe(EVIL_METER_INITIAL_SPREAD) expect(meter.certain).toBe(false) expect(meter.sampleCount).toBe(0) }) it('is unaffected by a null/undefined ritualComplete', () => { expect(computeEvilMeter([], null)).toEqual(computeEvilMeter([])) expect(computeEvilMeter([], undefined)).toEqual(computeEvilMeter([])) }) }) describe('computeEvilMeter — narrowing over a sequence of tells', () => { const tells = [ { text: 'the entity avoided a direct question' }, { text: 'a cold draft crossed the room' }, { text: 'it answered warmly, without hesitation' }, { text: 'the static thickened around your voice' }, { text: 'it lingered on your name' }, ] it('spread narrows monotonically (never widens) as tells accumulate', () => { let spreads: number[] = [] for (let n = 0; n <= tells.length; n++) { spreads.push(computeEvilMeter(tells.slice(0, n)).spread) } for (let i = 1; i < spreads.length; i++) { expect(spreads[i]).toBeLessThanOrEqual(spreads[i - 1]) } // strictly narrower after at least one tell expect(spreads[1]).toBeLessThan(spreads[0]) }) it('spread approaches but never drops below the narrowing floor', () => { const many = Array.from({ length: 200 }, (_, i) => ({ text: `tell number ${i}` })) const meter = computeEvilMeter(many) expect(meter.spread).toBeGreaterThan(0) expect(meter.spread).toBeLessThan(EVIL_METER_INITIAL_SPREAD) }) it('value stays within [0, 1] no matter how many tells arrive', () => { const many = Array.from({ length: 500 }, (_, i) => ({ text: `wildly evil tell ${i}` })) const meter = computeEvilMeter(many) expect(meter.value).toBeGreaterThanOrEqual(0) expect(meter.value).toBeLessThanOrEqual(1) }) it('is a pure function: same tell sequence always yields the same belief', () => { const a = computeEvilMeter(tells) const b = computeEvilMeter(tells.map((t) => ({ text: t.text }))) expect(a).toEqual(b) }) it('is sensitive to tell order (folds sequentially, not as a set)', () => { const forward = computeEvilMeter(tells) const reversed = computeEvilMeter([...tells].reverse()) expect(forward).not.toEqual(reversed) }) it('never reports certain from tells alone, however many arrive', () => { const many = Array.from({ length: 1000 }, (_, i) => ({ text: `tell ${i}` })) expect(computeEvilMeter(many).certain).toBe(false) }) it('sampleCount tracks the tell history length', () => { expect(computeEvilMeter(tells).sampleCount).toBe(tells.length) }) }) describe('applyTell — single-step narrowing primitive', () => { it('always narrows spread by the fixed decay factor', () => { const before = { value: 0.5, spread: 0.5 } const after = applyTell(before, { text: 'anything' }) expect(after.spread).toBeCloseTo(before.spread * 0.82, 5) }) it('is deterministic for a given text', () => { const a = applyTell({ value: 0.5, spread: 0.5 }, { text: 'the shadows lengthened' }) const b = applyTell({ value: 0.5, spread: 0.5 }, { text: 'the shadows lengthened' }) expect(a).toEqual(b) }) it('different text can nudge in different directions', () => { const base = { value: 0.5, spread: 0.5 } const results = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta'].map( (text) => applyTell(base, { text }).value, ) const above = results.some((v) => v > 0.5) const below = results.some((v) => v < 0.5) expect(above && below).toBe(true) }) }) describe('computeEvilMeter — ritual_complete override/snap', () => { const tells = [ { text: 'the entity avoided a direct question' }, { text: 'a cold draft crossed the room' }, ] it('snaps to the revealed alignment on a successful ritual, ignoring tell history', () => { const meter = computeEvilMeter(tells, { success: true, revealed: { alignment: 0.9 } }) expect(meter.value).toBe(0.9) expect(meter.spread).toBe(0) expect(meter.certain).toBe(true) }) it('snaps to a malevolent reading too (alignment near 0)', () => { const meter = computeEvilMeter(tells, { success: true, revealed: { alignment: 0.05 } }) expect(meter.value).toBe(0.05) expect(meter.certain).toBe(true) }) it('clamps an out-of-range revealed alignment defensively', () => { const meter = computeEvilMeter(tells, { success: true, revealed: { alignment: 1.4 } }) expect(meter.value).toBe(1) }) it('does NOT snap on a failed ritual — falls back to the tell-driven belief', () => { const withRitual = computeEvilMeter(tells, { success: false, revealed: null }) const withoutRitual = computeEvilMeter(tells) expect(withRitual).toEqual(withoutRitual) expect(withRitual.certain).toBe(false) }) it('sampleCount still reflects the tell history even when snapped', () => { const meter = computeEvilMeter(tells, { success: true, revealed: { alignment: 0.5 } }) expect(meter.sampleCount).toBe(tells.length) }) }) describe('evilMeterLabel', () => { it('reads "unknown" for the empty/idle state', () => { expect(evilMeterLabel(computeEvilMeter([]))).toBe('unknown') }) it('reads a confirmed label once certain', () => { expect( evilMeterLabel(computeEvilMeter([], { success: true, revealed: { alignment: 0.9 } })), ).toBe('confirmed benevolent') expect( evilMeterLabel(computeEvilMeter([], { success: true, revealed: { alignment: 0.1 } })), ).toBe('confirmed malevolent') }) it('reads a leaning label once tells push the value past the threshold', () => { // Hand-pick a value via the snap path (deterministic) to exercise the boundary text. expect( evilMeterLabel({ value: 0.7, spread: 0.2, certain: false, sampleCount: 3 }), ).toBe('leaning benevolent') expect( evilMeterLabel({ value: 0.2, spread: 0.2, certain: false, sampleCount: 3 }), ).toBe('leaning malevolent') expect( evilMeterLabel({ value: 0.5, spread: 0.2, certain: false, sampleCount: 3 }), ).toBe('unreadable') }) })