36570c5bdc
- Fix MUI v9 API: PaperProps/InputLabelProps/inputProps → slotProps in 6 components - Add ExternalMarketResult alias, PropertyUnit import, OPERATIONS workspace config - Fix TradeOff.description → .concern, ScoreFactor.label → .criterion - Make schattenmarktRelease.leadTimeMonths optional, fix mock-data enum values - Fix useMatchDetailData query typing, weightingService missing WeightProfile keys - Split Pipeline/Compare/MatchDetail/IntelligenceMatchCard into sub-components - Fix all test fixtures (CreateNeedInput, CreatePropertyInput, TradeOffInput, etc.) - Add vercel.json for deployment, zero tsc errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
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 = [{ id: 'shortlist', label: 'Shortlist', actionType: 'SAVE_SHORTLIST' as const, primary: true, onClick: () => {} }]
|
|
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')
|
|
})
|
|
})
|