@coldiq/mcp 5.4.6 → 5.4.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.
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { initClient } from '../../src/client.js'
3
+ import {
4
+ listWebsiteVisitorsSitesDescription,
5
+ listWebsiteVisitorsSitesHandler,
6
+ listWebsiteVisitorsSitesSchema,
7
+ listWebsiteVisitorsSitesName,
8
+ } from '../../src/tools/list-website-visitors-sites.js'
9
+
10
+ describe('list_website_visitors_sites', () => {
11
+ const originalFetch = globalThis.fetch
12
+
13
+ beforeEach(() => {
14
+ initClient('http://test-api.local', 'test-key')
15
+ })
16
+
17
+ afterEach(() => {
18
+ globalThis.fetch = originalFetch
19
+ vi.restoreAllMocks()
20
+ })
21
+
22
+ type Call = { path: string; method: string }
23
+
24
+ /** Route each request by pathname; record every call in order. */
25
+ function captureByPath(routes: Record<string, { payload: unknown; status?: number }>) {
26
+ const calls: Call[] = []
27
+ globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
28
+ const path = new URL(url.toString()).pathname
29
+ calls.push({ path, method: init?.method ?? 'GET' })
30
+ const route = routes[path]
31
+ if (!route) return new Response(JSON.stringify({ error: 'unexpected path' }), { status: 500 })
32
+ return new Response(JSON.stringify(route.payload), { status: route.status ?? 200 })
33
+ }) as typeof fetch
34
+ return calls
35
+ }
36
+
37
+ it('is named list_website_visitors_sites', () => {
38
+ expect(listWebsiteVisitorsSitesName).toBe('list_website_visitors_sites')
39
+ })
40
+
41
+ it('GETs /visitor-id/websites when no website_id is given', async () => {
42
+ const list = { websites: [{ id: 7, domain: 'coldiq.com', status: 'active' }] }
43
+ const calls = captureByPath({ '/v1/visitor-id/websites': { payload: list } })
44
+
45
+ const result = await listWebsiteVisitorsSitesHandler({})
46
+
47
+ expect(calls).toEqual([{ path: '/v1/visitor-id/websites', method: 'GET' }])
48
+ expect(result.isError).toBeFalsy()
49
+ expect(JSON.parse(result.content[0].text)).toEqual({ data: list })
50
+ })
51
+
52
+ it('with website_id GETs the website and its usage and merges them as { website, usage }', async () => {
53
+ const website = { id: 7, domain: 'coldiq.com', embed_html: '<script></script>', install: { is_installed: true } }
54
+ const usage = { people: 3, companies: 12, credits: 30 }
55
+ const calls = captureByPath({
56
+ '/v1/visitor-id/websites/7': { payload: website },
57
+ '/v1/visitor-id/websites/7/usage': { payload: usage },
58
+ })
59
+
60
+ const result = await listWebsiteVisitorsSitesHandler({ website_id: 7 })
61
+
62
+ // Both reads are issued together (Promise.all), so only the set is asserted, not the order.
63
+ expect(calls.map((call) => call.path).sort()).toEqual([
64
+ '/v1/visitor-id/websites/7',
65
+ '/v1/visitor-id/websites/7/usage',
66
+ ])
67
+ expect(calls.every((call) => call.method === 'GET')).toBe(true)
68
+ expect(result.isError).toBeFalsy()
69
+ expect(JSON.parse(result.content[0].text)).toEqual({ data: { website, usage } })
70
+ })
71
+
72
+ it('shapes a list failure as isError with status and data', async () => {
73
+ captureByPath({ '/v1/visitor-id/websites': { payload: { error: 'Unauthorized' }, status: 401 } })
74
+
75
+ const result = await listWebsiteVisitorsSitesHandler({})
76
+
77
+ expect(result.isError).toBe(true)
78
+ expect(JSON.parse(result.content[0].text)).toEqual({
79
+ error: 'Failed to list tracked websites',
80
+ status: 401,
81
+ data: { error: 'Unauthorized' },
82
+ })
83
+ })
84
+
85
+ it('shapes a website 404 as isError with status and data', async () => {
86
+ captureByPath({
87
+ '/v1/visitor-id/websites/999': { payload: { error: 'Website not found' }, status: 404 },
88
+ })
89
+
90
+ const result = await listWebsiteVisitorsSitesHandler({ website_id: 999 })
91
+
92
+ expect(result.isError).toBe(true)
93
+ expect(JSON.parse(result.content[0].text)).toEqual({
94
+ error: 'Failed to read tracked website',
95
+ status: 404,
96
+ data: { error: 'Website not found' },
97
+ })
98
+ })
99
+
100
+ it('keeps the website when the usage read fails: { website, usage: null, usage_error } (T13b)', async () => {
101
+ const website = { id: 7, domain: 'coldiq.com', embed_html: '<script></script>', install: null }
102
+ captureByPath({
103
+ '/v1/visitor-id/websites/7': { payload: website },
104
+ '/v1/visitor-id/websites/7/usage': { payload: { error: 'Provider temporarily unavailable' }, status: 502 },
105
+ })
106
+
107
+ const result = await listWebsiteVisitorsSitesHandler({ website_id: 7 })
108
+
109
+ expect(result.isError).toBeFalsy()
110
+ expect(JSON.parse(result.content[0].text)).toEqual({
111
+ data: {
112
+ website,
113
+ usage: null,
114
+ usage_error: { status: 502, data: { error: 'Provider temporarily unavailable' } },
115
+ },
116
+ })
117
+ })
118
+
119
+ it('describes the resilient shape, the live rate, the 409 split, install null and the member rule (T13b)', () => {
120
+ expect(listWebsiteVisitorsSitesDescription).toContain('usage_error')
121
+ expect(listWebsiteVisitorsSitesDescription).toContain('6× a company')
122
+ expect(listWebsiteVisitorsSitesDescription).toContain('credits_per_person')
123
+ expect(listWebsiteVisitorsSitesDescription).toMatch(/never numbers from memory/)
124
+ expect(listWebsiteVisitorsSitesDescription).not.toMatch(/\d+(\.\d+)? credits/)
125
+ expect(listWebsiteVisitorsSitesDescription).not.toMatch(/about \d/)
126
+ expect(listWebsiteVisitorsSitesDescription).toMatch(/409 there whose message mentions a deactivated workspace/)
127
+ expect(listWebsiteVisitorsSitesDescription).toContain('do not retry')
128
+ expect(listWebsiteVisitorsSitesDescription).toMatch(/any other 409/)
129
+ expect(listWebsiteVisitorsSitesDescription).not.toContain('400')
130
+ expect(listWebsiteVisitorsSitesDescription).toMatch(/`install` null = state unknown/)
131
+ expect(listWebsiteVisitorsSitesDescription).toContain('POST/PATCH/PUT/DELETE')
132
+ expect(listWebsiteVisitorsSitesDescription).toContain('403')
133
+ })
134
+
135
+ it('accepts only an optional integer website_id at the schema layer', () => {
136
+ expect(Object.keys(listWebsiteVisitorsSitesSchema)).toEqual(['website_id'])
137
+ expect(listWebsiteVisitorsSitesSchema.website_id.safeParse(undefined).success).toBe(true)
138
+ expect(listWebsiteVisitorsSitesSchema.website_id.safeParse(7).success).toBe(true)
139
+ expect(listWebsiteVisitorsSitesSchema.website_id.safeParse(7.5).success).toBe(false)
140
+ expect(listWebsiteVisitorsSitesSchema.website_id.safeParse('7').success).toBe(false)
141
+ })
142
+ })
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { initClient } from '../../src/client.js'
3
+ import {
4
+ setupWebsiteVisitorsDescription,
5
+ setupWebsiteVisitorsHandler,
6
+ setupWebsiteVisitorsSchema,
7
+ setupWebsiteVisitorsName,
8
+ } from '../../src/tools/setup-website-visitors.js'
9
+
10
+ describe('setup_website_visitors', () => {
11
+ const originalFetch = globalThis.fetch
12
+
13
+ beforeEach(() => {
14
+ initClient('http://test-api.local', 'test-key')
15
+ })
16
+
17
+ afterEach(() => {
18
+ globalThis.fetch = originalFetch
19
+ vi.restoreAllMocks()
20
+ })
21
+
22
+ function capture(payload: unknown, status = 201) {
23
+ const seen: { url: string; method: string; body: unknown } = { url: '', method: '', body: undefined }
24
+ globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
25
+ seen.url = url.toString()
26
+ seen.method = init?.method ?? 'GET'
27
+ seen.body = init?.body ? JSON.parse(String(init.body)) : undefined
28
+ return new Response(JSON.stringify(payload), { status })
29
+ }) as typeof fetch
30
+ return seen
31
+ }
32
+
33
+ it('is named setup_website_visitors', () => {
34
+ expect(setupWebsiteVisitorsName).toBe('setup_website_visitors')
35
+ })
36
+
37
+ it('quotes the live rate and the 6× person/company ratio, never an absolute credit number (T13b)', () => {
38
+ expect(setupWebsiteVisitorsDescription).toContain('6× a company')
39
+ expect(setupWebsiteVisitorsDescription).toContain('credits_per_person')
40
+ expect(setupWebsiteVisitorsDescription).toContain('credits_per_company')
41
+ expect(setupWebsiteVisitorsDescription).toContain('list_website_visitors_sites')
42
+ expect(setupWebsiteVisitorsDescription).toMatch(/never numbers from memory/)
43
+ // No literal price: "N credits", "about N", "N per identified".
44
+ expect(setupWebsiteVisitorsDescription).not.toMatch(/\d+(\.\d+)? credits/)
45
+ expect(setupWebsiteVisitorsDescription).not.toMatch(/about \d/)
46
+ expect(setupWebsiteVisitorsDescription).not.toMatch(/\d+ per identified/)
47
+ })
48
+
49
+ it('tells the agent what a deactivated-workspace 409 and any other 409 mean (T13b)', () => {
50
+ expect(setupWebsiteVisitorsDescription).toMatch(/409 whose message mentions a deactivated workspace/)
51
+ expect(setupWebsiteVisitorsDescription).toContain('do not retry')
52
+ expect(setupWebsiteVisitorsDescription).toContain('contact ColdIQ support')
53
+ expect(setupWebsiteVisitorsDescription).toMatch(/any other 409/)
54
+ expect(setupWebsiteVisitorsDescription).not.toContain('400')
55
+ })
56
+
57
+ it('explains install: null as unknown and the member 403 rule (T13b)', () => {
58
+ expect(setupWebsiteVisitorsDescription).toMatch(/`install` can be null/)
59
+ expect(setupWebsiteVisitorsDescription).toMatch(/never report "not installed" from null/)
60
+ expect(setupWebsiteVisitorsDescription).toContain('POST/PATCH/PUT/DELETE')
61
+ expect(setupWebsiteVisitorsDescription).toContain('403')
62
+ expect(setupWebsiteVisitorsDescription).toContain('workspace owner or an admin')
63
+ })
64
+
65
+ it('POSTs the domain to /visitor-id/websites and returns the website', async () => {
66
+ const website = { id: 7, domain: 'coldiq.com', embed_html: '<script></script>', install: { is_installed: false } }
67
+ const seen = capture(website)
68
+
69
+ const result = await setupWebsiteVisitorsHandler({ domain: 'coldiq.com' })
70
+
71
+ expect(seen.method).toBe('POST')
72
+ expect(new URL(seen.url).pathname).toBe('/v1/visitor-id/websites')
73
+ expect(seen.body).toEqual({ domain: 'coldiq.com' })
74
+ expect(result.isError).toBeFalsy()
75
+ expect(JSON.parse(result.content[0].text)).toEqual({ data: website })
76
+ })
77
+
78
+ it('forwards the optional name', async () => {
79
+ const seen = capture({ id: 7 })
80
+
81
+ await setupWebsiteVisitorsHandler({ domain: 'coldiq.com', name: 'ColdIQ' })
82
+
83
+ expect(seen.body).toEqual({ domain: 'coldiq.com', name: 'ColdIQ' })
84
+ })
85
+
86
+ it('shapes a 409 (domain already tracked) as isError with status and data', async () => {
87
+ capture({ error: 'coldiq.com already has a pixel on this account.' }, 409)
88
+
89
+ const result = await setupWebsiteVisitorsHandler({ domain: 'coldiq.com' })
90
+
91
+ expect(result.isError).toBe(true)
92
+ const parsed = JSON.parse(result.content[0].text)
93
+ expect(parsed).toEqual({
94
+ error: 'Failed to set up website visitor identification',
95
+ status: 409,
96
+ data: { error: 'coldiq.com already has a pixel on this account.' },
97
+ })
98
+ })
99
+
100
+ it('requires a non-empty domain at the schema layer and mirrors the API body fields', () => {
101
+ expect(Object.keys(setupWebsiteVisitorsSchema).sort()).toEqual(['domain', 'name'])
102
+ expect(setupWebsiteVisitorsSchema.domain.safeParse('').success).toBe(false)
103
+ expect(setupWebsiteVisitorsSchema.domain.safeParse('coldiq.com').success).toBe(true)
104
+ expect(setupWebsiteVisitorsSchema.name.safeParse(undefined).success).toBe(true)
105
+ })
106
+ })