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,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
- });