test: integration coverage for stores, services, MockAIService, and hooks

- Stores (29 tests): compareStore max-4 enforcement, toastStore unique IDs,
  shortlistStore dialog state, assistantStore context merge + clearConversation
- Services (37 tests): needService CRUD, propertyService dashboard summary
  active-status filter, matchService computeMatchesForNeed + getStrongMatches,
  futureSignalService summary math (distribution totals, highConfidence count)
- MockAIService (18 tests): follow-up question priority ordering, area ambiguity
  detection (ratio >8 and zero values), max-3 cap, trade-off risk logic
  (LOW/MEDIUM/HIGH thresholds), match explanation headline tiers, provenance shape
- Hooks (20 tests): useNeeds/useProperties/useMatches envelope unwrap, disabled-
  when-empty guards, useCreateNeed/useCreateProperty mutations, useNeed/useMatchDetail

All 257 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 16:55:02 +02:00
parent e1f4beb898
commit 58a0a05eff
12 changed files with 1051 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
import { describe, it, expect } from 'vitest'
import { matchService } from '../matchService'
describe('matchService', () => {
describe('getAll', () => {
it('returns a ListResponse with match data', async () => {
const result = await matchService.getAll()
expect(Array.isArray(result.data)).toBe(true)
expect(result.data.length).toBeGreaterThan(0)
expect(result.meta.total).toBe(result.data.length)
})
it('each match has required fields', async () => {
const { data } = await matchService.getAll()
for (const m of data) {
expect(typeof m.id).toBe('string')
expect(typeof m.needId).toBe('string')
expect(typeof m.propertyId).toBe('string')
expect(typeof m.matchScore).toBe('number')
expect(m.matchScore).toBeGreaterThanOrEqual(0)
expect(m.matchScore).toBeLessThanOrEqual(100)
}
})
})
describe('getById', () => {
it('returns a match for an existing id', async () => {
const { data: all } = await matchService.getAll()
const id = all[0].id
const result = await matchService.getById(id)
expect(result.data?.id).toBe(id)
})
it('returns null for an unknown id', async () => {
const result = await matchService.getById('no-such-match')
expect(result.data).toBeNull()
})
})
describe('getByNeed', () => {
it('returns only matches for the given needId', async () => {
const { data: all } = await matchService.getAll()
const needId = all[0].needId
const result = await matchService.getByNeed(needId)
expect(result.data.every(m => m.needId === needId)).toBe(true)
})
})
describe('getByProperty', () => {
it('returns only matches for the given propertyId', async () => {
const { data: all } = await matchService.getAll()
const propertyId = all[0].propertyId
const result = await matchService.getByProperty(propertyId)
expect(result.data.every(m => m.propertyId === propertyId)).toBe(true)
})
})
describe('getScoreBreakdown', () => {
it('returns a ScoreBreakdown for a valid match', async () => {
const { data: all } = await matchService.getAll()
const matchWithBreakdown = all.find(m => !!m.scoreBreakdown)
if (!matchWithBreakdown) return // seed data may not always have breakdowns
const result = await matchService.getScoreBreakdown(matchWithBreakdown.id)
expect(result.data).not.toBeNull()
})
it('returns null for an unknown match', async () => {
const result = await matchService.getScoreBreakdown('no-such-match')
expect(result.data).toBeNull()
})
})
describe('computeMatchesForNeed', () => {
it('returns computed matches for a valid needId', async () => {
const result = await matchService.computeMatchesForNeed('need-001')
expect(result.data.length).toBeGreaterThan(0)
expect(result.data.every(m => m.needId === 'need-001')).toBe(true)
})
it('returns empty array for unknown needId', async () => {
const result = await matchService.computeMatchesForNeed('no-such-need')
expect(result.data).toEqual([])
})
})
describe('getStrongMatches', () => {
it('returns only matches at or above the threshold', async () => {
const minScore = 70
const result = await matchService.getStrongMatches(minScore)
expect(result.every(m => m.matchScore >= minScore)).toBe(true)
})
it('returns at most 5 items', async () => {
const result = await matchService.getStrongMatches(0)
expect(result.length).toBeLessThanOrEqual(5)
})
it('each item has propertyTitle and topReason', async () => {
const result = await matchService.getStrongMatches(0)
for (const item of result) {
expect(typeof item.propertyTitle).toBe('string')
expect(typeof item.topReason).toBe('string')
expect(typeof item.matchScore).toBe('number')
}
})
})
})