create-ailk 0.2.0 → 0.3.0

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.
Files changed (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -1,341 +0,0 @@
1
- /**
2
- * @file waitlist-experiments.test.ts
3
- * @description Unit tests for the waitlist-experiments service (#4661 D5;
4
- * extended #4726 for `materializeWaitlistFlows`).
5
- *
6
- * External dependency boundaries mocked: @working-theory/database (auto-mock
7
- * via `../../lib/__mocks__/prisma.ts`, mirroring flow-engine.test.ts), plus
8
- * — since #4726 composes `upsertFlowDefinition` from `flow-engine.ts` — the
9
- * same `@working-theory/email` / `@working-theory/license` / `../aeo-score.js`
10
- * boundary mocks flow-engine.test.ts uses (flow-engine.ts's own import chain
11
- * reaches all three; `@working-theory/email` in particular re-exports a
12
- * `.tsx` React-email template Jest's CJS `moduleFileExtensions` can't
13
- * resolve unmocked).
14
- */
15
- // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
16
- jest.mock("../../lib/prisma.js");
17
-
18
- jest.mock("@working-theory/email", () => ({
19
- sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
20
- sendDeliverableEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-deliverable-test" }),
21
- }));
22
-
23
- jest.mock("@working-theory/license", () => ({
24
- checkLicense: jest.fn().mockResolvedValue({
25
- tier: "free",
26
- plan: null,
27
- features: [],
28
- expiresAt: null,
29
- revoked: false,
30
- }),
31
- }));
32
-
33
- // eslint-disable-next-line no-restricted-syntax -- aeo-score.ts is a network boundary (external crawl), same class as the DB mock above
34
- jest.mock("../aeo-score.js", () => ({
35
- scoreUrl: jest.fn(),
36
- loadExampleScore: jest.fn(),
37
- resolveScoreBudgetMs: () => 6000,
38
- }));
39
-
40
- import { prisma } from "../../lib/prisma.js";
41
- import {
42
- materializeWaitlistFlows,
43
- seedWaitlistExperiments,
44
- upsertWaitlistExperiment,
45
- } from "../waitlist-experiments";
46
-
47
- const en = (s: string) => ({ en: s });
48
- const SITE_ID = "site-abc";
49
-
50
- beforeEach(() => {
51
- jest.clearAllMocks();
52
- });
53
-
54
- function validConfig(key: string) {
55
- return {
56
- key,
57
- mode: "lead-capture" as const,
58
- status: "draft" as const,
59
- waitlists: [{ key: "main", flowSlug: key }],
60
- questionSet: {
61
- version: 1 as const,
62
- questions: [
63
- {
64
- id: "email",
65
- prompt: en("Your email"),
66
- answerType: "email" as const,
67
- mapsTo: "email" as const,
68
- },
69
- ],
70
- },
71
- tracking: {
72
- version: 1 as const,
73
- utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
74
- waitlists: { main: { campaign: "launch", channels: ["organic"] } },
75
- },
76
- stripeConfigRef: null,
77
- };
78
- }
79
-
80
- /** `validConfig` with two waitlists — for the per-waitlist materialize loop. */
81
- function validConfigWithTwoWaitlists(key: string) {
82
- const base = validConfig(key);
83
- return {
84
- ...base,
85
- waitlists: [
86
- { key: "main", flowSlug: `${key}-main` },
87
- { key: "vip", flowSlug: `${key}-vip` },
88
- ],
89
- tracking: {
90
- ...base.tracking,
91
- waitlists: {
92
- main: { campaign: "launch", channels: ["organic"] },
93
- vip: { campaign: "launch-vip", channels: ["organic"] },
94
- },
95
- },
96
- };
97
- }
98
-
99
- /**
100
- * `validConfig` with a SECOND question whose prompt carries no locale in
101
- * common with the first — `waitlistExperimentConfigSchema` has no
102
- * locale-parity check (it validates the shape of each question's
103
- * `LocalizedString` independently), so this config parses successfully, but
104
- * `compileWaitlistFlow` refuses `unsupported_by_engine` ("no locale is
105
- * common to every question's prompt") — the compile-time-only failure #4726
106
- * AC2 targets. Never a `showIf`/score atom: those the SCHEMA-level
107
- * predicate cross-validation (`walkPredicateAgainstQuestions`) would refuse
108
- * on its own scoring-register check, before `compileWaitlistFlow` is ever
109
- * reached — this locale route isolates the compile-only failure cleanly.
110
- */
111
- function configWithUncompilableFlow(key: string) {
112
- const base = validConfig(key);
113
- return {
114
- ...base,
115
- questionSet: {
116
- ...base.questionSet,
117
- questions: [
118
- ...base.questionSet.questions,
119
- { id: "name", prompt: { fr: "Votre nom" }, answerType: "text" as const },
120
- ],
121
- },
122
- };
123
- }
124
-
125
- describe("upsertWaitlistExperiment (D5)", () => {
126
- it("validates + upserts a well-formed config on [siteId, key], writing mode/status/config", async () => {
127
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({
128
- id: "wlexp_1",
129
- });
130
-
131
- const config = validConfig("my-experiment");
132
- const result = await upsertWaitlistExperiment(SITE_ID, config);
133
-
134
- expect(result).toEqual({ ok: true, id: "wlexp_1" });
135
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledWith(
136
- expect.objectContaining({
137
- where: { siteId_key: { siteId: SITE_ID, key: "my-experiment" } },
138
- create: expect.objectContaining({
139
- siteId: SITE_ID,
140
- key: "my-experiment",
141
- mode: "lead-capture",
142
- status: "draft",
143
- }),
144
- update: expect.objectContaining({
145
- mode: "lead-capture",
146
- status: "draft",
147
- }),
148
- }),
149
- );
150
- });
151
-
152
- it("rejects an invalid config without touching prisma", async () => {
153
- const result = await upsertWaitlistExperiment(SITE_ID, { not: "a config" });
154
-
155
- expect(result.ok).toBe(false);
156
- if (!result.ok) expect(result.error.kind).toBe("invalid_input");
157
- expect(prisma.waitlistExperiment.upsert).not.toHaveBeenCalled();
158
- });
159
-
160
- it("is the ONLY write path — a single upsert call, no second Prisma write", async () => {
161
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({
162
- id: "wlexp_1",
163
- });
164
-
165
- await upsertWaitlistExperiment(SITE_ID, validConfig("my-experiment"));
166
-
167
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledTimes(1);
168
- });
169
- });
170
-
171
- describe("materializeWaitlistFlows (#4726) — the compile+upsert path and the failure path", () => {
172
- it("compiles + upserts one flow_definitions row for a single-waitlist config, slug = flowSlug", async () => {
173
- (prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({
174
- id: "flow_1",
175
- });
176
-
177
- const config = validConfig("my-experiment");
178
- const result = await materializeWaitlistFlows(SITE_ID, config);
179
-
180
- expect(result).toEqual({ ok: true });
181
- expect(prisma.flowDefinition.upsert).toHaveBeenCalledTimes(1);
182
- expect(prisma.flowDefinition.upsert).toHaveBeenCalledWith(
183
- expect.objectContaining({
184
- where: { siteId_slug: { siteId: SITE_ID, slug: "my-experiment" } },
185
- create: expect.objectContaining({
186
- siteId: SITE_ID,
187
- slug: "my-experiment",
188
- config: expect.objectContaining({ slug: "my-experiment" }),
189
- }),
190
- }),
191
- );
192
- });
193
-
194
- it("compiles + upserts one flow_definitions row PER waitlists[] entry, each named by its own flowSlug", async () => {
195
- (prisma.flowDefinition.upsert as jest.Mock)
196
- .mockResolvedValueOnce({ id: "flow_main" })
197
- .mockResolvedValueOnce({ id: "flow_vip" });
198
-
199
- const config = validConfigWithTwoWaitlists("two-wl");
200
- const result = await materializeWaitlistFlows(SITE_ID, config);
201
-
202
- expect(result).toEqual({ ok: true });
203
- expect(prisma.flowDefinition.upsert).toHaveBeenCalledTimes(2);
204
- expect(prisma.flowDefinition.upsert).toHaveBeenNthCalledWith(
205
- 1,
206
- expect.objectContaining({
207
- where: { siteId_slug: { siteId: SITE_ID, slug: "two-wl-main" } },
208
- }),
209
- );
210
- expect(prisma.flowDefinition.upsert).toHaveBeenNthCalledWith(
211
- 2,
212
- expect.objectContaining({
213
- where: { siteId_slug: { siteId: SITE_ID, slug: "two-wl-vip" } },
214
- }),
215
- );
216
- });
217
-
218
- it("rejects an invalid config without touching prisma", async () => {
219
- const result = await materializeWaitlistFlows(SITE_ID, { not: "a config" });
220
-
221
- expect(result.ok).toBe(false);
222
- if (!result.ok) expect(result.error.kind).toBe("invalid_input");
223
- expect(prisma.flowDefinition.upsert).not.toHaveBeenCalled();
224
- });
225
-
226
- it("a compile failure (unsupported_by_engine) exits non-zero naming the waitlist key, and writes no flow_definitions row for it (AC2)", async () => {
227
- const config = configWithUncompilableFlow("bad-experiment");
228
-
229
- const result = await materializeWaitlistFlows(SITE_ID, config);
230
-
231
- expect(result.ok).toBe(false);
232
- if (!result.ok) {
233
- expect(result.error.kind).toBe("unsupported_by_engine");
234
- expect(result.error.message).toContain('"main"');
235
- }
236
- expect(prisma.flowDefinition.upsert).not.toHaveBeenCalled();
237
- });
238
- });
239
-
240
- describe("seedWaitlistExperiments (D5 — the seedFlowPresets loop verbatim, extended #4726 to materialize flows)", () => {
241
- beforeEach(() => {
242
- (prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({
243
- id: "flow_x",
244
- });
245
- });
246
-
247
- it("upserts each config once, in order, through upsertWaitlistExperiment", async () => {
248
- (prisma.waitlistExperiment.upsert as jest.Mock)
249
- .mockResolvedValueOnce({ id: "wlexp_a" })
250
- .mockResolvedValueOnce({ id: "wlexp_b" });
251
-
252
- const configA = validConfig("experiment-a");
253
- const configB = validConfig("experiment-b");
254
-
255
- const result = await seedWaitlistExperiments(SITE_ID, [configA, configB]);
256
-
257
- expect(result).toEqual({
258
- ok: true,
259
- seeded: ["experiment-a", "experiment-b"],
260
- });
261
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledTimes(2);
262
- expect(prisma.waitlistExperiment.upsert).toHaveBeenNthCalledWith(
263
- 1,
264
- expect.objectContaining({
265
- where: { siteId_key: { siteId: SITE_ID, key: "experiment-a" } },
266
- }),
267
- );
268
- expect(prisma.waitlistExperiment.upsert).toHaveBeenNthCalledWith(
269
- 2,
270
- expect.objectContaining({
271
- where: { siteId_key: { siteId: SITE_ID, key: "experiment-b" } },
272
- }),
273
- );
274
- });
275
-
276
- it("leaves both a waitlist_experiments row AND one flow_definitions row per waitlists[] entry (AC1)", async () => {
277
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValueOnce({
278
- id: "wlexp_a",
279
- });
280
-
281
- const config = validConfigWithTwoWaitlists("two-wl");
282
-
283
- const result = await seedWaitlistExperiments(SITE_ID, [config]);
284
-
285
- expect(result).toEqual({ ok: true, seeded: ["two-wl"] });
286
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledTimes(1);
287
- expect(prisma.flowDefinition.upsert).toHaveBeenCalledTimes(2);
288
- expect(prisma.flowDefinition.upsert).toHaveBeenNthCalledWith(
289
- 1,
290
- expect.objectContaining({
291
- where: { siteId_slug: { siteId: SITE_ID, slug: "two-wl-main" } },
292
- }),
293
- );
294
- expect(prisma.flowDefinition.upsert).toHaveBeenNthCalledWith(
295
- 2,
296
- expect.objectContaining({
297
- where: { siteId_slug: { siteId: SITE_ID, slug: "two-wl-vip" } },
298
- }),
299
- );
300
- });
301
-
302
- it("stops at the first invalid config and reports what seeded before it — never a silent partial success", async () => {
303
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValueOnce({
304
- id: "wlexp_a",
305
- });
306
-
307
- const configA = validConfig("experiment-a");
308
- const invalid = { not: "a config" };
309
-
310
- const result = await seedWaitlistExperiments(SITE_ID, [configA, invalid]);
311
-
312
- expect(result.ok).toBe(false);
313
- if (!result.ok) {
314
- expect(result.seeded).toEqual(["experiment-a"]);
315
- expect(result.error.kind).toBe("invalid_input");
316
- }
317
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledTimes(1);
318
- });
319
-
320
- it("a compile failure stops the run, names the waitlist key, reports nothing seeded for that config, and writes no flow_definitions row for it", async () => {
321
- (prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValueOnce({
322
- id: "wlexp_bad",
323
- });
324
-
325
- const bad = configWithUncompilableFlow("bad-experiment");
326
-
327
- const result = await seedWaitlistExperiments(SITE_ID, [bad]);
328
-
329
- expect(result.ok).toBe(false);
330
- if (!result.ok) {
331
- expect(result.seeded).toEqual([]);
332
- expect(result.error.kind).toBe("unsupported_by_engine");
333
- expect(result.error.message).toContain('"main"');
334
- }
335
- // The experiment row upserts before flow materialization is attempted —
336
- // it is `flow_definitions`, not `waitlist_experiments`, that the
337
- // compile failure leaves unwritten.
338
- expect(prisma.waitlistExperiment.upsert).toHaveBeenCalledTimes(1);
339
- expect(prisma.flowDefinition.upsert).not.toHaveBeenCalled();
340
- });
341
- });