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,273 +0,0 @@
1
- /**
2
- * Tests for the operator dashboard's comparison view (#4665 D10, AC2/AC4).
3
- *
4
- * Covers:
5
- * 1. Renders per-waitlist + total comparison rows, computed per D4.
6
- * 2. Renders a `null` qualified/rate as "—" for a lead-capture experiment
7
- * (D4 — never `0`).
8
- * 3. Renders the per-waitlist tabs (by-source table + distributions).
9
- * 4. Renders the signups table with a "next" link when nextCursor is present.
10
- * 5. Calls notFound() on a 404.
11
- * 6. Renders ForbiddenState on a 401/403.
12
- */
13
- import { render, screen } from "@testing-library/react";
14
-
15
- jest.mock("next-intl/server", () => ({
16
- __esModule: true,
17
- getTranslations: jest.fn().mockImplementation(async () => (key: string, values?: Record<string, unknown>) =>
18
- values ? `copy:${key}:${JSON.stringify(values)}` : `copy:${key}`,
19
- ),
20
- }));
21
-
22
- jest.mock("next/headers", () => ({
23
- __esModule: true,
24
- cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
25
- }));
26
-
27
- const mockNotFound = jest.fn(() => {
28
- throw new Error("NEXT_NOT_FOUND");
29
- });
30
- jest.mock("next/navigation", () => ({
31
- __esModule: true,
32
- notFound: () => mockNotFound(),
33
- }));
34
-
35
- const mockComparison = jest.fn();
36
- const mockSignups = jest.fn();
37
- jest.mock("@working-theory/api-client", () => ({
38
- __esModule: true,
39
- waitlistExperiments: {
40
- comparison: (...args: unknown[]) => mockComparison(...args),
41
- signups: (...args: unknown[]) => mockSignups(...args),
42
- },
43
- }));
44
-
45
- import Page from "../[experimentId]/page";
46
-
47
- const EXPERIMENT = {
48
- id: "wlexp_1",
49
- siteId: "site_1",
50
- key: "early-access",
51
- mode: "lead-capture" as const,
52
- status: "live" as const,
53
- createdAt: "2026-08-01T00:00:00.000Z",
54
- updatedAt: "2026-09-01T00:00:00.000Z",
55
- waitlists: [{ key: "main", flowSlug: "main-flow" }],
56
- counts: { raw: 100, qualified: null, qualificationRate: null },
57
- };
58
-
59
- const COMPARISON = {
60
- experiment: EXPERIMENT,
61
- questions: [
62
- {
63
- id: "q1",
64
- prompt: { en: "Do you use a CRM?" },
65
- answerType: "single" as const,
66
- options: [{ value: "yes", label: { en: "Yes" } }],
67
- },
68
- ],
69
- serviceableExcludes: [],
70
- waitlists: [
71
- {
72
- key: "main",
73
- raw: 100,
74
- qualified: null,
75
- qualificationRate: null,
76
- byTier: [],
77
- bySource: [{ source: "google", raw: 60, qualified: null, qualificationRate: null }],
78
- distributions: [
79
- { questionId: "q1", answerType: "single" as const, answered: 60, options: [{ value: "yes", count: 60, share: 1 }] },
80
- ],
81
- },
82
- ],
83
- total: {
84
- key: "total" as const,
85
- raw: 100,
86
- qualified: null,
87
- qualificationRate: null,
88
- byTier: [],
89
- bySource: [{ source: "google", raw: 60, qualified: null, qualificationRate: null }],
90
- distributions: [
91
- { questionId: "q1", answerType: "single" as const, answered: 60, options: [{ value: "yes", count: 60, share: 1 }] },
92
- ],
93
- },
94
- };
95
-
96
- const SIGNUPS = {
97
- experimentId: "wlexp_1",
98
- items: [
99
- {
100
- leadId: "lead_1",
101
- waitlistKey: "main",
102
- email: "a@example.com",
103
- createdAt: "2026-08-15T00:00:00.000Z",
104
- source: "google",
105
- tier: null,
106
- score: null,
107
- },
108
- ],
109
- nextCursor: "lead_1",
110
- };
111
-
112
- // FEAT-074 U4a (#4715, AC-5) — the SAME comparison + signups fixtures, with
113
- // `experiment.key: "targeting-maturity"` and a `level_N` tier so the Level
114
- // display + distribution can be asserted.
115
- const TARGETING_MATURITY_EXPERIMENT = { ...EXPERIMENT, key: "targeting-maturity" };
116
-
117
- const TARGETING_MATURITY_COMPARISON = {
118
- ...COMPARISON,
119
- experiment: TARGETING_MATURITY_EXPERIMENT,
120
- waitlists: [
121
- {
122
- ...COMPARISON.waitlists[0],
123
- byTier: [
124
- { tier: "level_0", count: 2 },
125
- { tier: "level_1", count: 5 },
126
- ],
127
- },
128
- ],
129
- total: {
130
- ...COMPARISON.total,
131
- byTier: [
132
- { tier: "level_0", count: 2 },
133
- { tier: "level_1", count: 5 },
134
- ],
135
- },
136
- };
137
-
138
- const TARGETING_MATURITY_SIGNUPS = {
139
- experimentId: "wlexp_1",
140
- items: [{ ...SIGNUPS.items[0]!, tier: "level_3" }],
141
- nextCursor: "lead_1",
142
- };
143
-
144
- function baseProps() {
145
- return {
146
- params: Promise.resolve({ locale: "en", experimentId: "wlexp_1" }),
147
- searchParams: Promise.resolve({}),
148
- };
149
- }
150
-
151
- describe("WaitlistComparisonPage — comparison + signups (AC2/AC4)", () => {
152
- beforeEach(() => {
153
- mockComparison.mockReset();
154
- mockSignups.mockReset();
155
- mockNotFound.mockClear();
156
- mockSignups.mockResolvedValue({ ok: true, body: SIGNUPS });
157
- });
158
-
159
- it("renders the top comparison table with null rendered as unavailable", async () => {
160
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
161
- const element = await Page(baseProps());
162
- render(element);
163
-
164
- expect(screen.getByTestId("metric-comparison-table-cell-raw-total")).toHaveTextContent("100");
165
- expect(screen.getByTestId("metric-comparison-table-cell-qualified-total")).toHaveTextContent(
166
- "copy:common.unavailable",
167
- );
168
- });
169
-
170
- it("renders per-waitlist tabs with a by-source table and distributions", async () => {
171
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
172
- const element = await Page(baseProps());
173
- render(element);
174
-
175
- expect(screen.getByTestId("waitlist-comparison-tabs")).toBeInTheDocument();
176
- expect(screen.getByText("Do you use a CRM?")).toBeInTheDocument();
177
- });
178
-
179
- it("wires the by-source table's row-header label to the source-column message", async () => {
180
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
181
- const element = await Page(baseProps());
182
- render(element);
183
-
184
- expect(
185
- screen.getAllByTestId("metric-comparison-table-row-header")[1],
186
- ).toHaveTextContent("copy:comparison.bySourceColumnSource");
187
- });
188
-
189
- it("renders the signups table with a next link when nextCursor is present", async () => {
190
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
191
- const element = await Page(baseProps());
192
- render(element);
193
-
194
- expect(screen.getByTestId("signups-table-row-lead_1")).toBeInTheDocument();
195
- expect(screen.getByTestId("signups-table-next")).toHaveAttribute(
196
- "href",
197
- "/en/waitlist/wlexp_1?cursor=lead_1",
198
- );
199
- });
200
-
201
- it("omits the next link when nextCursor is null", async () => {
202
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
203
- mockSignups.mockResolvedValue({ ok: true, body: { ...SIGNUPS, nextCursor: null } });
204
- const element = await Page(baseProps());
205
- render(element);
206
-
207
- expect(screen.queryByTestId("signups-table-next")).not.toBeInTheDocument();
208
- });
209
-
210
- it("calls notFound() on a 404", async () => {
211
- mockComparison.mockResolvedValue({
212
- ok: false,
213
- status: 404,
214
- body: { error: { code: "not_found", message: "Experiment not found" } },
215
- });
216
- await expect(Page(baseProps())).rejects.toThrow("NEXT_NOT_FOUND");
217
- expect(mockNotFound).toHaveBeenCalled();
218
- });
219
-
220
- it("renders ForbiddenState on a 403", async () => {
221
- mockComparison.mockResolvedValue({
222
- ok: false,
223
- status: 403,
224
- body: { error: { code: "forbidden", message: "Forbidden" } },
225
- });
226
- const element = await Page(baseProps());
227
- render(element);
228
-
229
- expect(screen.getByTestId("waitlist-forbidden")).toBeInTheDocument();
230
- });
231
- });
232
-
233
- describe("WaitlistComparisonPage — FEAT-074 U4a targeting-maturity Level display (#4715, AC-5)", () => {
234
- beforeEach(() => {
235
- mockComparison.mockReset();
236
- mockSignups.mockReset();
237
- mockNotFound.mockClear();
238
- });
239
-
240
- it("shows Level distribution rows (Gate / Level N) instead of raw tier strings", async () => {
241
- mockComparison.mockResolvedValue({ ok: true, body: TARGETING_MATURITY_COMPARISON });
242
- mockSignups.mockResolvedValue({ ok: true, body: TARGETING_MATURITY_SIGNUPS });
243
- const element = await Page(baseProps());
244
- render(element);
245
-
246
- expect(screen.getByText("copy:level.gate")).toBeInTheDocument();
247
- expect(screen.getByText('copy:level.label:{"n":1}')).toBeInTheDocument();
248
- expect(screen.queryByText(/copy:comparison.tierRowPrefix/)).not.toBeInTheDocument();
249
- });
250
-
251
- it("SignupsTable shows the Level label, not the raw level_N tier string", async () => {
252
- mockComparison.mockResolvedValue({ ok: true, body: TARGETING_MATURITY_COMPARISON });
253
- mockSignups.mockResolvedValue({ ok: true, body: TARGETING_MATURITY_SIGNUPS });
254
- const element = await Page(baseProps());
255
- render(element);
256
-
257
- const row = screen.getByTestId("signups-table-row-lead_1");
258
- expect(row).toHaveTextContent('copy:level.label:{"n":3}');
259
- expect(row).not.toHaveTextContent("level_3");
260
- });
261
-
262
- it("a non-targeting experiment's SignupsTable renders the raw tier unchanged (byte-unchanged floor)", async () => {
263
- mockComparison.mockResolvedValue({ ok: true, body: COMPARISON });
264
- mockSignups.mockResolvedValue({
265
- ok: true,
266
- body: { ...SIGNUPS, items: [{ ...SIGNUPS.items[0]!, tier: "qualified" }] },
267
- });
268
- const element = await Page(baseProps());
269
- render(element);
270
-
271
- expect(screen.getByTestId("signups-table-row-lead_1")).toHaveTextContent("qualified");
272
- });
273
- });
@@ -1,171 +0,0 @@
1
- /**
2
- * Tests for the operator dashboard's list + rollup view (#4665 D10, AC1).
3
- *
4
- * Covers:
5
- * 1. Renders the experiments table + rollup from a D6 fixture list.
6
- * 2. Renders both active (live) and completed (closed) experiments, with
7
- * status visible.
8
- * 3. Renders an EmptyState when the scope has no experiments.
9
- * 4. Renders ForbiddenState on a 401/403 rather than throwing.
10
- */
11
- import { render, screen } from "@testing-library/react";
12
-
13
- jest.mock("next-intl/server", () => ({
14
- __esModule: true,
15
- getTranslations: jest.fn().mockImplementation(async () => (key: string, values?: Record<string, unknown>) =>
16
- values ? `copy:${key}:${JSON.stringify(values)}` : `copy:${key}`,
17
- ),
18
- }));
19
-
20
- jest.mock("next/headers", () => ({
21
- __esModule: true,
22
- cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
23
- }));
24
-
25
- const mockList = jest.fn();
26
- jest.mock("@working-theory/api-client", () => ({
27
- __esModule: true,
28
- waitlistExperiments: { list: (...args: unknown[]) => mockList(...args) },
29
- }));
30
-
31
- import Page from "../page";
32
-
33
- const FIXTURE_LIST = {
34
- scope: "site" as const,
35
- experiments: [
36
- {
37
- id: "wlexp_live",
38
- siteId: "site_1",
39
- key: "early-access",
40
- mode: "qualifying" as const,
41
- status: "live" as const,
42
- createdAt: "2026-08-01T00:00:00.000Z",
43
- updatedAt: "2026-09-01T00:00:00.000Z",
44
- waitlists: [{ key: "main", flowSlug: "main-flow" }],
45
- counts: { raw: 120, qualified: 90, qualificationRate: 0.75 },
46
- },
47
- {
48
- id: "wlexp_closed",
49
- siteId: "site_1",
50
- key: "beta-cohort",
51
- mode: "lead-capture" as const,
52
- status: "closed" as const,
53
- createdAt: "2026-07-01T00:00:00.000Z",
54
- updatedAt: "2026-08-01T00:00:00.000Z",
55
- waitlists: [{ key: "main", flowSlug: "main-flow" }],
56
- counts: { raw: 40, qualified: null, qualificationRate: null },
57
- },
58
- ],
59
- rollup: {
60
- experiments: { total: 2, live: 1, closed: 1, draft: 0 },
61
- raw: 160,
62
- qualified: 90,
63
- qualificationRate: 0.5625,
64
- sites: 1,
65
- },
66
- };
67
-
68
- describe("WaitlistListPage — list + rollup (AC1)", () => {
69
- beforeEach(() => {
70
- mockList.mockReset();
71
- });
72
-
73
- it("renders active and completed experiments with status visible", async () => {
74
- mockList.mockResolvedValue({ ok: true, body: FIXTURE_LIST });
75
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
76
- render(element);
77
-
78
- expect(screen.getByTestId("experiments-table-row-wlexp_live")).toBeInTheDocument();
79
- expect(screen.getByTestId("experiments-table-row-wlexp_closed")).toBeInTheDocument();
80
- expect(screen.getAllByTestId("experiment-status-badge")).toHaveLength(2);
81
- });
82
-
83
- it("renders the rollup strip", async () => {
84
- mockList.mockResolvedValue({ ok: true, body: FIXTURE_LIST });
85
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
86
- render(element);
87
-
88
- expect(screen.getByTestId("rollup-ticker")).toBeInTheDocument();
89
- });
90
-
91
- it("renders an EmptyState when there are no experiments", async () => {
92
- mockList.mockResolvedValue({
93
- ok: true,
94
- body: { ...FIXTURE_LIST, experiments: [], rollup: { ...FIXTURE_LIST.rollup, experiments: { total: 0, live: 0, closed: 0, draft: 0 }, raw: 0, qualified: 0, qualificationRate: null } },
95
- });
96
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
97
- render(element);
98
-
99
- expect(screen.getByTestId("experiments-table-empty")).toBeInTheDocument();
100
- });
101
-
102
- it("renders ForbiddenState on a 403 rather than throwing", async () => {
103
- mockList.mockResolvedValue({ ok: false, status: 403, body: { error: { code: "forbidden", message: "Forbidden" } } });
104
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
105
- render(element);
106
-
107
- expect(screen.getByTestId("waitlist-forbidden")).toBeInTheDocument();
108
- });
109
-
110
- it("rethrows a non-403/401 error", async () => {
111
- mockList.mockResolvedValue({ ok: false, status: 500, body: { error: { code: "internal_error", message: "Boom" } } });
112
- await expect(Page({ params: Promise.resolve({ locale: "en" }) })).rejects.toThrow();
113
- });
114
- });
115
-
116
- // ── #4839 AC-3 — the per-offer funnel renders for a recommending experiment ──
117
-
118
- const RECOMMENDING_EXPERIMENT = {
119
- id: "wlexp_reco",
120
- siteId: "site_1",
121
- key: "ailk-reco",
122
- mode: "recommending" as const,
123
- status: "live" as const,
124
- createdAt: "2026-09-01T00:00:00.000Z",
125
- updatedAt: "2026-09-04T00:00:00.000Z",
126
- waitlists: [{ key: "main", flowSlug: "ailk-reco" }],
127
- counts: {
128
- raw: 3,
129
- qualified: null,
130
- qualificationRate: null,
131
- offers: [
132
- { offerKey: "pro", recommended: 2, saved: 1, checkedOut: 2 },
133
- { offerKey: "starter", recommended: 1, saved: 0, checkedOut: 0 },
134
- ],
135
- },
136
- };
137
-
138
- describe("WaitlistListPage — per-offer funnel (#4839 AC-3)", () => {
139
- beforeEach(() => {
140
- mockList.mockReset();
141
- });
142
-
143
- it("renders one funnel table per recommending experiment, one row per offer, counts formatted", async () => {
144
- mockList.mockResolvedValue({
145
- ok: true,
146
- body: { ...FIXTURE_LIST, experiments: [...FIXTURE_LIST.experiments, RECOMMENDING_EXPERIMENT] },
147
- });
148
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
149
- render(element);
150
-
151
- const funnel = screen.getByTestId("experiments-offer-funnel-wlexp_reco");
152
- expect(funnel).toBeInTheDocument();
153
- expect(funnel).toHaveTextContent("copy:list.offers.captionPrefix ailk-reco");
154
- expect(screen.getByTestId("experiments-offer-funnel-wlexp_reco-pro")).toHaveTextContent("pro");
155
- expect(screen.getByTestId("experiments-offer-funnel-wlexp_reco-pro")).toHaveTextContent("2");
156
- expect(screen.getByTestId("experiments-offer-funnel-wlexp_reco-pro")).toHaveTextContent("1");
157
- expect(screen.getByTestId("experiments-offer-funnel-wlexp_reco-starter")).toHaveTextContent("starter");
158
- expect(screen.getAllByTestId(/^experiments-offer-funnel-wlexp_reco-/)).toHaveLength(2);
159
- expect(screen.getAllByTestId(/^experiments-offer-funnel-wlexp_/)).toHaveLength(3);
160
- // The experiments table is still the list's first table, untouched.
161
- expect(screen.getByTestId("experiments-table-row-wlexp_reco")).toBeInTheDocument();
162
- });
163
-
164
- it("renders no funnel when only non-recommending experiments are in scope", async () => {
165
- mockList.mockResolvedValue({ ok: true, body: FIXTURE_LIST });
166
- const element = await Page({ params: Promise.resolve({ locale: "en" }) });
167
- render(element);
168
-
169
- expect(screen.queryAllByTestId(/^experiments-offer-funnel-/)).toHaveLength(0);
170
- });
171
- });
@@ -1,179 +0,0 @@
1
- /**
2
- * Tests for the operator dashboard's signup drill-down view (#4665 D10, AC3).
3
- *
4
- * Covers:
5
- * 1. Renders answers, tier, score, categoryScores, and ruleTrace (JSON)
6
- * when a WaitlistScore row exists.
7
- * 2. Renders "not scored" EmptyState when score is null.
8
- * 3. Calls notFound() on a 404.
9
- * 4. Renders ForbiddenState on a 401/403.
10
- */
11
- import { render, screen } from "@testing-library/react";
12
-
13
- jest.mock("next-intl/server", () => ({
14
- __esModule: true,
15
- getTranslations: jest.fn().mockImplementation(async () => (key: string) => `copy:${key}`),
16
- }));
17
-
18
- jest.mock("next/headers", () => ({
19
- __esModule: true,
20
- cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
21
- }));
22
-
23
- const mockNotFound = jest.fn(() => {
24
- throw new Error("NEXT_NOT_FOUND");
25
- });
26
- jest.mock("next/navigation", () => ({
27
- __esModule: true,
28
- notFound: () => mockNotFound(),
29
- }));
30
-
31
- const mockSignup = jest.fn();
32
- jest.mock("@working-theory/api-client", () => ({
33
- __esModule: true,
34
- waitlistExperiments: { signup: (...args: unknown[]) => mockSignup(...args) },
35
- }));
36
-
37
- import Page from "../[experimentId]/signups/[leadId]/page";
38
-
39
- const SCORED_DETAIL = {
40
- leadId: "lead_1",
41
- experimentId: "wlexp_1",
42
- experimentKey: "early-access",
43
- waitlistKey: "main",
44
- email: "a@example.com",
45
- createdAt: "2026-08-15T00:00:00.000Z",
46
- source: "google",
47
- answers: [{ questionId: "q1", prompt: { en: "Do you use a CRM?" }, value: "yes" }],
48
- score: {
49
- tier: "qualified",
50
- score: 87,
51
- categoryScores: { fit: 40, intent: 47 },
52
- ruleTrace: { matched: ["rule_1"] },
53
- updatedAt: "2026-08-15T00:05:00.000Z",
54
- },
55
- };
56
-
57
- const UNSCORED_DETAIL = { ...SCORED_DETAIL, score: null };
58
-
59
- // FEAT-074 U4a (#4715, AC-5) — a targeting-maturity signup whose ruleTrace
60
- // carries the SAME `RuleTraceEntry[]` shape `evaluateWaitlistScoring`
61
- // produces, so `decidingDefsFor` has something real to extract.
62
- const TARGETING_MATURITY_DETAIL = {
63
- ...SCORED_DETAIL,
64
- experimentKey: "targeting-maturity",
65
- score: {
66
- tier: "level_1",
67
- score: 0,
68
- categoryScores: { total: 0 },
69
- ruleTrace: [
70
- {
71
- kind: "predicate",
72
- tier: "level_1",
73
- path: "allOf[0].ref(level1_only).allOf[0].ref(level1_signal).anyOf[0].ref(has_customers).eq",
74
- node: {},
75
- result: true,
76
- detail: "has_customers = true",
77
- },
78
- {
79
- kind: "predicate",
80
- tier: "level_1",
81
- path: "allOf[0].ref(level1_only).allOf[0].ref(level1_signal).anyOf[1].ref(has_confirmed_fit_cohort).eq",
82
- node: {},
83
- result: false,
84
- detail: "has_confirmed_fit_cohort = false",
85
- },
86
- ],
87
- updatedAt: "2026-08-15T00:05:00.000Z",
88
- },
89
- };
90
-
91
- function baseProps() {
92
- return {
93
- params: Promise.resolve({ locale: "en", experimentId: "wlexp_1", leadId: "lead_1" }),
94
- };
95
- }
96
-
97
- describe("WaitlistSignupPage — drill-down (AC3)", () => {
98
- beforeEach(() => {
99
- mockSignup.mockReset();
100
- mockNotFound.mockClear();
101
- });
102
-
103
- it("renders answers, tier, score, categoryScores, and ruleTrace when scored", async () => {
104
- mockSignup.mockResolvedValue({ ok: true, body: SCORED_DETAIL });
105
- const element = await Page(baseProps());
106
- render(element);
107
-
108
- expect(screen.getByText("Do you use a CRM?")).toBeInTheDocument();
109
- expect(screen.getByText("yes")).toBeInTheDocument();
110
- expect(screen.getByText("qualified")).toBeInTheDocument();
111
- expect(screen.getByText("87")).toBeInTheDocument();
112
- expect(screen.getByTestId("score-trace-category-scores")).toBeInTheDocument();
113
- expect(screen.getByTestId("code-block")).toBeInTheDocument();
114
- expect(screen.getByText(/"matched"/)).toBeInTheDocument();
115
- });
116
-
117
- it("locale-formats the score and category scores rather than rendering raw numbers", async () => {
118
- mockSignup.mockResolvedValue({
119
- ok: true,
120
- body: { ...SCORED_DETAIL, score: { ...SCORED_DETAIL.score, score: 1234, categoryScores: { fit: 2500 } } },
121
- });
122
- const element = await Page(baseProps());
123
- render(element);
124
-
125
- expect(screen.getByText("1,234")).toBeInTheDocument();
126
- expect(screen.getByText("2,500")).toBeInTheDocument();
127
- });
128
-
129
- it('renders "not scored" EmptyState when score is null', async () => {
130
- mockSignup.mockResolvedValue({ ok: true, body: UNSCORED_DETAIL });
131
- const element = await Page(baseProps());
132
- render(element);
133
-
134
- expect(screen.getByTestId("score-trace-not-scored")).toBeInTheDocument();
135
- expect(screen.queryByTestId("code-block")).not.toBeInTheDocument();
136
- });
137
-
138
- it("calls notFound() on a 404", async () => {
139
- mockSignup.mockResolvedValue({
140
- ok: false,
141
- status: 404,
142
- body: { error: { code: "not_found", message: "Signup not found" } },
143
- });
144
- await expect(Page(baseProps())).rejects.toThrow("NEXT_NOT_FOUND");
145
- expect(mockNotFound).toHaveBeenCalled();
146
- });
147
-
148
- it("renders ForbiddenState on a 401", async () => {
149
- mockSignup.mockResolvedValue({
150
- ok: false,
151
- status: 401,
152
- body: { error: { code: "unauthorized", message: "Authentication required" } },
153
- });
154
- const element = await Page(baseProps());
155
- render(element);
156
-
157
- expect(screen.getByTestId("waitlist-forbidden")).toBeInTheDocument();
158
- });
159
-
160
- it("a non-targeting experiment renders no deciding-defs section (AC-5 — byte-unchanged)", async () => {
161
- mockSignup.mockResolvedValue({ ok: true, body: SCORED_DETAIL });
162
- const element = await Page(baseProps());
163
- render(element);
164
-
165
- expect(screen.queryByTestId("score-trace-deciding-defs")).not.toBeInTheDocument();
166
- });
167
-
168
- it("a targeting-maturity signup lists the deciding def names extracted from its own tier's trace (AC-5)", async () => {
169
- mockSignup.mockResolvedValue({ ok: true, body: TARGETING_MATURITY_DETAIL });
170
- const element = await Page(baseProps());
171
- render(element);
172
-
173
- const section = screen.getByTestId("score-trace-deciding-defs");
174
- expect(section).toHaveTextContent("has_customers");
175
- // The false-result entry's def must NOT appear — only true-result
176
- // predicates for the matched tier are "deciding".
177
- expect(section).not.toHaveTextContent("has_confirmed_fit_cohort");
178
- });
179
- });