test: Vitest test infrastructure + 52 unit tests for matching engine
Install vitest, @vitest/coverage-v8, jsdom, @testing-library/react/jest-dom. Add test / test:watch / test:coverage scripts to package.json. Three test suites covering the business-critical scoring pipeline: scoreCalculator.test.ts (28 tests) - calcDataQualityModifier: all 5 boundary thresholds (+5 / 0 / -5 / -10 / -15) - calcConfidenceModifier: verified/external/maison-work/future + low-conf stacking - applyHardFilters: pass, wrong asset type, area tolerance, budget exclusion, OCCUPIED penalty, excluded city - calculateScore: strong match ≥85, weak match <50, excluded=0, determinism, formula verification, DQ+confidence direction, occupied 25-point penalty, positive factors, allHardFactors completeness rankingEngine.test.ts (10 tests) - matchStrengthFromScore: STRONG/MODERATE/WEAK boundaries (78/52) - rankMatches: score sort, type tiebreak (VERIFIED > EXTERNAL), confidence tiebreak - buildFullMatch: all explainability fields, SHORTLIST+CONTACT for strong match, SCHEDULE for future signals, uncertainty indicators matchCardAdapter.test.ts (14 tests) - VERIFIED_PORTFOLIO: resultType, title, locationLabel, matchScore, scoreBreakdown, no disclaimer, reasons from positiveFactors, actions passthrough - FUTURE_AVAILABILITY: resultType, disclaimer always present, signalProbability, signalQuality HIGH for probability >= 0.70 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { buildMatchCardViewModel } from '../matchCardAdapter'
|
||||
import { buildFullMatch } from '../rankingEngine'
|
||||
import { ResultType } from '../../../domain/enums'
|
||||
import type { VerifiedPortfolioResult, FutureAvailabilityResult } from '../../../domain/unifiedResult'
|
||||
import type { FutureSignal } from '../../../domain/futureSignal'
|
||||
import { makeNeed, makeProperty } from './fixtures'
|
||||
|
||||
// ── buildMatchCardViewModel — verified portfolio ──────────────────────────────
|
||||
|
||||
describe('buildMatchCardViewModel — VERIFIED_PORTFOLIO', () => {
|
||||
const need = makeNeed()
|
||||
const property = makeProperty()
|
||||
const match = buildFullMatch(need, property)
|
||||
|
||||
const result: VerifiedPortfolioResult = {
|
||||
matchId: 'vm-test-1',
|
||||
needId: need.id,
|
||||
matchScore: match.matchScore,
|
||||
resultType: 'VERIFIED_PORTFOLIO',
|
||||
property,
|
||||
match,
|
||||
}
|
||||
|
||||
it('sets resultType correctly', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.resultType).toBe('VERIFIED_PORTFOLIO')
|
||||
})
|
||||
|
||||
it('uses property title as the card title', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.title).toBe(property.title)
|
||||
})
|
||||
|
||||
it('builds locationLabel from city (and district when present)', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.locationLabel).toContain('Zürich')
|
||||
})
|
||||
|
||||
it('propagates matchScore from the result', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.matchScore).toBe(match.matchScore)
|
||||
})
|
||||
|
||||
it('scoreBreakdown reflects hard and soft components from the engine', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.scoreBreakdown.hardMatchScore).toBe(match.scoreBreakdown.hardMatchScore)
|
||||
expect(vm.scoreBreakdown.softFactorScore).toBe(match.scoreBreakdown.softFactorScore)
|
||||
expect(vm.scoreBreakdown.totalScore).toBe(match.scoreBreakdown.totalScore)
|
||||
})
|
||||
|
||||
it('does NOT set a disclaimer for verified portfolio results', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
expect(vm.disclaimer).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns reasons derived from top positive factors', () => {
|
||||
const vm = buildMatchCardViewModel(result, [])
|
||||
// A high-scoring match should have at least one reason
|
||||
expect(vm.reasons.length).toBeGreaterThan(0)
|
||||
vm.reasons.forEach(r => {
|
||||
expect(r.type).toMatch(/HARD_FACT|SOFT_FACTOR/)
|
||||
expect(r.score).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('passes through supplied actions unchanged', () => {
|
||||
const actions = [{ label: 'Shortlist', actionType: 'SHORTLIST' as const, primary: true }]
|
||||
const vm = buildMatchCardViewModel(result, actions)
|
||||
expect(vm.actions).toBe(actions)
|
||||
})
|
||||
})
|
||||
|
||||
// ── buildMatchCardViewModel — future availability ─────────────────────────────
|
||||
|
||||
describe('buildMatchCardViewModel — FUTURE_AVAILABILITY', () => {
|
||||
const need = makeNeed()
|
||||
const futureProperty = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY })
|
||||
const match = buildFullMatch(need, futureProperty)
|
||||
|
||||
const minimalSignal = {
|
||||
id: 'sig-1',
|
||||
probability: 0.72,
|
||||
locationHint: 'Zürich Oerlikon',
|
||||
isVerified: false,
|
||||
source: { type: 'AI_SIGNAL', credibility: 0.7 },
|
||||
} as unknown as FutureSignal
|
||||
|
||||
const futureResult: FutureAvailabilityResult = {
|
||||
matchId: 'future-test-1',
|
||||
needId: need.id,
|
||||
matchScore: match.matchScore,
|
||||
resultType: 'FUTURE_AVAILABILITY',
|
||||
signal: minimalSignal,
|
||||
property: futureProperty,
|
||||
match,
|
||||
}
|
||||
|
||||
it('sets resultType to FUTURE_AVAILABILITY', () => {
|
||||
const vm = buildMatchCardViewModel(futureResult, [])
|
||||
expect(vm.resultType).toBe('FUTURE_AVAILABILITY')
|
||||
})
|
||||
|
||||
it('always includes a disclaimer for future signals', () => {
|
||||
const vm = buildMatchCardViewModel(futureResult, [])
|
||||
expect(vm.disclaimer).toBeTruthy()
|
||||
expect(typeof vm.disclaimer).toBe('string')
|
||||
})
|
||||
|
||||
it('exposes signal probability in the view model', () => {
|
||||
const vm = buildMatchCardViewModel(futureResult, [])
|
||||
expect(vm.signalProbability).toBe(0.72)
|
||||
})
|
||||
|
||||
it('classifies signal quality as HIGH when probability >= 0.70', () => {
|
||||
const vm = buildMatchCardViewModel(futureResult, [])
|
||||
expect(vm.signalQuality).toBe('HIGH')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user