118 lines
5.1 KiB
TypeScript
118 lines
5.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import { applyHysteresis } from '@/processors/pii-extractor/relationship-kind';
|
|
import type { CurrentKindState } from '@/processors/pii-extractor/relationship-kind';
|
|
|
|
describe('applyHysteresis', () => {
|
|
// -----------------------------------------------------------------------
|
|
// No current kind — always update
|
|
// -----------------------------------------------------------------------
|
|
|
|
test('no current kind → update immediately', () => {
|
|
const state: CurrentKindState = { kind: null, confidence: 0, challengerKind: null, streak: 0 };
|
|
const result = applyHysteresis(state, 'client', 0.7);
|
|
expect(result.shouldUpdate).toBe(true);
|
|
expect(result.nextKind).toBe('client');
|
|
expect(result.nextConfidence).toBe(0.7);
|
|
expect(result.nextStreak).toBe(1);
|
|
expect(result.nextChallengerKind).toBe('client');
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Confidence gap rule: new > current + 0.15 → update
|
|
// -----------------------------------------------------------------------
|
|
|
|
test('new confidence > current + 0.15 → update', () => {
|
|
const state: CurrentKindState = { kind: 'prospect', confidence: 0.6, challengerKind: 'prospect', streak: 1 };
|
|
const result = applyHysteresis(state, 'client', 0.76);
|
|
expect(result.shouldUpdate).toBe(true);
|
|
expect(result.nextKind).toBe('client');
|
|
expect(result.nextConfidence).toBe(0.76);
|
|
});
|
|
|
|
test('new confidence exactly at current + 0.15 boundary → does NOT update', () => {
|
|
// Must be strictly greater than current + 0.15
|
|
const state: CurrentKindState = { kind: 'prospect', confidence: 0.6, challengerKind: 'prospect', streak: 1 };
|
|
const result = applyHysteresis(state, 'client', 0.75);
|
|
expect(result.shouldUpdate).toBe(false);
|
|
expect(result.nextKind).toBe('prospect');
|
|
});
|
|
|
|
test('single low-confidence extraction of different kind → no flip', () => {
|
|
const state: CurrentKindState = { kind: 'client', confidence: 0.8, challengerKind: 'client', streak: 2 };
|
|
const result = applyHysteresis(state, 'friend', 0.55);
|
|
expect(result.shouldUpdate).toBe(false);
|
|
expect(result.nextKind).toBe('client');
|
|
expect(result.nextStreak).toBe(1);
|
|
expect(result.nextChallengerKind).toBe('friend');
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Corroboration rule: 3 consecutive same-kind → update on third
|
|
// -----------------------------------------------------------------------
|
|
|
|
test('three consecutive same-kind extractions → update on third', () => {
|
|
const initial: CurrentKindState = { kind: 'prospect', confidence: 0.8, challengerKind: null, streak: 0 };
|
|
|
|
// First 'client' extraction — challenger starts at streak 1
|
|
const r1 = applyHysteresis(initial, 'client', 0.6);
|
|
expect(r1.shouldUpdate).toBe(false);
|
|
expect(r1.nextStreak).toBe(1);
|
|
expect(r1.nextChallengerKind).toBe('client');
|
|
|
|
// Second 'client' — streak 1→2
|
|
const r2 = applyHysteresis(
|
|
{ kind: r1.nextKind, confidence: r1.nextConfidence, challengerKind: r1.nextChallengerKind, streak: r1.nextStreak },
|
|
'client',
|
|
0.6,
|
|
);
|
|
expect(r2.shouldUpdate).toBe(false);
|
|
expect(r2.nextStreak).toBe(2);
|
|
|
|
// Third 'client' — streak 2→3, corroboration triggers update
|
|
const r3 = applyHysteresis(
|
|
{ kind: r2.nextKind, confidence: r2.nextConfidence, challengerKind: r2.nextChallengerKind, streak: r2.nextStreak },
|
|
'client',
|
|
0.6,
|
|
);
|
|
expect(r3.shouldUpdate).toBe(true);
|
|
expect(r3.nextKind).toBe('client');
|
|
expect(r3.nextStreak).toBe(3);
|
|
});
|
|
|
|
test('two corroborating then one different resets streak', () => {
|
|
const initial: CurrentKindState = { kind: 'prospect', confidence: 0.8, challengerKind: null, streak: 0 };
|
|
|
|
const r1 = applyHysteresis(initial, 'client', 0.6);
|
|
expect(r1.nextStreak).toBe(1);
|
|
|
|
const r2 = applyHysteresis(
|
|
{ kind: r1.nextKind, confidence: r1.nextConfidence, challengerKind: r1.nextChallengerKind, streak: r1.nextStreak },
|
|
'client',
|
|
0.6,
|
|
);
|
|
expect(r2.nextStreak).toBe(2);
|
|
|
|
// Different kind resets challenger streak to 1
|
|
const r3 = applyHysteresis(
|
|
{ kind: r2.nextKind, confidence: r2.nextConfidence, challengerKind: r2.nextChallengerKind, streak: r2.nextStreak },
|
|
'friend',
|
|
0.6,
|
|
);
|
|
expect(r3.shouldUpdate).toBe(false);
|
|
expect(r3.nextStreak).toBe(1);
|
|
expect(r3.nextChallengerKind).toBe('friend');
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Same kind as current — still counts toward corroboration
|
|
// -----------------------------------------------------------------------
|
|
|
|
test('same kind as current but not yet at corroboration count → no update', () => {
|
|
const state: CurrentKindState = { kind: 'client', confidence: 0.75, challengerKind: 'client', streak: 1 };
|
|
const result = applyHysteresis(state, 'client', 0.78);
|
|
// 0.78 - 0.75 = 0.03 < 0.15; streak is now 2 — not enough for corroboration
|
|
expect(result.shouldUpdate).toBe(false);
|
|
expect(result.nextStreak).toBe(2);
|
|
});
|
|
});
|