mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
468 lines
16 KiB
TypeScript
468 lines
16 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
evaluateMunicipalityTriggers,
|
|
getSuggestionPattern,
|
|
isSuggestionSuppressedByDecision,
|
|
shouldEnterSilenceReview,
|
|
SUGGESTION_SUPPRESSION_DAYS,
|
|
suggestionPatternIds,
|
|
suggestionPatterns,
|
|
suggestionPostponeDays,
|
|
type MunicipalityTriggerInput,
|
|
type SuggestionDecisionLike,
|
|
type SuggestionPatternId,
|
|
} from '@/lib/suggestionCatalog'
|
|
|
|
/**
|
|
* A quiet, healthy manutenção município: nothing may fire on it. Every
|
|
* pattern's green case is a minimal deviation from this baseline, so a test
|
|
* that goes red names exactly the condition that broke.
|
|
*/
|
|
const input = (overrides: Partial<MunicipalityTriggerInput> = {}): MunicipalityTriggerInput => ({
|
|
priority: 'normal',
|
|
engagementLevel: null,
|
|
territorialClass: 'manutencao',
|
|
inCoreBlock: false,
|
|
lqByYear: { 2014: 1, 2018: 1, 2022: 1 },
|
|
ownVotesByYear: { 2014: 900, 2018: 1000, 2022: 1000 },
|
|
projectedValidVotes: 8_000,
|
|
projectedValidVotesCut: 10_000,
|
|
projectedValidVotesUpperCut: 20_000,
|
|
uncapturedFieldVotes: 2_000,
|
|
uncapturedFieldVotesCut: 3_000,
|
|
captureRate2022: 0.2,
|
|
fieldShareOfValid2022: 0.3,
|
|
intraFieldShare2022: 0.1,
|
|
intraFieldShareStateStandard2022: 0.1,
|
|
advisorCount: 1,
|
|
leadershipCount: 2,
|
|
pledgeCount: 3,
|
|
lastSignalAgeDays: 3,
|
|
lastPledgeAgeDays: 3,
|
|
coverageRatio: 0.6,
|
|
coverageDeficit: 400,
|
|
adversarySignal: null,
|
|
hasRecentOrUpcomingActivity: false,
|
|
completedActivityCount: 0,
|
|
...overrides,
|
|
})
|
|
|
|
const evaluate = (id: SuggestionPatternId, triggerInput: MunicipalityTriggerInput) =>
|
|
getSuggestionPattern(id).evaluate(triggerInput)
|
|
|
|
describe('suggestionCatalog — catalog integrity', () => {
|
|
it('ships exactly the 8 v1 patterns, in declared id order', () => {
|
|
expect(suggestionPatterns.map((pattern) => pattern.id)).toEqual([...suggestionPatternIds])
|
|
})
|
|
|
|
it('every pattern carries complete card copy', () => {
|
|
for (const pattern of suggestionPatterns) {
|
|
expect(pattern.title.trim()).not.toHaveLength(0)
|
|
expect(pattern.probableReading.trim()).not.toHaveLength(0)
|
|
expect(pattern.contraindication.trim()).not.toHaveLength(0)
|
|
expect(pattern.alternativeReadings.length).toBeGreaterThan(0)
|
|
expect(pattern.menu.length).toBeGreaterThan(0)
|
|
const actionIds = pattern.menu.map((action) => action.id)
|
|
expect(new Set(actionIds).size).toBe(actionIds.length)
|
|
}
|
|
})
|
|
|
|
it('getSuggestionPattern refuses an unknown id', () => {
|
|
expect(() => getSuggestionPattern('P99' as SuggestionPatternId)).toThrow()
|
|
})
|
|
})
|
|
|
|
describe('P1 — reduto dormente ou ameaçado', () => {
|
|
const reduto = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({
|
|
territorialClass: 'reduto',
|
|
inCoreBlock: true,
|
|
lqByYear: { 2014: 2.5, 2018: 2.6, 2022: 2.4 },
|
|
...overrides,
|
|
})
|
|
|
|
it('fires at level 3 on a dormant core reduto', () => {
|
|
const triggered = evaluate('P1', reduto({ lastSignalAgeDays: 30 }))
|
|
expect(triggered).toMatchObject({ patternId: 'P1', triageLevel: 3 })
|
|
expect(triggered?.factors.join(' ')).toContain('30 dias')
|
|
})
|
|
|
|
it('never-signaled counts as dormant', () => {
|
|
expect(evaluate('P1', reduto({ lastSignalAgeDays: null }))?.triageLevel).toBe(3)
|
|
})
|
|
|
|
it('escalates to level 2 on a single-source adversary signal and 1 when triangulated', () => {
|
|
expect(evaluate('P1', reduto({ adversarySignal: { triangulated: false } }))?.triageLevel).toBe(
|
|
2,
|
|
)
|
|
expect(evaluate('P1', reduto({ adversarySignal: { triangulated: true } }))?.triageLevel).toBe(1)
|
|
})
|
|
|
|
it('fires on a 2018→2022 vote drop even with a fresh signal', () => {
|
|
const triggered = evaluate(
|
|
'P1',
|
|
reduto({ ownVotesByYear: { 2018: 1000, 2022: 800 }, lastSignalAgeDays: 2 }),
|
|
)
|
|
expect(triggered?.triageLevel).toBe(3)
|
|
expect(triggered?.factors.join(' ')).toContain('2018')
|
|
})
|
|
|
|
it('stays silent when the reduto is watched: fresh signal or agenda in the window', () => {
|
|
expect(evaluate('P1', reduto({ lastSignalAgeDays: 5 }))).toBeNull()
|
|
expect(
|
|
evaluate('P1', reduto({ lastSignalAgeDays: 30, hasRecentOrUpcomingActivity: true })),
|
|
).toBeNull()
|
|
})
|
|
|
|
it('requires the core block — a trophy reduto outside it is P10 territory, not P1', () => {
|
|
expect(evaluate('P1', reduto({ inCoreBlock: false, lastSignalAgeDays: 30 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('P2 — ataque: campo forte, captura baixa', () => {
|
|
const attack = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({
|
|
territorialClass: 'expansao',
|
|
uncapturedFieldVotes: 5_000,
|
|
projectedValidVotes: 12_000,
|
|
...overrides,
|
|
})
|
|
|
|
it('fires at level 4 on a big open-field município', () => {
|
|
expect(evaluate('P2', attack())).toMatchObject({ patternId: 'P2', triageLevel: 4 })
|
|
})
|
|
|
|
it('yields to P3 when a correligionário holds the campo', () => {
|
|
const held = attack({
|
|
fieldShareOfValid2022: 0.5,
|
|
intraFieldShare2022: 0.03,
|
|
intraFieldShareStateStandard2022: 0.1,
|
|
})
|
|
expect(evaluate('P2', held)).toBeNull()
|
|
expect(evaluate('P3', held)).toMatchObject({ patternId: 'P3', triageLevel: 4 })
|
|
})
|
|
|
|
it('yields to K-B when the series says perdida', () => {
|
|
const lost = attack({
|
|
captureRate2022: 0.01,
|
|
lqByYear: { 2014: 0.3, 2018: 0.2, 2022: 0.25 },
|
|
})
|
|
expect(evaluate('P2', lost)).toBeNull()
|
|
expect(evaluate('K-B', lost)).toMatchObject({ patternId: 'K-B', triageLevel: 5 })
|
|
})
|
|
|
|
it('needs volume and open field', () => {
|
|
expect(evaluate('P2', attack({ projectedValidVotes: 5_000 }))).toBeNull()
|
|
expect(evaluate('P2', attack({ uncapturedFieldVotes: 1_000 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('P3 — correligionário na frente', () => {
|
|
const held = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({
|
|
fieldShareOfValid2022: 0.5,
|
|
intraFieldShare2022: 0.04,
|
|
intraFieldShareStateStandard2022: 0.1,
|
|
projectedValidVotes: 12_000,
|
|
...overrides,
|
|
})
|
|
|
|
it('fires when the campo is strong and the own intra-field slice is far below standard', () => {
|
|
expect(evaluate('P3', held())).toMatchObject({ patternId: 'P3', triageLevel: 4 })
|
|
})
|
|
|
|
it('stays silent below the volume cut or without the campo', () => {
|
|
expect(evaluate('P3', held({ projectedValidVotes: 5_000 }))).toBeNull()
|
|
expect(evaluate('P3', held({ fieldShareOfValid2022: 0.2 }))).toBeNull()
|
|
expect(evaluate('P3', held({ intraFieldShare2022: 0.09 }))).toBeNull()
|
|
})
|
|
|
|
it('tolerates missing artifact cells', () => {
|
|
expect(evaluate('P3', held({ intraFieldShareStateStandard2022: null }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('P5 — expansão acima do padrão', () => {
|
|
it('fires on a rising LQ series', () => {
|
|
const triggered = evaluate('P5', input({ lqByYear: { 2014: 0.5, 2018: 0.8, 2022: 1.2 } }))
|
|
expect(triggered).toMatchObject({ patternId: 'P5', triageLevel: 4 })
|
|
expect(triggered?.factors.join(' ')).toContain('2014')
|
|
})
|
|
|
|
it('fires on coverage above 100%', () => {
|
|
expect(evaluate('P5', input({ coverageRatio: 1.2 }))?.triageLevel).toBe(4)
|
|
})
|
|
|
|
it('demands real growth, not monotonic noise', () => {
|
|
expect(evaluate('P5', input({ lqByYear: { 2014: 1, 2018: 1.05, 2022: 1.1 } }))).toBeNull()
|
|
})
|
|
|
|
it('never fires on sem_base', () => {
|
|
expect(evaluate('P5', input({ territorialClass: 'sem_base', coverageRatio: 1.2 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('P6 — município grande sem rede', () => {
|
|
const orphan = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({
|
|
projectedValidVotes: 25_000,
|
|
leadershipCount: 0,
|
|
advisorCount: 0,
|
|
pledgeCount: 0,
|
|
...overrides,
|
|
})
|
|
|
|
it('fires at level 5 without prioritization and 3 with it', () => {
|
|
expect(evaluate('P6', orphan())?.triageLevel).toBe(5)
|
|
expect(evaluate('P6', orphan({ priority: 'alta' }))?.triageLevel).toBe(3)
|
|
expect(evaluate('P6', orphan({ engagementLevel: 'n2' }))?.triageLevel).toBe(3)
|
|
})
|
|
|
|
it('any network kills the trigger', () => {
|
|
expect(evaluate('P6', orphan({ leadershipCount: 1 }))).toBeNull()
|
|
expect(evaluate('P6', orphan({ advisorCount: 1 }))).toBeNull()
|
|
expect(evaluate('P6', orphan({ pledgeCount: 1 }))).toBeNull()
|
|
})
|
|
|
|
it('requires the upper quartile', () => {
|
|
expect(evaluate('P6', orphan({ projectedValidVotes: 15_000 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('P7 — pledges estagnados vs. meta', () => {
|
|
const stagnant = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({ priority: 'alta', lastPledgeAgeDays: 25, ...overrides })
|
|
|
|
it('fires at level 5 on a prioritized município with a registering-but-stale network', () => {
|
|
expect(evaluate('P7', stagnant())).toMatchObject({ patternId: 'P7', triageLevel: 5 })
|
|
})
|
|
|
|
it('prioritization can come from the engagement level', () => {
|
|
expect(evaluate('P7', stagnant({ priority: 'normal', engagementLevel: 'n3' }))).not.toBeNull()
|
|
expect(evaluate('P7', stagnant({ priority: 'normal' }))).toBeNull()
|
|
})
|
|
|
|
it('hands the case to K-A when there was recorded effort in the cycle', () => {
|
|
expect(evaluate('P7', stagnant({ completedActivityCount: 1 }))).toBeNull()
|
|
})
|
|
|
|
it('needs pledges to exist, be stale, and coverage to be short', () => {
|
|
expect(evaluate('P7', stagnant({ pledgeCount: 0 }))).toBeNull()
|
|
expect(evaluate('P7', stagnant({ lastPledgeAgeDays: 10 }))).toBeNull()
|
|
expect(evaluate('P7', stagnant({ coverageRatio: 1.1 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('K-A — não responde a investimento', () => {
|
|
const invested = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({ completedActivityCount: 2, lastPledgeAgeDays: 30, ...overrides })
|
|
|
|
it('fires when effort was recorded and pledges did not move', () => {
|
|
expect(evaluate('K-A', invested())).toMatchObject({ patternId: 'K-A', triageLevel: 5 })
|
|
})
|
|
|
|
it('a município that never pledged despite effort also fires', () => {
|
|
const triggered = evaluate('K-A', invested({ lastPledgeAgeDays: null }))
|
|
expect(triggered?.factors.join(' ')).toContain('Nenhum pledge')
|
|
})
|
|
|
|
it('recent pledge movement or met coverage silences it', () => {
|
|
expect(evaluate('K-A', invested({ lastPledgeAgeDays: 10 }))).toBeNull()
|
|
expect(evaluate('K-A', invested({ coverageRatio: 1.05 }))).toBeNull()
|
|
expect(evaluate('K-A', invested({ completedActivityCount: 0 }))).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('K-B — perdida vestida de oportunidade', () => {
|
|
const mirage = (overrides: Partial<MunicipalityTriggerInput> = {}) =>
|
|
input({
|
|
territorialClass: 'marginal',
|
|
uncapturedFieldVotes: 5_000,
|
|
captureRate2022: 0.01,
|
|
lqByYear: { 2014: 0.3, 2018: 0.2, 2022: 0.25 },
|
|
...overrides,
|
|
})
|
|
|
|
it('fires at level 5 on apparent headroom with a minimal-capture series', () => {
|
|
expect(evaluate('K-B', mirage())).toMatchObject({ patternId: 'K-B', triageLevel: 5 })
|
|
})
|
|
|
|
it('a single strong year in the series breaks the "perdida" reading', () => {
|
|
expect(evaluate('K-B', mirage({ lqByYear: { 2014: 0.3, 2018: 1.4, 2022: 0.25 } }))).toBeNull()
|
|
})
|
|
|
|
it('capture above minimal or no headroom silences it', () => {
|
|
expect(evaluate('K-B', mirage({ captureRate2022: 0.05 }))).toBeNull()
|
|
expect(evaluate('K-B', mirage({ uncapturedFieldVotes: 1_000 }))).toBeNull()
|
|
})
|
|
|
|
it('yields to P3 when the campo itself holds the município', () => {
|
|
expect(
|
|
evaluate(
|
|
'K-B',
|
|
mirage({
|
|
fieldShareOfValid2022: 0.5,
|
|
intraFieldShare2022: 0.03,
|
|
intraFieldShareStateStandard2022: 0.1,
|
|
}),
|
|
),
|
|
).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('evaluateMunicipalityTriggers — ordering and silence', () => {
|
|
it('returns triggered patterns sorted by triage level', () => {
|
|
const triggered = evaluateMunicipalityTriggers(
|
|
input({
|
|
territorialClass: 'reduto',
|
|
inCoreBlock: true,
|
|
lqByYear: { 2014: 2.5, 2018: 2.6, 2022: 2.4 },
|
|
adversarySignal: { triangulated: true },
|
|
coverageRatio: 1.2,
|
|
}),
|
|
)
|
|
expect(triggered.map((entry) => entry.patternId)).toEqual(['P1', 'P5'])
|
|
expect(triggered[0]?.triageLevel).toBe(1)
|
|
})
|
|
|
|
it('returns nothing on the healthy baseline', () => {
|
|
expect(evaluateMunicipalityTriggers(input())).toEqual([])
|
|
})
|
|
|
|
it('silence review needs prioritization, no triggers and a silent month', () => {
|
|
expect(shouldEnterSilenceReview(input({ priority: 'alta', lastSignalAgeDays: 35 }), 0)).toBe(
|
|
true,
|
|
)
|
|
expect(shouldEnterSilenceReview(input({ priority: 'alta', lastSignalAgeDays: null }), 0)).toBe(
|
|
true,
|
|
)
|
|
expect(
|
|
shouldEnterSilenceReview(input({ engagementLevel: 'n2', lastSignalAgeDays: 40 }), 0),
|
|
).toBe(true)
|
|
expect(shouldEnterSilenceReview(input({ priority: 'alta', lastSignalAgeDays: 10 }), 0)).toBe(
|
|
false,
|
|
)
|
|
expect(shouldEnterSilenceReview(input({ priority: 'alta', lastSignalAgeDays: 35 }), 1)).toBe(
|
|
false,
|
|
)
|
|
expect(shouldEnterSilenceReview(input({ lastSignalAgeDays: 35 }), 0)).toBe(false)
|
|
})
|
|
|
|
it('postpone durations: 7 days for urgent levels, 14 for optimization levels', () => {
|
|
expect(suggestionPostponeDays(1)).toBe(7)
|
|
expect(suggestionPostponeDays(3)).toBe(7)
|
|
expect(suggestionPostponeDays(4)).toBe(14)
|
|
expect(suggestionPostponeDays(5)).toBe(14)
|
|
})
|
|
|
|
it('suppression windows exist for both terminal outcomes', () => {
|
|
expect(SUGGESTION_SUPPRESSION_DAYS.aceita).toBeGreaterThan(0)
|
|
expect(SUGGESTION_SUPPRESSION_DAYS.descarta).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('isSuggestionSuppressedByDecision — churn windows (§6.4)', () => {
|
|
const now = new Date('2026-07-28T12:00:00.000Z')
|
|
const daysAgo = (days: number): string =>
|
|
new Date(now.getTime() - days * 86_400_000).toISOString()
|
|
const decision = (overrides: Partial<SuggestionDecisionLike>): SuggestionDecisionLike => ({
|
|
outcome: 'descarta',
|
|
createdAt: daysAgo(1),
|
|
snapshot: { triageLevel: 5 },
|
|
...overrides,
|
|
})
|
|
|
|
it('a dismissal suppresses inside its window and expires after it', () => {
|
|
expect(isSuggestionSuppressedByDecision(decision({}), { triageLevel: 5 }, now)).toBe(true)
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ createdAt: daysAgo(SUGGESTION_SUPPRESSION_DAYS.descarta + 1) }),
|
|
{ triageLevel: 5 },
|
|
now,
|
|
),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('an acceptance suppresses for its own, shorter window', () => {
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ outcome: 'aceita', createdAt: daysAgo(7) }),
|
|
{
|
|
triageLevel: 4,
|
|
},
|
|
now,
|
|
),
|
|
).toBe(true)
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ outcome: 'aceita', createdAt: daysAgo(SUGGESTION_SUPPRESSION_DAYS.aceita + 1) }),
|
|
{ triageLevel: 4 },
|
|
now,
|
|
),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('a postponement suppresses until its snapshot suppressUntil, then resurfaces', () => {
|
|
const future = new Date(now.getTime() + 86_400_000).toISOString()
|
|
const past = daysAgo(1)
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ outcome: 'adiada', snapshot: { triageLevel: 5, suppressUntil: future } }),
|
|
{ triageLevel: 5 },
|
|
now,
|
|
),
|
|
).toBe(true)
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({
|
|
outcome: 'adiada',
|
|
createdAt: daysAgo(10),
|
|
snapshot: { triageLevel: 5, suppressUntil: past },
|
|
}),
|
|
{ triageLevel: 5 },
|
|
now,
|
|
),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('a malformed postponement snapshot falls back to the shortest postpone, never forever', () => {
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ outcome: 'adiada', createdAt: daysAgo(1), snapshot: null }),
|
|
{ triageLevel: 5 },
|
|
now,
|
|
),
|
|
).toBe(true)
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ outcome: 'adiada', createdAt: daysAgo(8), snapshot: null }),
|
|
{ triageLevel: 5 },
|
|
now,
|
|
),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('a confirmed risk (level 1) pierces a decision recorded at lower urgency', () => {
|
|
const dismissedAtLevel3 = decision({ snapshot: { triageLevel: 3 } })
|
|
expect(isSuggestionSuppressedByDecision(dismissedAtLevel3, { triageLevel: 1 }, now)).toBe(false)
|
|
// Seen and decided AT level 1: the window holds.
|
|
expect(
|
|
isSuggestionSuppressedByDecision(
|
|
decision({ snapshot: { triageLevel: 1 } }),
|
|
{
|
|
triageLevel: 1,
|
|
},
|
|
now,
|
|
),
|
|
).toBe(true)
|
|
// Lower-urgency re-trigger of a level-3 dismissal: the window holds too.
|
|
expect(isSuggestionSuppressedByDecision(dismissedAtLevel3, { triageLevel: 3 }, now)).toBe(true)
|
|
})
|
|
|
|
it('a level movement (E14) never suppresses a suggestion', () => {
|
|
expect(
|
|
isSuggestionSuppressedByDecision(decision({ outcome: 'movimento' }), { triageLevel: 5 }, now),
|
|
).toBe(false)
|
|
})
|
|
})
|