Files
property-match/src/features/matching/__tests__/rankingEngine.test.ts
T
Benjamin Sutter 609a3634bd feat: remove EXTERNAL_MARKET result type entirely
All external market properties now surface as VERIFIED_PORTFOLIO.
Removes the type from enums, domain types, mock data, matching engine,
components, hooks, and tests — zero user-visible distinction remains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:28:42 +02:00

105 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import { matchStrengthFromScore, rankMatches, buildFullMatch } from '../rankingEngine'
import { MatchStrength, ResultType } from '../../../domain/enums'
import { makeNeed, makeProperty } from './fixtures'
// ── matchStrengthFromScore ────────────────────────────────────────────────────
describe('matchStrengthFromScore', () => {
it('classifies score >= 78 as STRONG', () => {
expect(matchStrengthFromScore(78)).toBe(MatchStrength.STRONG)
expect(matchStrengthFromScore(100)).toBe(MatchStrength.STRONG)
expect(matchStrengthFromScore(90)).toBe(MatchStrength.STRONG)
})
it('classifies score 5277 as MODERATE', () => {
expect(matchStrengthFromScore(77)).toBe(MatchStrength.MODERATE)
expect(matchStrengthFromScore(52)).toBe(MatchStrength.MODERATE)
expect(matchStrengthFromScore(65)).toBe(MatchStrength.MODERATE)
})
it('classifies score < 52 as WEAK', () => {
expect(matchStrengthFromScore(51)).toBe(MatchStrength.WEAK)
expect(matchStrengthFromScore(0)).toBe(MatchStrength.WEAK)
expect(matchStrengthFromScore(30)).toBe(MatchStrength.WEAK)
})
})
// ── rankMatches ───────────────────────────────────────────────────────────────
describe('rankMatches', () => {
const need = makeNeed()
it('sorts matches by matchScore descending', () => {
const high = buildFullMatch(need, makeProperty({ id: 'high', rentPricePerSqm: 40 }))
const low = buildFullMatch(need, makeProperty({ id: 'low', rentPricePerSqm: 65, location: { city: 'Basel', country: 'CH' } }))
const ranked = rankMatches([low, high])
expect(ranked[0].matchScore).toBeGreaterThanOrEqual(ranked[1].matchScore)
expect(ranked[0].propertyId).toBe('high')
})
it('breaks ties by result type: VERIFIED_PORTFOLIO and MAISON_WORK ranked above FUTURE_AVAILABILITY', () => {
// Build one of each type but force identical score by using same property body
const prop = makeProperty()
const verified = buildFullMatch(need, { ...prop, id: 'v', resultType: ResultType.VERIFIED_PORTFOLIO })
const future = buildFullMatch(need, { ...prop, id: 'f', resultType: ResultType.FUTURE_AVAILABILITY, confidenceScore: 0.85 })
// Force same score for a clean tiebreak test
verified.matchScore = 75
future.matchScore = 75
const ranked = rankMatches([future, verified])
expect(ranked[0].resultType).toBe(ResultType.VERIFIED_PORTFOLIO)
})
it('breaks ties among same type by confidence descending', () => {
const highConf = buildFullMatch(need, makeProperty({ id: 'hc', confidenceScore: 0.90 }))
const lowConf = buildFullMatch(need, makeProperty({ id: 'lc', confidenceScore: 0.60 }))
// Force same score and same type
highConf.matchScore = 75
lowConf.matchScore = 75
const ranked = rankMatches([lowConf, highConf])
expect(ranked[0].confidenceLevel).toBeGreaterThan(ranked[1].confidenceLevel)
})
})
// ── buildFullMatch ────────────────────────────────────────────────────────────
describe('buildFullMatch', () => {
it('returns a Match with all required explainability fields', () => {
const match = buildFullMatch(makeNeed(), makeProperty())
expect(match.id).toContain('match-')
expect(match.scoreBreakdown).toBeDefined()
expect(match.scoreBreakdown.hardMatchScore).toBeGreaterThan(0)
expect(match.scoreBreakdown.softFactorScore).toBeGreaterThan(0)
expect(match.scoreBreakdown.totalScore).toBe(match.matchScore)
expect(match.matchStrength).toBeDefined()
expect(match.explainabilitySummary).toBeTruthy()
})
it('generates shortlist and contact actions for a strong match', () => {
const match = buildFullMatch(makeNeed(), makeProperty())
// Perfect fixture should score >= 78 (STRONG) and generate SHORTLIST + CONTACT actions
if (match.matchScore >= 78) {
const types = match.nextBestActions?.map(a => a.actionType) ?? []
expect(types).toContain('SHORTLIST')
expect(types).toContain('CONTACT')
}
})
it('adds a SCHEDULE action for future availability properties', () => {
const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY })
const match = buildFullMatch(makeNeed(), prop)
const types = match.nextBestActions?.map(a => a.actionType) ?? []
expect(types).toContain('SCHEDULE')
})
it('uncertainty indicators include a note for future availability', () => {
const prop = makeProperty({ resultType: ResultType.FUTURE_AVAILABILITY })
const match = buildFullMatch(makeNeed(), prop)
expect(match.uncertaintyIndicators?.some(s => s.includes('Signal'))).toBe(true)
})
})