create-ailk 0.2.0 → 0.4.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 (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -1,752 +0,0 @@
1
- /**
2
- * @file index.test.ts
3
- * Route-level inject tests for GET /v1/flows/:slug and
4
- * POST /v1/flows/:slug/steps (#3956 D4).
5
- *
6
- * Mirrors apps/api/src/routes/leads/__tests__/index.test.ts's shape: a bare
7
- * Fastify instance with only the routes under test registered, prisma
8
- * mocked via __mocks__/prisma.ts.
9
- */
10
-
11
- import type { FastifyInstance } from "fastify";
12
-
13
- // 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)
14
- jest.mock("../../../lib/prisma.js");
15
-
16
- jest.mock("@working-theory/email", () => ({
17
- sendLeadNotification: jest.fn().mockResolvedValue({
18
- ok: true,
19
- messageId: "msg-test",
20
- }),
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
- import { flowAggregateSchema } from "@working-theory/validation";
34
-
35
- import {
36
- getFlowAggregateRoute,
37
- getFlowRoute,
38
- submitFlowStepRoute,
39
- } from "../index";
40
- import { prisma } from "../../../lib/prisma.js";
41
-
42
- const en = (s: string) => ({ en: s });
43
-
44
- function contactFirstFlow() {
45
- return {
46
- slug: "get-a-quote",
47
- defaultLocale: "en",
48
- steps: [
49
- {
50
- key: "contact",
51
- type: "field" as const,
52
- copy: { title: en("Let's get started") },
53
- fields: [
54
- {
55
- name: "email",
56
- inputType: "email" as const,
57
- required: true,
58
- mapsTo: "email" as const,
59
- copy: { label: en("Email") },
60
- },
61
- ],
62
- },
63
- {
64
- key: "budget",
65
- type: "segmentation" as const,
66
- copy: { title: en("What's your budget?") },
67
- questions: [
68
- {
69
- name: "budget",
70
- options: [{ value: "small", label: en("Under $10k") }],
71
- },
72
- ],
73
- },
74
- ],
75
- };
76
- }
77
-
78
- // contactFirstFlow() with a two-option driving question + a segments band
79
- // map on "budget" (#3959 D1) — small -> "starter", large -> "enterprise".
80
- // Landing "budget" both lands the band map AND completes the session (no
81
- // trailing message step), the HTTP-layer analog of the engine-level
82
- // terminal-segmentation-step fixture.
83
- function contactFirstFlowWithSegments() {
84
- const flow = contactFirstFlow();
85
- const budgetStep = flow.steps.find((s) => s.key === "budget")!;
86
- (
87
- budgetStep as typeof budgetStep & { segments?: unknown }
88
- ).questions = [
89
- {
90
- name: "budget",
91
- options: [
92
- { value: "small", label: en("Under $10k") },
93
- { value: "large", label: en("$10k+") },
94
- ],
95
- },
96
- ];
97
- (budgetStep as typeof budgetStep & { segments?: unknown }).segments = {
98
- question: "budget",
99
- bands: [
100
- { key: "starter", values: ["small"] },
101
- { key: "enterprise", values: ["large"] },
102
- ],
103
- };
104
- return flow;
105
- }
106
-
107
- describe("GET /v1/flows/:slug and POST /v1/flows/:slug/steps", () => {
108
- let app: FastifyInstance;
109
-
110
- beforeEach(async () => {
111
- jest.clearAllMocks();
112
- process.env.DATABASE_URL = "postgres://stub";
113
-
114
- const fastify = require("fastify")();
115
- await fastify.register(getFlowRoute);
116
- await fastify.register(submitFlowStepRoute);
117
- app = fastify;
118
- });
119
-
120
- afterEach(async () => {
121
- await app.close();
122
- delete process.env.DATABASE_URL;
123
- delete process.env.AILK_SITE_ID;
124
- });
125
-
126
- // ── GET /v1/flows/:slug ──────────────────────────────────────────────────
127
-
128
- it("GET — returns the ok:true envelope carrying the flow config", async () => {
129
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
130
- config: contactFirstFlow(),
131
- });
132
-
133
- const res = await app.inject({
134
- method: "GET",
135
- url: "/v1/flows/get-a-quote",
136
- });
137
-
138
- expect(res.statusCode).toBe(200);
139
- const body = res.json();
140
- expect(body.ok).toBe(true);
141
- expect(body.data.slug).toBe("get-a-quote");
142
- expect(body.data.steps.map((s: { type: string }) => s.type)).toEqual([
143
- "field",
144
- "segmentation",
145
- ]);
146
- });
147
-
148
- it("GET — 404 flow_not_found for an unknown slug", async () => {
149
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue(null);
150
-
151
- const res = await app.inject({
152
- method: "GET",
153
- url: "/v1/flows/does-not-exist",
154
- });
155
-
156
- expect(res.statusCode).toBe(404);
157
- const body = res.json();
158
- expect(body).toEqual({
159
- ok: false,
160
- error: { code: "flow_not_found", message: expect.any(String) },
161
- });
162
- });
163
-
164
- it("GET — 503 flows_not_configured when DATABASE_URL is unset", async () => {
165
- delete process.env.DATABASE_URL;
166
-
167
- const res = await app.inject({
168
- method: "GET",
169
- url: "/v1/flows/get-a-quote",
170
- });
171
-
172
- expect(res.statusCode).toBe(503);
173
- expect(res.json()).toEqual({
174
- ok: false,
175
- error: { code: "flows_not_configured", message: expect.any(String) },
176
- });
177
- });
178
-
179
- it("GET — 400 invalid_input for a malformed slug", async () => {
180
- const res = await app.inject({
181
- method: "GET",
182
- url: "/v1/flows/Not_A_Valid_Slug!",
183
- });
184
-
185
- expect(res.statusCode).toBe(400);
186
- expect(res.json().error.code).toBe("invalid_input");
187
- });
188
-
189
- // ── POST /v1/flows/:slug/steps — AC2 walk at the HTTP layer ─────────────
190
-
191
- it("POST — AC2: submitting the contact step persists a partial lead and never auto-completes", async () => {
192
- const sessionId = "flsess_00000000-0000-7000-8000-0000000000f1";
193
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
194
- id: "flow_00000000-0000-7000-8000-0000000000f0",
195
- config: contactFirstFlow(),
196
- });
197
- (prisma.flowSession.create as jest.Mock).mockResolvedValue({
198
- id: sessionId,
199
- siteId: "default",
200
- state: {},
201
- email: null,
202
- leadId: null,
203
- completedAt: null,
204
- });
205
- (prisma.flowSession.update as jest.Mock).mockResolvedValue({});
206
-
207
- const res = await app.inject({
208
- method: "POST",
209
- url: "/v1/flows/get-a-quote/steps",
210
- payload: {
211
- stepKey: "contact",
212
- values: { email: "abandoner@example.com" },
213
- },
214
- });
215
-
216
- expect(res.statusCode).toBe(200);
217
- const body = res.json();
218
- expect(body).toEqual({
219
- ok: true,
220
- data: {
221
- sessionId,
222
- landed: "contact",
223
- nextStepKey: "budget",
224
- completed: false,
225
- },
226
- });
227
- expect(prisma.flowSession.update).toHaveBeenCalledWith({
228
- where: { id: sessionId },
229
- data: {
230
- state: { contact: { email: "abandoner@example.com" } },
231
- // #4663 D1/D2 — flags is re-derived + persisted on EVERY landing.
232
- flags: {},
233
- email: "abandoner@example.com",
234
- },
235
- });
236
- expect(prisma.lead.create).not.toHaveBeenCalled();
237
- });
238
-
239
- it("POST — 400 invalid_input for a missing required field", async () => {
240
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
241
- id: "flow_x",
242
- config: contactFirstFlow(),
243
- });
244
-
245
- const res = await app.inject({
246
- method: "POST",
247
- url: "/v1/flows/get-a-quote/steps",
248
- payload: { stepKey: "contact", values: {} },
249
- });
250
-
251
- expect(res.statusCode).toBe(400);
252
- expect(res.json().error.code).toBe("invalid_input");
253
- });
254
-
255
- it("POST — 404 flow_not_found for a step submission against an unknown flow (no sessionId)", async () => {
256
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue(null);
257
-
258
- const res = await app.inject({
259
- method: "POST",
260
- url: "/v1/flows/does-not-exist/steps",
261
- payload: { stepKey: "contact", values: {} },
262
- });
263
-
264
- expect(res.statusCode).toBe(404);
265
- expect(res.json().error.code).toBe("flow_not_found");
266
- });
267
-
268
- it("POST — 409 step_conflict for an unknown stepKey", async () => {
269
- (prisma.flowSession.findUnique as jest.Mock).mockResolvedValue({
270
- id: "flsess_x",
271
- siteId: "default",
272
- flowSlug: "get-a-quote",
273
- configSnapshot: contactFirstFlow(),
274
- state: {},
275
- email: null,
276
- completedAt: null,
277
- });
278
-
279
- const res = await app.inject({
280
- method: "POST",
281
- url: "/v1/flows/get-a-quote/steps",
282
- payload: {
283
- sessionId: "flsess_x",
284
- stepKey: "does_not_exist",
285
- values: {},
286
- },
287
- });
288
-
289
- expect(res.statusCode).toBe(409);
290
- expect(res.json().error.code).toBe("step_conflict");
291
- });
292
-
293
- it("POST — 503 flows_not_configured when DATABASE_URL is unset", async () => {
294
- delete process.env.DATABASE_URL;
295
-
296
- const res = await app.inject({
297
- method: "POST",
298
- url: "/v1/flows/get-a-quote/steps",
299
- payload: { stepKey: "contact", values: {} },
300
- });
301
-
302
- expect(res.statusCode).toBe(503);
303
- expect(res.json().error.code).toBe("flows_not_configured");
304
- });
305
-
306
- it("POST — 400 invalid_input for an unknown top-level body key (.strict())", async () => {
307
- const res = await app.inject({
308
- method: "POST",
309
- url: "/v1/flows/get-a-quote/steps",
310
- payload: { stepKey: "contact", values: {}, unexpected: true },
311
- });
312
-
313
- expect(res.statusCode).toBe(400);
314
- expect(res.json().error.code).toBe("invalid_input");
315
- });
316
-
317
- // ── AC4 at the HTTP layer — completion returns leadId ───────────────────
318
-
319
- it("POST — completing the final step returns completed:true with leadId", async () => {
320
- process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
321
- process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
322
- const leadRowId = "lead_00000000-0000-7000-8000-0000000000f2";
323
-
324
- (prisma.flowSession.findUnique as jest.Mock).mockResolvedValue({
325
- id: "flsess_y",
326
- siteId: "default",
327
- flowSlug: "get-a-quote",
328
- configSnapshot: contactFirstFlow(),
329
- state: { contact: { email: "done@example.com" } },
330
- email: "done@example.com",
331
- leadId: null,
332
- completedAt: null,
333
- });
334
- (prisma.flowSession.update as jest.Mock).mockResolvedValue({});
335
- (prisma.lead.create as jest.Mock).mockResolvedValue({ id: leadRowId });
336
-
337
- const res = await app.inject({
338
- method: "POST",
339
- url: "/v1/flows/get-a-quote/steps",
340
- payload: {
341
- sessionId: "flsess_y",
342
- stepKey: "budget",
343
- values: { budget: "small" },
344
- },
345
- });
346
-
347
- expect(res.statusCode).toBe(200);
348
- const body = res.json();
349
- expect(body.data.completed).toBe(true);
350
- expect(body.data.leadId).toBe(leadRowId);
351
-
352
- delete process.env.AILK_LEAD_NOTIFICATION_TO;
353
- delete process.env.AILK_LEAD_NOTIFICATION_FROM;
354
- });
355
-
356
- // ── AC1 at the HTTP layer — the two-submission divergence walk (#3959) ──
357
-
358
- it("AC1 — two full walks differing only on the segmentation answer derive distinct segments, each surfaced in the response", async () => {
359
- const definitionRow = {
360
- id: "flow_00000000-0000-7000-8000-0000000000f3",
361
- config: contactFirstFlowWithSegments(),
362
- };
363
- const sessionA = "flsess_00000000-0000-7000-8000-0000000000a1";
364
- const sessionB = "flsess_00000000-0000-7000-8000-0000000000b1";
365
-
366
- // ── Walk A: contact -> budget:"small" -> segment "starter" ──
367
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValueOnce(
368
- definitionRow,
369
- );
370
- (prisma.flowSession.create as jest.Mock).mockResolvedValueOnce({
371
- id: sessionA,
372
- siteId: "default",
373
- state: {},
374
- email: null,
375
- segment: null,
376
- leadId: null,
377
- completedAt: null,
378
- });
379
- (prisma.flowSession.update as jest.Mock).mockResolvedValueOnce({});
380
-
381
- const resA1 = await app.inject({
382
- method: "POST",
383
- url: "/v1/flows/get-a-quote/steps",
384
- payload: { stepKey: "contact", values: { email: "a@example.com" } },
385
- });
386
- expect(resA1.statusCode).toBe(200);
387
-
388
- (prisma.flowSession.findUnique as jest.Mock).mockResolvedValueOnce({
389
- id: sessionA,
390
- siteId: "default",
391
- flowSlug: "get-a-quote",
392
- configSnapshot: contactFirstFlowWithSegments(),
393
- state: { contact: { email: "a@example.com" } },
394
- email: "a@example.com",
395
- segment: null,
396
- leadId: null,
397
- completedAt: null,
398
- });
399
- (prisma.flowSession.update as jest.Mock).mockResolvedValueOnce({});
400
- (prisma.lead.create as jest.Mock).mockResolvedValueOnce({
401
- id: "lead_a",
402
- });
403
-
404
- const resA2 = await app.inject({
405
- method: "POST",
406
- url: "/v1/flows/get-a-quote/steps",
407
- payload: {
408
- sessionId: sessionA,
409
- stepKey: "budget",
410
- values: { budget: "small" },
411
- },
412
- });
413
- expect(resA2.statusCode).toBe(200);
414
- const bodyA = resA2.json();
415
- expect(bodyA.data.segment).toBe("starter");
416
- expect(prisma.lead.create).toHaveBeenLastCalledWith(
417
- expect.objectContaining({
418
- data: expect.objectContaining({
419
- payload: expect.objectContaining({ segment: "starter" }),
420
- }),
421
- }),
422
- );
423
-
424
- // ── Walk B: contact -> budget:"large" -> segment "enterprise" ──
425
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValueOnce(
426
- definitionRow,
427
- );
428
- (prisma.flowSession.create as jest.Mock).mockResolvedValueOnce({
429
- id: sessionB,
430
- siteId: "default",
431
- state: {},
432
- email: null,
433
- segment: null,
434
- leadId: null,
435
- completedAt: null,
436
- });
437
- (prisma.flowSession.update as jest.Mock).mockResolvedValueOnce({});
438
-
439
- const resB1 = await app.inject({
440
- method: "POST",
441
- url: "/v1/flows/get-a-quote/steps",
442
- payload: { stepKey: "contact", values: { email: "b@example.com" } },
443
- });
444
- expect(resB1.statusCode).toBe(200);
445
-
446
- (prisma.flowSession.findUnique as jest.Mock).mockResolvedValueOnce({
447
- id: sessionB,
448
- siteId: "default",
449
- flowSlug: "get-a-quote",
450
- configSnapshot: contactFirstFlowWithSegments(),
451
- state: { contact: { email: "b@example.com" } },
452
- email: "b@example.com",
453
- segment: null,
454
- leadId: null,
455
- completedAt: null,
456
- });
457
- (prisma.flowSession.update as jest.Mock).mockResolvedValueOnce({});
458
- (prisma.lead.create as jest.Mock).mockResolvedValueOnce({
459
- id: "lead_b",
460
- });
461
-
462
- const resB2 = await app.inject({
463
- method: "POST",
464
- url: "/v1/flows/get-a-quote/steps",
465
- payload: {
466
- sessionId: sessionB,
467
- stepKey: "budget",
468
- values: { budget: "large" },
469
- },
470
- });
471
- expect(resB2.statusCode).toBe(200);
472
- const bodyB = resB2.json();
473
- expect(bodyB.data.segment).toBe("enterprise");
474
- expect(prisma.lead.create).toHaveBeenLastCalledWith(
475
- expect.objectContaining({
476
- data: expect.objectContaining({
477
- payload: expect.objectContaining({ segment: "enterprise" }),
478
- }),
479
- }),
480
- );
481
-
482
- // Distinct segments, distinct leads — the AC1 divergence.
483
- expect(bodyA.data.segment).not.toBe(bodyB.data.segment);
484
- });
485
-
486
- it("POST — a client-posted `segment` value in `values` never surfaces in the response or the session write (D2 makes it unrepresentable as a declared name)", async () => {
487
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
488
- id: "flow_x",
489
- config: contactFirstFlow(),
490
- });
491
- (prisma.flowSession.create as jest.Mock).mockResolvedValue({
492
- id: "flsess_z",
493
- siteId: "default",
494
- state: {},
495
- email: null,
496
- segment: null,
497
- leadId: null,
498
- completedAt: null,
499
- });
500
- (prisma.flowSession.update as jest.Mock).mockResolvedValue({});
501
-
502
- const res = await app.inject({
503
- method: "POST",
504
- url: "/v1/flows/get-a-quote/steps",
505
- payload: {
506
- stepKey: "contact",
507
- values: { email: "a@example.com", segment: "attacker-value" },
508
- },
509
- });
510
-
511
- expect(res.statusCode).toBe(200);
512
- const body = res.json();
513
- expect(body.data.segment).toBeUndefined();
514
- expect(prisma.flowSession.update).toHaveBeenCalledWith({
515
- where: { id: "flsess_z" },
516
- data: {
517
- state: { contact: { email: "a@example.com" } },
518
- flags: {},
519
- email: "a@example.com",
520
- },
521
- });
522
- });
523
- });
524
-
525
- // ── GET /v1/flows/:slug/aggregate (#4771) ────────────────────────────────
526
-
527
- function aggregateConfig() {
528
- return {
529
- key: "founder-diagnostic",
530
- mode: "lead-capture" as const,
531
- status: "live" as const,
532
- waitlists: [{ key: "general", flowSlug: "founder-diagnostic-general" }],
533
- questionSet: {
534
- version: 1 as const,
535
- questions: [
536
- {
537
- id: "role",
538
- prompt: en("Role?"),
539
- answerType: "single" as const,
540
- options: [
541
- { value: "founder", label: en("Founder") },
542
- { value: "other", label: en("Other") },
543
- ],
544
- resultsReveal: true as const,
545
- },
546
- {
547
- id: "team_size",
548
- prompt: en("Team size?"),
549
- answerType: "single" as const,
550
- options: [{ value: "solo", label: en("Solo") }],
551
- },
552
- ],
553
- },
554
- tracking: {
555
- version: 1 as const,
556
- utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
557
- waitlists: { general: { campaign: "gen-campaign", channels: ["organic"] } },
558
- },
559
- stripeConfigRef: null,
560
- };
561
- }
562
-
563
- describe("GET /v1/flows/:slug/aggregate", () => {
564
- let app: FastifyInstance;
565
-
566
- beforeEach(async () => {
567
- jest.clearAllMocks();
568
- process.env.DATABASE_URL = "postgres://stub";
569
-
570
- const fastify = require("fastify")();
571
- await fastify.register(getFlowAggregateRoute);
572
- app = fastify;
573
- });
574
-
575
- afterEach(async () => {
576
- await app.close();
577
- delete process.env.DATABASE_URL;
578
- delete process.env.AILK_SITE_ID;
579
- });
580
-
581
- it("AC1/AC3/AC4: returns only the flagged question, per-option counts, and no PII, at any nesting depth", async () => {
582
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
583
- id: "flow_00000000-0000-7000-8000-000000000001",
584
- });
585
- (prisma.waitlistExperiment.findMany as jest.Mock).mockResolvedValue([
586
- {
587
- id: "wlexp_00000000-0000-7000-8000-000000000001",
588
- siteId: "default",
589
- config: aggregateConfig(),
590
- },
591
- ]);
592
- (prisma.flowSession.findMany as jest.Mock).mockResolvedValue([
593
- {
594
- siteId: "default",
595
- flowSlug: "founder-diagnostic-general",
596
- completedAt: new Date("2026-09-01T00:00:00.000Z"),
597
- leadId: null,
598
- state: { role_step: { role: "founder" } },
599
- },
600
- {
601
- siteId: "default",
602
- flowSlug: "founder-diagnostic-general",
603
- completedAt: new Date("2026-09-02T00:00:00.000Z"),
604
- leadId: null,
605
- state: { role_step: { role: "other" } },
606
- },
607
- ]);
608
-
609
- const res = await app.inject({
610
- method: "GET",
611
- url: "/v1/flows/founder-diagnostic-general/aggregate",
612
- });
613
-
614
- expect(res.statusCode).toBe(200);
615
- const body = res.json();
616
- expect(body.ok).toBe(true);
617
- expect(body.data.slug).toBe("founder-diagnostic-general");
618
- expect(body.data.respondents).toBe(2);
619
- // AC3 — team_size is unflagged and must be absent.
620
- expect(body.data.questions.map((q: { id: string }) => q.id)).toEqual(["role"]);
621
- const role = body.data.questions[0];
622
- expect(role.answered).toBe(2);
623
- const byValue = new Map(
624
- role.options.map((o: { value: string; count: number }) => [o.value, o.count]),
625
- );
626
- expect(byValue.get("founder")).toBe(1);
627
- expect(byValue.get("other")).toBe(1);
628
- // AC2 — the response validates against flowAggregateSchema (against the
629
- // ACTUAL service output, not a hand-written fixture — the .strict()
630
- // check this schema-parse exercises is what would catch an unnamed
631
- // leaked key, e.g. leadId/siteId/completedAt, that the name-based regex
632
- // sweeps below cannot see).
633
- expect(flowAggregateSchema.safeParse(body.data).success).toBe(true);
634
- // AC2 — no email/name/session id/score anywhere in the serialized body.
635
- const serialized = JSON.stringify(body);
636
- expect(serialized).not.toMatch(/email/i);
637
- expect(serialized).not.toMatch(/name/i);
638
- expect(serialized).not.toMatch(/session/i);
639
- expect(serialized).not.toMatch(/score/i);
640
- });
641
-
642
- it("AC5: an unknown slug returns 404 flow_not_found", async () => {
643
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue(null);
644
-
645
- const res = await app.inject({
646
- method: "GET",
647
- url: "/v1/flows/does-not-exist/aggregate",
648
- });
649
-
650
- expect(res.statusCode).toBe(404);
651
- expect(res.json()).toEqual({
652
- ok: false,
653
- error: { code: "flow_not_found", message: expect.any(String) },
654
- });
655
- });
656
-
657
- it("AC5: a known flow with no backing register returns questions: [] with a truthful respondents", async () => {
658
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
659
- id: "flow_00000000-0000-7000-8000-000000000002",
660
- });
661
- (prisma.waitlistExperiment.findMany as jest.Mock).mockResolvedValue([]);
662
- (prisma.flowSession.findMany as jest.Mock).mockResolvedValue([
663
- {
664
- siteId: "default",
665
- flowSlug: "no-register-flow",
666
- completedAt: new Date("2026-09-01T00:00:00.000Z"),
667
- leadId: null,
668
- state: {},
669
- },
670
- ]);
671
-
672
- const res = await app.inject({
673
- method: "GET",
674
- url: "/v1/flows/no-register-flow/aggregate",
675
- });
676
-
677
- expect(res.statusCode).toBe(200);
678
- const body = res.json();
679
- expect(body.ok).toBe(true);
680
- expect(body.data.questions).toEqual([]);
681
- expect(body.data.respondents).toBe(1);
682
- });
683
-
684
- it("AC6: excludes an incomplete session (completedAt: null)", async () => {
685
- (prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
686
- id: "flow_00000000-0000-7000-8000-000000000003",
687
- });
688
- (prisma.waitlistExperiment.findMany as jest.Mock).mockImplementation(
689
- ({ where }: { where: { siteId: string } }) =>
690
- Promise.resolve(
691
- where.siteId === "default"
692
- ? [
693
- {
694
- id: "wlexp_00000000-0000-7000-8000-000000000001",
695
- siteId: "default",
696
- config: aggregateConfig(),
697
- },
698
- ]
699
- : [],
700
- ),
701
- );
702
- (prisma.flowSession.findMany as jest.Mock).mockImplementation(
703
- ({ where }: { where: { completedAt?: { not: unknown } } }) =>
704
- Promise.resolve(
705
- where.completedAt?.not === null
706
- ? [
707
- {
708
- siteId: "default",
709
- flowSlug: "founder-diagnostic-general",
710
- completedAt: new Date("2026-09-01T00:00:00.000Z"),
711
- leadId: null,
712
- state: { role_step: { role: "founder" } },
713
- },
714
- ]
715
- : [],
716
- ),
717
- );
718
-
719
- const res = await app.inject({
720
- method: "GET",
721
- url: "/v1/flows/founder-diagnostic-general/aggregate",
722
- });
723
-
724
- expect(res.statusCode).toBe(200);
725
- expect(res.json().data.respondents).toBe(1);
726
- });
727
-
728
- it("400 invalid_input for a malformed slug", async () => {
729
- const res = await app.inject({
730
- method: "GET",
731
- url: "/v1/flows/Not_A_Valid_Slug!/aggregate",
732
- });
733
-
734
- expect(res.statusCode).toBe(400);
735
- expect(res.json().error.code).toBe("invalid_input");
736
- });
737
-
738
- it("503 flows_not_configured when DATABASE_URL is unset", async () => {
739
- delete process.env.DATABASE_URL;
740
-
741
- const res = await app.inject({
742
- method: "GET",
743
- url: "/v1/flows/founder-diagnostic-general/aggregate",
744
- });
745
-
746
- expect(res.statusCode).toBe(503);
747
- expect(res.json()).toEqual({
748
- ok: false,
749
- error: { code: "flows_not_configured", message: expect.any(String) },
750
- });
751
- });
752
- });