connect.schema.test.ts 18 KB

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