connect.schema.test.ts468 lines · main
1import { describe, expect, test } from 'vitest'
2
3import { resolveSteps } from './connect.resolver'
4import { connectSchema, EXTRA_PACKAGES, INSTALL_COMMANDS } from './connect.schema'
5import type { ConnectState } from './Connect.types'
6
7// ============================================================================
8// Schema Structure Tests
9// ============================================================================
10
11describe('connect.schema:structure', () => {
12 test('should have all required modes', () => {
13 const modeIds = connectSchema.modes.map((m) => m.id)
14 expect(modeIds).toContain('framework')
15 expect(modeIds).toContain('direct')
16 expect(modeIds).toContain('orm')
17 expect(modeIds).toContain('mcp')
18 })
19
20 test('each mode should have required properties', () => {
21 connectSchema.modes.forEach((mode) => {
22 expect(mode.id).toBeDefined()
23 expect(mode.label).toBeDefined()
24 expect(mode.description).toBeDefined()
25 expect(mode.fields).toBeDefined()
26 expect(Array.isArray(mode.fields)).toBe(true)
27 })
28 })
29
30 test('framework mode should have correct fields', () => {
31 const frameworkMode = connectSchema.modes.find((m) => m.id === 'framework')
32 expect(frameworkMode?.fields).toContain('framework')
33 expect(frameworkMode?.fields).toContain('frameworkVariant')
34 expect(frameworkMode?.fields).toContain('library')
35 expect(frameworkMode?.fields).toContain('frameworkUi')
36 })
37
38 test('direct mode should have correct fields', () => {
39 const directMode = connectSchema.modes.find((m) => m.id === 'direct')
40 expect(directMode?.fields).toContain('connectionMethod')
41 expect(directMode?.fields).toContain('useSharedPooler')
42 expect(directMode?.fields).toContain('connectionType')
43 })
44
45 test('orm mode should have correct fields', () => {
46 const ormMode = connectSchema.modes.find((m) => m.id === 'orm')
47 expect(ormMode?.fields).toContain('orm')
48 })
49
50 test('mcp mode should have correct fields', () => {
51 const mcpMode = connectSchema.modes.find((m) => m.id === 'mcp')
52 expect(mcpMode?.fields).toContain('mcpClient')
53 expect(mcpMode?.fields).toContain('mcpReadonly')
54 expect(mcpMode?.fields).toContain('mcpFeatures')
55 })
56
57 test('all mode fields should exist in fields definition', () => {
58 connectSchema.modes.forEach((mode) => {
59 mode.fields.forEach((fieldId) => {
60 expect(
61 connectSchema.fields[fieldId],
62 `Field "${fieldId}" in mode "${mode.id}" should exist in fields definition`
63 ).toBeDefined()
64 })
65 })
66 })
67})
68
69// ============================================================================
70// Field Definition Tests
71// ============================================================================
72
73describe('connect.schema:fields', () => {
74 test('framework field should have correct type', () => {
75 const field = connectSchema.fields.framework
76 expect(field.type).toBe('select')
77 expect(field.options).toEqual({ source: 'frameworks' })
78 expect(field.defaultValue).toBe('nextjs')
79 })
80
81 test('frameworkVariant field should depend on framework', () => {
82 const field = connectSchema.fields.frameworkVariant
83 expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
84 })
85
86 test('frameworkUi field should be a switch type', () => {
87 const field = connectSchema.fields.frameworkUi
88 expect(field.type).toBe('switch')
89 expect(field.defaultValue).toBe(false)
90 expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
91 })
92
93 test('connectionMethod field should have radio-list type', () => {
94 const field = connectSchema.fields.connectionMethod
95 expect(field.type).toBe('radio-list')
96 expect(field.options).toEqual({ source: 'connectionMethods' })
97 expect(field.defaultValue).toBe('direct')
98 })
99
100 test('useSharedPooler field should depend on transaction connection method', () => {
101 const field = connectSchema.fields.useSharedPooler
102 expect(field.type).toBe('switch')
103 expect(field.dependsOn).toEqual({ connectionMethod: ['transaction'] })
104 })
105
106 test('orm field should have radio-list type', () => {
107 const field = connectSchema.fields.orm
108 expect(field.type).toBe('radio-list')
109 expect(field.options).toEqual({ source: 'orms' })
110 expect(field.defaultValue).toBe('prisma')
111 })
112
113 test('mcpClient field should have select type', () => {
114 const field = connectSchema.fields.mcpClient
115 expect(field.type).toBe('select')
116 expect(field.options).toEqual({ source: 'mcpClients' })
117 expect(field.defaultValue).toBe('claude-code')
118 })
119
120 test('mcpFeatures field should have multi-select type', () => {
121 const field = connectSchema.fields.mcpFeatures
122 expect(field.type).toBe('multi-select')
123 expect(field.options).toEqual({ source: 'mcpFeatures' })
124 })
125})
126
127// ============================================================================
128// Install Commands Tests
129// ============================================================================
130
131describe('connect.schema:INSTALL_COMMANDS', () => {
132 test('should have install command for briven-js', () => {
133 expect(INSTALL_COMMANDS.brivenjs).toBe('npm install @supabase/supabase-js')
134 })
135
136 test('should have install command for briven-py', () => {
137 expect(INSTALL_COMMANDS.brivenpy).toBe('pip install briven')
138 })
139
140 test('should have install command for briven-flutter', () => {
141 expect(INSTALL_COMMANDS.brivenflutter).toBe('flutter pub add briven_flutter')
142 })
143
144 test('should have install command for briven-swift', () => {
145 expect(INSTALL_COMMANDS.brivenswift).toContain('swift package add-dependency')
146 })
147
148 test('should have install command for briven-kt', () => {
149 expect(INSTALL_COMMANDS.brivenkt).toContain('io.github.jan-tennert.briven')
150 })
151})
152
153describe('connect.schema:EXTRA_PACKAGES', () => {
154 test('should have @supabase/ssr as extra package for nextjs app router with brivenjs', () => {
155 expect(EXTRA_PACKAGES.brivenjs?.['nextjs/app']).toContain('@supabase/ssr')
156 })
157
158 test('should not have extra packages for nextjs pages router', () => {
159 expect(EXTRA_PACKAGES.brivenjs?.['nextjs/pages']).toBeUndefined()
160 })
161
162 test('should have @supabase/ssr as extra package for remix with brivenjs', () => {
163 expect(EXTRA_PACKAGES.brivenjs?.remix).toContain('@supabase/ssr')
164 })
165})
166
167// ============================================================================
168// Steps Resolution Integration Tests
169// ============================================================================
170
171describe('connect.schema:steps resolution', () => {
172 describe('framework mode steps', () => {
173 test('should resolve steps for nextjs without shadcn', () => {
174 const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: false }
175 const steps = resolveSteps(connectSchema, state)
176
177 expect(steps.length).toBeGreaterThan(0)
178 expect(steps.find((s) => s.id === 'install')).toBeDefined()
179 expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
180 })
181
182 test('should use "Install packages" title for nextjs app router', () => {
183 const state: ConnectState = {
184 mode: 'framework',
185 framework: 'nextjs',
186 frameworkVariant: 'app',
187 frameworkUi: false,
188 }
189 const steps = resolveSteps(connectSchema, state)
190 const installStep = steps.find((s) => s.id === 'install')
191
192 expect(installStep?.title).toBe('Install packages')
193 })
194
195 test('should use "Install package" title for nextjs pages router', () => {
196 const state: ConnectState = {
197 mode: 'framework',
198 framework: 'nextjs',
199 frameworkVariant: 'pages',
200 frameworkUi: false,
201 }
202 const steps = resolveSteps(connectSchema, state)
203 const installStep = steps.find((s) => s.id === 'install')
204
205 expect(installStep?.title).toBe('Install package')
206 })
207
208 test('should use "Install packages" title for remix install step', () => {
209 const state: ConnectState = { mode: 'framework', framework: 'remix' }
210 const steps = resolveSteps(connectSchema, state)
211 const installStep = steps.find((s) => s.id === 'install')
212
213 expect(installStep?.title).toBe('Install packages')
214 })
215
216 test('should use "Install package" title for frameworks without extra packages', () => {
217 const state: ConnectState = { mode: 'framework', framework: 'vuejs' }
218 const steps = resolveSteps(connectSchema, state)
219 const installStep = steps.find((s) => s.id === 'install')
220
221 expect(installStep?.title).toBe('Install package')
222 })
223
224 test('should resolve shadcn steps for nextjs with frameworkUi true', () => {
225 const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
226 const steps = resolveSteps(connectSchema, state)
227
228 expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
229 expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
230 expect(steps.find((s) => s.id === 'shadcn-explore')).toBeDefined()
231 })
232
233 test('should resolve steps for react without shadcn', () => {
234 const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: false }
235 const steps = resolveSteps(connectSchema, state)
236
237 expect(steps.length).toBeGreaterThan(0)
238 expect(steps.find((s) => s.id === 'install')).toBeDefined()
239 })
240
241 test('should resolve shadcn steps for react with frameworkUi true', () => {
242 const state: ConnectState = { mode: 'framework', framework: 'react', frameworkUi: true }
243 const steps = resolveSteps(connectSchema, state)
244
245 expect(steps.find((s) => s.id === 'shadcn-add')).toBeDefined()
246 expect(steps.find((s) => s.id === 'shadcn-env')).toBeDefined()
247 })
248
249 test('should resolve default steps for other frameworks', () => {
250 const state: ConnectState = { mode: 'framework', framework: 'remix' }
251 const steps = resolveSteps(connectSchema, state)
252
253 expect(steps.length).toBeGreaterThan(0)
254 expect(steps.find((s) => s.id === 'install')).toBeDefined()
255 expect(steps.find((s) => s.id === 'configure')).toBeDefined()
256 })
257 })
258
259 describe('direct mode steps', () => {
260 test('should resolve connection step for default direct mode', () => {
261 const state: ConnectState = { mode: 'direct' }
262 const steps = resolveSteps(connectSchema, state)
263
264 expect(steps.find((s) => s.id === 'connection')).toBeDefined()
265 })
266
267 test('should resolve install and files steps for nodejs connection type', () => {
268 const state: ConnectState = { mode: 'direct', connectionType: 'nodejs' }
269 const steps = resolveSteps(connectSchema, state)
270
271 expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
272 expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
273 })
274
275 test('should resolve install and files steps for golang connection type', () => {
276 const state: ConnectState = { mode: 'direct', connectionType: 'golang' }
277 const steps = resolveSteps(connectSchema, state)
278
279 expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
280 expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
281 })
282
283 test('should resolve install and files steps for python connection type', () => {
284 const state: ConnectState = { mode: 'direct', connectionType: 'python' }
285 const steps = resolveSteps(connectSchema, state)
286
287 expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
288 })
289
290 test('should resolve install and files steps for dotnet connection type', () => {
291 const state: ConnectState = { mode: 'direct', connectionType: 'dotnet' }
292 const steps = resolveSteps(connectSchema, state)
293
294 expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
295 })
296 })
297
298 describe('orm mode steps', () => {
299 test('should resolve install and configure steps for prisma', () => {
300 const state: ConnectState = { mode: 'orm', orm: 'prisma' }
301 const steps = resolveSteps(connectSchema, state)
302
303 expect(steps.find((s) => s.id === 'install')).toBeDefined()
304 expect(steps.find((s) => s.id === 'configure')).toBeDefined()
305 expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
306 })
307
308 test('should resolve install and configure steps for drizzle', () => {
309 const state: ConnectState = { mode: 'orm', orm: 'drizzle' }
310 const steps = resolveSteps(connectSchema, state)
311
312 expect(steps.find((s) => s.id === 'install')).toBeDefined()
313 expect(steps.find((s) => s.id === 'configure')).toBeDefined()
314 })
315 })
316
317 describe('mcp mode steps', () => {
318 test('should resolve configure step for cursor client', () => {
319 const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
320 const steps = resolveSteps(connectSchema, state)
321
322 expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
323 expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
324 })
325
326 test('should resolve codex-specific steps for codex client', () => {
327 const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
328 const steps = resolveSteps(connectSchema, state)
329
330 expect(steps.find((s) => s.id === 'codex-add-server')).toBeDefined()
331 expect(steps.find((s) => s.id === 'codex-enable-remote')).toBeDefined()
332 expect(steps.find((s) => s.id === 'codex-authenticate')).toBeDefined()
333 expect(steps.find((s) => s.id === 'codex-verify')).toBeDefined()
334 })
335
336 test('should resolve claude-code-specific steps for claude-code client', () => {
337 const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
338 const steps = resolveSteps(connectSchema, state)
339
340 expect(steps.find((s) => s.id === 'claude-add-server')).toBeDefined()
341 expect(steps.find((s) => s.id === 'claude-authenticate')).toBeDefined()
342 })
343
344 test('should resolve default mcp steps for other clients', () => {
345 const state: ConnectState = { mode: 'mcp', mcpClient: 'unknown-client' }
346 const steps = resolveSteps(connectSchema, state)
347
348 expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
349 })
350 })
351
352 describe('skills install step', () => {
353 test('should include skills install step in all modes', () => {
354 const modes: ConnectState['mode'][] = ['framework', 'direct', 'orm', 'mcp']
355
356 modes.forEach((mode) => {
357 const state: ConnectState = { mode }
358 const steps = resolveSteps(connectSchema, state)
359
360 expect(
361 steps.find((s) => s.id === 'install-skills'),
362 `Mode "${mode}" should have skills install step`
363 ).toBeDefined()
364 })
365 })
366 })
367})
368
369// ============================================================================
370// Step Content Path Tests
371// ============================================================================
372
373describe('connect.schema:step content paths', () => {
374 test('install step should have valid content path', () => {
375 const state: ConnectState = { mode: 'framework', framework: 'nextjs' }
376 const steps = resolveSteps(connectSchema, state)
377 const installStep = steps.find((s) => s.id === 'install')
378
379 expect(installStep?.content).toBe('steps/install')
380 })
381
382 test('shadcn command step should have valid content path', () => {
383 const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
384 const steps = resolveSteps(connectSchema, state)
385 const shadcnStep = steps.find((s) => s.id === 'shadcn-add')
386
387 expect(shadcnStep?.content).toBe('steps/shadcn/command')
388 })
389
390 test('shadcn explore step should have valid content path', () => {
391 const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
392 const steps = resolveSteps(connectSchema, state)
393 const exploreStep = steps.find((s) => s.id === 'shadcn-explore')
394
395 expect(exploreStep?.content).toBe('steps/shadcn/explore')
396 })
397
398 test('shadcn env step should have valid content path', () => {
399 const state: ConnectState = { mode: 'framework', framework: 'nextjs', frameworkUi: true }
400 const steps = resolveSteps(connectSchema, state)
401 const envStep = steps.find((s) => s.id === 'shadcn-env')
402
403 expect(envStep?.content).toBe('steps/shadcn/env')
404 })
405
406 test('direct connection step should have valid content path', () => {
407 const state: ConnectState = { mode: 'direct' }
408 const steps = resolveSteps(connectSchema, state)
409 const connectionStep = steps.find((s) => s.id === 'connection')
410
411 expect(connectionStep?.content).toBe('steps/direct-connection')
412 })
413
414 test('skills install step should have valid content path', () => {
415 const state: ConnectState = { mode: 'framework' }
416 const steps = resolveSteps(connectSchema, state)
417 const skillsStep = steps.find((s) => s.id === 'install-skills')
418
419 expect(skillsStep?.content).toBe('steps/skills-install')
420 })
421
422 test('orm configure step should use template content path', () => {
423 // The ORM configure step uses a template {{orm}} that gets resolved
424 // by the dynamic import system, not the resolver
425 const state: ConnectState = { mode: 'orm', orm: 'prisma' }
426 const steps = resolveSteps(connectSchema, state)
427 const configureStep = steps.find((s) => s.id === 'configure')
428
429 // The content path uses template syntax for the component loader
430 expect(configureStep?.content).toBe('{{orm}}')
431 })
432
433 test('mcp cursor configure step should have valid content path', () => {
434 const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
435 const steps = resolveSteps(connectSchema, state)
436 const configureStep = steps.find((s) => s.id === 'configure-mcp')
437
438 expect(configureStep?.content).toBe('steps/mcp/cursor')
439 })
440
441 test('codex steps should have valid content paths', () => {
442 const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
443 const steps = resolveSteps(connectSchema, state)
444
445 expect(steps.find((s) => s.id === 'codex-add-server')?.content).toBe(
446 'steps/mcp/codex/add-server'
447 )
448 expect(steps.find((s) => s.id === 'codex-enable-remote')?.content).toBe(
449 'steps/mcp/codex/enable-remote'
450 )
451 expect(steps.find((s) => s.id === 'codex-authenticate')?.content).toBe(
452 'steps/mcp/codex/authenticate'
453 )
454 expect(steps.find((s) => s.id === 'codex-verify')?.content).toBe('steps/mcp/codex/verify')
455 })
456
457 test('claude-code steps should have valid content paths', () => {
458 const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
459 const steps = resolveSteps(connectSchema, state)
460
461 expect(steps.find((s) => s.id === 'claude-add-server')?.content).toBe(
462 'steps/mcp/claude-code/add-server'
463 )
464 expect(steps.find((s) => s.id === 'claude-authenticate')?.content).toBe(
465 'steps/mcp/claude-code/authenticate'
466 )
467 })
468})