@coldiq/mcp 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,54 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
2
  import { initClient } from '../../src/client.js'
3
- import { searchPlacesHandler } from '../../src/tools/search-places.js'
3
+ import { searchPlacesHandler, applyPlaceFilters } from '../../src/tools/search-places.js'
4
+
5
+ describe('applyPlaceFilters', () => {
6
+ const sample = {
7
+ places: [
8
+ { title: 'Low rating, many reviews', totalScore: 2.9, reviewsCount: 81 },
9
+ { title: 'Mid rating, mid reviews', totalScore: 3.9, reviewsCount: 322 },
10
+ { title: 'High rating, many reviews', totalScore: 4.6, reviewsCount: 1101 },
11
+ { title: 'High rating, few reviews', totalScore: 4.7, reviewsCount: 12 },
12
+ { title: 'Missing fields', /* no totalScore or reviewsCount */ },
13
+ ],
14
+ }
15
+
16
+ it('passes through unchanged when no filters supplied', () => {
17
+ const out = applyPlaceFilters(sample, {}) as { places: unknown[] }
18
+ expect(out.places).toHaveLength(5)
19
+ })
20
+
21
+ it('does not mutate its input', () => {
22
+ const before = JSON.stringify(sample)
23
+ applyPlaceFilters(sample, { maxRating: 4 })
24
+ expect(JSON.stringify(sample)).toBe(before)
25
+ })
26
+
27
+ it('max_overall_rating drops places above the cap', () => {
28
+ const out = applyPlaceFilters(sample, { maxRating: 4 }) as { places: Array<{ totalScore?: number }> }
29
+ expect(out.places.map((p) => p.totalScore)).toEqual([2.9, 3.9])
30
+ })
31
+
32
+ it('min_total_reviews drops places below the floor', () => {
33
+ const out = applyPlaceFilters(sample, { minReviews: 20 }) as { places: Array<{ reviewsCount?: number }> }
34
+ expect(out.places.map((p) => p.reviewsCount)).toEqual([81, 322, 1101])
35
+ })
36
+
37
+ it('combined filters apply as AND', () => {
38
+ const out = applyPlaceFilters(sample, { maxRating: 4, minReviews: 20 }) as { places: Array<{ title?: string }> }
39
+ expect(out.places.map((p) => p.title)).toEqual(['Low rating, many reviews', 'Mid rating, mid reviews'])
40
+ })
41
+
42
+ it('places with missing totalScore/reviewsCount are dropped when corresponding filter is active', () => {
43
+ const out = applyPlaceFilters(sample, { minRating: 0 }) as { places: Array<{ title?: string }> }
44
+ expect(out.places.map((p) => p.title)).not.toContain('Missing fields')
45
+ })
46
+
47
+ it('non-object / non-array shapes are passed through untouched', () => {
48
+ expect(applyPlaceFilters(null, { maxRating: 4 })).toBe(null)
49
+ expect(applyPlaceFilters({ places: 'not an array' }, { maxRating: 4 })).toEqual({ places: 'not an array' })
50
+ })
51
+ })
4
52
 
5
53
  describe('search_places handler', () => {
6
54
  const originalFetch = globalThis.fetch
@@ -100,6 +148,54 @@ describe('search_places handler', () => {
100
148
  }
101
149
  })
102
150
 
151
+ it('google_maps response is post-filtered by max_overall_rating + min_total_reviews', async () => {
152
+ // Regression guard: if the handler stops wiring applyPlaceFilters, this test fails.
153
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
154
+ const u = url.toString()
155
+ if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
156
+ return new Response(JSON.stringify({ jobId: 'gm-filter' }), { status: 200 })
157
+ }
158
+ if (u.includes('/google-maps/scraper/gm-filter')) {
159
+ return new Response(JSON.stringify({
160
+ jobId: 'gm-filter',
161
+ status: 'done',
162
+ places: [
163
+ { title: 'Strict match — keep', totalScore: 3.5, reviewsCount: 120 },
164
+ { title: 'Rating too high — drop', totalScore: 4.7, reviewsCount: 800 },
165
+ { title: 'Too few reviews — drop', totalScore: 2.9, reviewsCount: 10 },
166
+ ],
167
+ }), { status: 200 })
168
+ }
169
+ return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
170
+ }) as typeof fetch
171
+
172
+ const { getProviders } = await import('../../src/registry.js')
173
+ const providers = getProviders('search_places')
174
+ const gm = providers.find((p) => p.id === 'google_maps')!
175
+ const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
176
+
177
+ try {
178
+ gm.async!.timeoutMs = 500
179
+ gm.async!.pollIntervalMs = 20
180
+ const result = await searchPlacesHandler({
181
+ query: 'dentist',
182
+ country: 'FR',
183
+ provider: 'google_maps',
184
+ max_overall_rating: 4,
185
+ min_total_reviews: 20,
186
+ limit: 10,
187
+ })
188
+
189
+ expect(result.isError).toBeFalsy()
190
+ const parsed = JSON.parse(result.content[0].text)
191
+ expect(parsed._meta.provider).toBe('google_maps')
192
+ expect(parsed.data.places.map((p: { title: string }) => p.title)).toEqual(['Strict match — keep'])
193
+ } finally {
194
+ gm.async!.timeoutMs = orig.t
195
+ gm.async!.pollIntervalMs = orig.i
196
+ }
197
+ })
198
+
103
199
  it('all providers fail — returns isError', async () => {
104
200
  globalThis.fetch = vi.fn(async () => {
105
201
  return new Response(JSON.stringify({ error: 'down' }), { status: 503 })