create-ailk 0.1.1 → 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.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file waitlist-scoring.test.ts
|
|
3
|
-
* @description Unit tests for the waitlist-scoring glue service (#4662 D6).
|
|
4
|
-
*
|
|
5
|
-
* External dependency boundary mocked: @working-theory/database (auto-mock
|
|
6
|
-
* via `../../lib/__mocks__/prisma.ts`, mirroring waitlist-experiments.test.ts).
|
|
7
|
-
* `@working-theory/observability`'s logger is mocked to a spyable stub
|
|
8
|
-
* (mirrors deliverable-fulfillment.test.ts / abandon-email.test.ts) so the
|
|
9
|
-
* PII-safety of the error logs is directly assertable.
|
|
10
|
-
*
|
|
11
|
-
* `@working-theory/validation`'s `evaluateWaitlistScoring` is a PARTIAL mock
|
|
12
|
-
* (every other export runs real, incl. `waitlistExperimentConfigSchema` and
|
|
13
|
-
* `answersFromPayload`) — every one of the pure evaluator's own defensive
|
|
14
|
-
* throws (unresolvable ref/score/sum term, a ref cycle) is on a register
|
|
15
|
-
* shape `waitlistExperimentConfigSchema`'s own superRefine already rejects
|
|
16
|
-
* (waitlist-scoring.test.ts, packages/validation, covers those exhaustively
|
|
17
|
-
* against a register that bypasses the schema entirely). So the ONLY way to
|
|
18
|
-
* observe this glue's `evaluation_failed` outcome against a schema-VALID
|
|
19
|
-
* config is to make the (already schema-validated, already unit-tested)
|
|
20
|
-
* evaluator itself throw for one call — not to hand-craft an "invalid but
|
|
21
|
-
* schema-valid" register, which cannot exist by construction.
|
|
22
|
-
*/
|
|
23
|
-
// 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)
|
|
24
|
-
jest.mock("../../lib/prisma.js");
|
|
25
|
-
|
|
26
|
-
const mockLoggerError = jest.fn();
|
|
27
|
-
jest.mock("@working-theory/observability", () => ({
|
|
28
|
-
logger: {
|
|
29
|
-
info: jest.fn(),
|
|
30
|
-
warn: jest.fn(),
|
|
31
|
-
error: (...args: unknown[]) => mockLoggerError(...args),
|
|
32
|
-
debug: jest.fn(),
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
jest.mock("@working-theory/validation", () => {
|
|
37
|
-
const actual = jest.requireActual("@working-theory/validation");
|
|
38
|
-
return { ...actual, evaluateWaitlistScoring: jest.fn(actual.evaluateWaitlistScoring) };
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
import { evaluateWaitlistScoring } from "@working-theory/validation";
|
|
42
|
-
|
|
43
|
-
import { prisma } from "../../lib/prisma.js";
|
|
44
|
-
import { scoreCompletedSignup } from "../waitlist-scoring";
|
|
45
|
-
|
|
46
|
-
const en = (s: string) => ({ en: s });
|
|
47
|
-
const SITE_ID = "site-abc";
|
|
48
|
-
const LEAD_ID = "lead-1";
|
|
49
|
-
const EXPERIMENT_KEY = "my-experiment";
|
|
50
|
-
const WAITLIST_KEY = "main";
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
jest.clearAllMocks();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function noScoringConfig() {
|
|
57
|
-
return {
|
|
58
|
-
key: EXPERIMENT_KEY,
|
|
59
|
-
mode: "lead-capture" as const,
|
|
60
|
-
status: "draft" as const,
|
|
61
|
-
waitlists: [{ key: WAITLIST_KEY, flowSlug: EXPERIMENT_KEY }],
|
|
62
|
-
questionSet: {
|
|
63
|
-
version: 1 as const,
|
|
64
|
-
questions: [
|
|
65
|
-
{
|
|
66
|
-
id: "email",
|
|
67
|
-
prompt: en("Your email"),
|
|
68
|
-
answerType: "email" as const,
|
|
69
|
-
mapsTo: "email" as const,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
tracking: {
|
|
74
|
-
version: 1 as const,
|
|
75
|
-
utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
|
|
76
|
-
waitlists: { [WAITLIST_KEY]: { campaign: "launch", channels: ["organic"] } },
|
|
77
|
-
},
|
|
78
|
-
stripeConfigRef: null,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function validQualifyingConfig() {
|
|
83
|
-
return {
|
|
84
|
-
key: EXPERIMENT_KEY,
|
|
85
|
-
mode: "qualifying" as const,
|
|
86
|
-
status: "draft" as const,
|
|
87
|
-
waitlists: [{ key: WAITLIST_KEY, flowSlug: EXPERIMENT_KEY }],
|
|
88
|
-
questionSet: {
|
|
89
|
-
version: 1 as const,
|
|
90
|
-
questions: [
|
|
91
|
-
{
|
|
92
|
-
id: "email",
|
|
93
|
-
prompt: en("Your email"),
|
|
94
|
-
answerType: "email" as const,
|
|
95
|
-
mapsTo: "email" as const,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: "budget",
|
|
99
|
-
prompt: en("What's your budget?"),
|
|
100
|
-
answerType: "single" as const,
|
|
101
|
-
options: [
|
|
102
|
-
{ value: "small", label: en("Small") },
|
|
103
|
-
{ value: "large", label: en("Large") },
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
scoring: {
|
|
109
|
-
version: 1 as const,
|
|
110
|
-
points: [
|
|
111
|
-
{
|
|
112
|
-
name: "budget_points",
|
|
113
|
-
question: "budget",
|
|
114
|
-
kind: "optionWeights" as const,
|
|
115
|
-
weights: { small: 1, large: 10 },
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
scores: { total: { sum: ["budget_points"] } },
|
|
119
|
-
tiers: [{ key: "hot", when: { score: "total", gte: 5 } }],
|
|
120
|
-
default: "hot",
|
|
121
|
-
},
|
|
122
|
-
tracking: {
|
|
123
|
-
version: 1 as const,
|
|
124
|
-
utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
|
|
125
|
-
waitlists: { [WAITLIST_KEY]: { campaign: "launch", channels: ["organic"] } },
|
|
126
|
-
},
|
|
127
|
-
stripeConfigRef: null,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
describe("scoreCompletedSignup (D6)", () => {
|
|
132
|
-
it("no experiment row ⇒ skipped: no_experiment, no upsert", async () => {
|
|
133
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue(null);
|
|
134
|
-
|
|
135
|
-
const result = await scoreCompletedSignup({
|
|
136
|
-
siteId: SITE_ID,
|
|
137
|
-
leadId: LEAD_ID,
|
|
138
|
-
experimentKey: EXPERIMENT_KEY,
|
|
139
|
-
waitlistKey: WAITLIST_KEY,
|
|
140
|
-
payload: {},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(result).toEqual({ ok: true, skipped: "no_experiment" });
|
|
144
|
-
expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("a config with no scoring block ⇒ skipped: no_scoring, no upsert (a lead-capture experiment)", async () => {
|
|
148
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
149
|
-
config: noScoringConfig(),
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = await scoreCompletedSignup({
|
|
153
|
-
siteId: SITE_ID,
|
|
154
|
-
leadId: LEAD_ID,
|
|
155
|
-
experimentKey: EXPERIMENT_KEY,
|
|
156
|
-
waitlistKey: WAITLIST_KEY,
|
|
157
|
-
payload: {},
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
expect(result).toEqual({ ok: true, skipped: "no_scoring" });
|
|
161
|
-
expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("an invalid stored config (defensive — the row was validated on write) ⇒ invalid_config, no upsert", async () => {
|
|
165
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
166
|
-
config: { not: "a config" },
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const result = await scoreCompletedSignup({
|
|
170
|
-
siteId: SITE_ID,
|
|
171
|
-
leadId: LEAD_ID,
|
|
172
|
-
experimentKey: EXPERIMENT_KEY,
|
|
173
|
-
waitlistKey: WAITLIST_KEY,
|
|
174
|
-
payload: {},
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(result).toEqual({ ok: false, error: { kind: "invalid_config" } });
|
|
178
|
-
expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("a valid qualifying config ⇒ exactly one upsert on {leadId, experimentKey} carrying the evaluator's output", async () => {
|
|
182
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
183
|
-
config: validQualifyingConfig(),
|
|
184
|
-
});
|
|
185
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
|
|
186
|
-
|
|
187
|
-
const result = await scoreCompletedSignup({
|
|
188
|
-
siteId: SITE_ID,
|
|
189
|
-
leadId: LEAD_ID,
|
|
190
|
-
experimentKey: EXPERIMENT_KEY,
|
|
191
|
-
waitlistKey: WAITLIST_KEY,
|
|
192
|
-
payload: { budget: "large" },
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
expect(result).toEqual({ ok: true, tier: "hot" });
|
|
196
|
-
expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(1);
|
|
197
|
-
expect(prisma.waitlistScore.upsert).toHaveBeenCalledWith(
|
|
198
|
-
expect.objectContaining({
|
|
199
|
-
// The composite key since #5036 D6d — `{ leadId }` alone scoped one
|
|
200
|
-
// score per LEAD, which silently erased a founder's evidence the
|
|
201
|
-
// moment a visitor joined a second listing's waitlist.
|
|
202
|
-
where: { leadId_experimentKey: { leadId: LEAD_ID, experimentKey: EXPERIMENT_KEY } },
|
|
203
|
-
create: expect.objectContaining({
|
|
204
|
-
siteId: SITE_ID,
|
|
205
|
-
leadId: LEAD_ID,
|
|
206
|
-
experimentKey: EXPERIMENT_KEY,
|
|
207
|
-
waitlistKey: WAITLIST_KEY,
|
|
208
|
-
tier: "hot",
|
|
209
|
-
score: 10,
|
|
210
|
-
categoryScores: { total: 10 },
|
|
211
|
-
}),
|
|
212
|
-
update: expect.objectContaining({
|
|
213
|
-
tier: "hot",
|
|
214
|
-
score: 10,
|
|
215
|
-
categoryScores: { total: 10 },
|
|
216
|
-
}),
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("a second call on the same lead re-scores via the same upsert call — update, never a second row", async () => {
|
|
222
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
223
|
-
config: validQualifyingConfig(),
|
|
224
|
-
});
|
|
225
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
|
|
226
|
-
|
|
227
|
-
await scoreCompletedSignup({
|
|
228
|
-
siteId: SITE_ID,
|
|
229
|
-
leadId: LEAD_ID,
|
|
230
|
-
experimentKey: EXPERIMENT_KEY,
|
|
231
|
-
waitlistKey: WAITLIST_KEY,
|
|
232
|
-
payload: { budget: "large" },
|
|
233
|
-
});
|
|
234
|
-
await scoreCompletedSignup({
|
|
235
|
-
siteId: SITE_ID,
|
|
236
|
-
leadId: LEAD_ID,
|
|
237
|
-
experimentKey: EXPERIMENT_KEY,
|
|
238
|
-
waitlistKey: WAITLIST_KEY,
|
|
239
|
-
payload: { budget: "small" },
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(2);
|
|
243
|
-
const calls = (prisma.waitlistScore.upsert as jest.Mock).mock.calls;
|
|
244
|
-
// Same lead, same experiment ⇒ the SAME composite key both times, so the
|
|
245
|
-
// second write lands as an update. Re-scoring overwriting is the intended
|
|
246
|
-
// behaviour; #5036 D6d only scoped it per experiment, it did not remove it.
|
|
247
|
-
const key = { leadId_experimentKey: { leadId: LEAD_ID, experimentKey: EXPERIMENT_KEY } };
|
|
248
|
-
expect(calls[0]![0]).toMatchObject({ where: key });
|
|
249
|
-
expect(calls[1]![0]).toMatchObject({ where: key });
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// ── #5036 D6d — one lead, two experiments, two surviving rows ───────────
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* The correctness defect D6d straightened, asserted from the outside.
|
|
256
|
-
*
|
|
257
|
-
* `WaitlistScore` carried `@@unique([leadId])` — "one row per lead". Right
|
|
258
|
-
* for a single instrument, wrong for a marketplace of them: a visitor who
|
|
259
|
-
* joins two founders' waitlists is one lead with TWO scores, and under
|
|
260
|
-
* `where: { leadId }` the second upsert resolved the SAME row as the first
|
|
261
|
-
* and overwrote it. One founder's evidence vanished into a CSV that
|
|
262
|
-
* reported fewer signups than actually happened, with no error raised
|
|
263
|
-
* anywhere.
|
|
264
|
-
*
|
|
265
|
-
* What makes the two rows survive is that the two writes address DIFFERENT
|
|
266
|
-
* keys, so this test asserts exactly that and nothing about Prisma's own
|
|
267
|
-
* upsert semantics.
|
|
268
|
-
*/
|
|
269
|
-
it("scores one lead against two experiments — the two upserts carry DIFFERENT keys, so neither can overwrite the other", async () => {
|
|
270
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
271
|
-
config: validQualifyingConfig(),
|
|
272
|
-
});
|
|
273
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
|
|
274
|
-
|
|
275
|
-
for (const experimentKey of ["exp-a", "exp-b"]) {
|
|
276
|
-
await scoreCompletedSignup({
|
|
277
|
-
siteId: SITE_ID,
|
|
278
|
-
leadId: LEAD_ID,
|
|
279
|
-
experimentKey,
|
|
280
|
-
waitlistKey: WAITLIST_KEY,
|
|
281
|
-
payload: { budget: "large" },
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(2);
|
|
286
|
-
const [first, second] = (prisma.waitlistScore.upsert as jest.Mock).mock.calls.map(
|
|
287
|
-
(call) => call[0] as { where: unknown },
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
expect(first!.where).toEqual({
|
|
291
|
-
leadId_experimentKey: { leadId: LEAD_ID, experimentKey: "exp-a" },
|
|
292
|
-
});
|
|
293
|
-
expect(second!.where).toEqual({
|
|
294
|
-
leadId_experimentKey: { leadId: LEAD_ID, experimentKey: "exp-b" },
|
|
295
|
-
});
|
|
296
|
-
// The load-bearing assertion: two distinct addresses, so the second write
|
|
297
|
-
// cannot resolve the first row. Under the old `where: { leadId }` both of
|
|
298
|
-
// these were `{ leadId: "lead-1" }` and this comparison would be equal.
|
|
299
|
-
expect(first!.where).not.toEqual(second!.where);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("an evaluator throw ⇒ evaluation_failed, no upsert", async () => {
|
|
303
|
-
// The evaluator's own defensive throws (WaitlistEvaluationError, for an
|
|
304
|
-
// unresolvable ref/score/sum term or a ref cycle) are exhaustively
|
|
305
|
-
// covered against a register that bypasses the schema, in
|
|
306
|
-
// packages/validation/src/schemas/__tests__/waitlist-scoring.test.ts — a
|
|
307
|
-
// schema-VALID config cannot reach any of them (waitlistExperimentConfigSchema's
|
|
308
|
-
// own superRefine already rejects every such shape). So this glue-level
|
|
309
|
-
// test forces the (real, unit-tested) evaluator to throw for one call,
|
|
310
|
-
// to prove the glue's own catch → `evaluation_failed` → no upsert path.
|
|
311
|
-
(evaluateWaitlistScoring as jest.Mock).mockImplementationOnce(() => {
|
|
312
|
-
throw new Error("boom");
|
|
313
|
-
});
|
|
314
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
315
|
-
config: validQualifyingConfig(),
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const result = await scoreCompletedSignup({
|
|
319
|
-
siteId: SITE_ID,
|
|
320
|
-
leadId: LEAD_ID,
|
|
321
|
-
experimentKey: EXPERIMENT_KEY,
|
|
322
|
-
waitlistKey: WAITLIST_KEY,
|
|
323
|
-
payload: { budget: "large" },
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
expect(result).toEqual({ ok: false, error: { kind: "evaluation_failed" } });
|
|
327
|
-
expect(prisma.waitlistScore.upsert).not.toHaveBeenCalled();
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("a persist (Prisma) failure ⇒ persist_failed", async () => {
|
|
331
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
332
|
-
config: validQualifyingConfig(),
|
|
333
|
-
});
|
|
334
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockRejectedValue(new Error("db down"));
|
|
335
|
-
|
|
336
|
-
const result = await scoreCompletedSignup({
|
|
337
|
-
siteId: SITE_ID,
|
|
338
|
-
leadId: LEAD_ID,
|
|
339
|
-
experimentKey: EXPERIMENT_KEY,
|
|
340
|
-
waitlistKey: WAITLIST_KEY,
|
|
341
|
-
payload: { budget: "large" },
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
expect(result).toEqual({ ok: false, error: { kind: "persist_failed" } });
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("the log line carries no answer value — PII-safe outcome logs only", async () => {
|
|
348
|
-
(evaluateWaitlistScoring as jest.Mock).mockImplementationOnce(() => {
|
|
349
|
-
throw new Error("boom");
|
|
350
|
-
});
|
|
351
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
352
|
-
config: validQualifyingConfig(),
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
await scoreCompletedSignup({
|
|
356
|
-
siteId: SITE_ID,
|
|
357
|
-
leadId: LEAD_ID,
|
|
358
|
-
experimentKey: EXPERIMENT_KEY,
|
|
359
|
-
waitlistKey: WAITLIST_KEY,
|
|
360
|
-
payload: { budget: "large" },
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
expect(mockLoggerError).toHaveBeenCalledWith(
|
|
364
|
-
expect.objectContaining({
|
|
365
|
-
siteId: SITE_ID,
|
|
366
|
-
experimentKey: EXPERIMENT_KEY,
|
|
367
|
-
leadId: LEAD_ID,
|
|
368
|
-
kind: "evaluation_failed",
|
|
369
|
-
}),
|
|
370
|
-
expect.any(String),
|
|
371
|
-
);
|
|
372
|
-
const [logMeta] = mockLoggerError.mock.calls[0]!;
|
|
373
|
-
expect(JSON.stringify(logMeta)).not.toContain("large");
|
|
374
|
-
expect(JSON.stringify(logMeta)).not.toContain("budget");
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe("scoreCompletedSignup — FEAT-074 U4a playbook compile hook (#4715)", () => {
|
|
379
|
-
// Exercises the REAL compileAndStorePlaybook (no jest.mock of the internal
|
|
380
|
-
// module — CONTRIBUTING.md § Test code quality / #507 forbids mocking an
|
|
381
|
-
// internal module; the real boundary is prisma + observability, both
|
|
382
|
-
// already mocked above). The tier must be a real `level_N` the VENDORED
|
|
383
|
-
// library declares (Level 1) so the real compile succeeds — this mirrors
|
|
384
|
-
// validQualifyingConfig()'s own register shape, only its tiers/default
|
|
385
|
-
// differ, so it stays a minimal diff from the existing fixture.
|
|
386
|
-
const TARGETING_MATURITY_KEY = "targeting-maturity";
|
|
387
|
-
|
|
388
|
-
function targetingMaturityConfig() {
|
|
389
|
-
const base = validQualifyingConfig();
|
|
390
|
-
return {
|
|
391
|
-
...base,
|
|
392
|
-
key: TARGETING_MATURITY_KEY,
|
|
393
|
-
scoring: {
|
|
394
|
-
...base.scoring,
|
|
395
|
-
tiers: [{ key: "level_1", when: { score: "total", gte: 0 } }],
|
|
396
|
-
default: "level_1",
|
|
397
|
-
},
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
it("experimentKey === targeting-maturity ⇒ a PlaybookInstance row is created from the upserted score's id and tier (AC-3)", async () => {
|
|
402
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
403
|
-
config: targetingMaturityConfig(),
|
|
404
|
-
});
|
|
405
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_playbook_1" });
|
|
406
|
-
(prisma.playbookInstance.create as jest.Mock).mockResolvedValue({ id: "pbk_1" });
|
|
407
|
-
|
|
408
|
-
const result = await scoreCompletedSignup({
|
|
409
|
-
siteId: SITE_ID,
|
|
410
|
-
leadId: LEAD_ID,
|
|
411
|
-
experimentKey: TARGETING_MATURITY_KEY,
|
|
412
|
-
waitlistKey: WAITLIST_KEY,
|
|
413
|
-
payload: { budget: "large" },
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
expect(result).toEqual({ ok: true, tier: "level_1" });
|
|
417
|
-
expect(prisma.playbookInstance.create).toHaveBeenCalledTimes(1);
|
|
418
|
-
const [call] = (prisma.playbookInstance.create as jest.Mock).mock.calls[0]!;
|
|
419
|
-
expect(call.data).toMatchObject({
|
|
420
|
-
siteId: SITE_ID,
|
|
421
|
-
leadId: LEAD_ID,
|
|
422
|
-
experimentKey: TARGETING_MATURITY_KEY,
|
|
423
|
-
scoreId: "wlscore_playbook_1",
|
|
424
|
-
tier: "level_1",
|
|
425
|
-
level: 1,
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it("any other experimentKey ⇒ no PlaybookInstance row is created", async () => {
|
|
430
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
431
|
-
config: validQualifyingConfig(),
|
|
432
|
-
});
|
|
433
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_1" });
|
|
434
|
-
|
|
435
|
-
const result = await scoreCompletedSignup({
|
|
436
|
-
siteId: SITE_ID,
|
|
437
|
-
leadId: LEAD_ID,
|
|
438
|
-
experimentKey: EXPERIMENT_KEY,
|
|
439
|
-
waitlistKey: WAITLIST_KEY,
|
|
440
|
-
payload: { budget: "large" },
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
expect(result).toEqual({ ok: true, tier: "hot" });
|
|
444
|
-
expect(prisma.playbookInstance.create).not.toHaveBeenCalled();
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it("a compile/store failure leaves the score intact — scoreCompletedSignup still resolves ok:true, never throws to its caller (AC-4)", async () => {
|
|
448
|
-
(prisma.waitlistExperiment.findUnique as jest.Mock).mockResolvedValue({
|
|
449
|
-
config: targetingMaturityConfig(),
|
|
450
|
-
});
|
|
451
|
-
(prisma.waitlistScore.upsert as jest.Mock).mockResolvedValue({ id: "wlscore_playbook_2" });
|
|
452
|
-
(prisma.playbookInstance.create as jest.Mock).mockRejectedValueOnce(new Error("db down"));
|
|
453
|
-
|
|
454
|
-
const result = await scoreCompletedSignup({
|
|
455
|
-
siteId: SITE_ID,
|
|
456
|
-
leadId: LEAD_ID,
|
|
457
|
-
experimentKey: TARGETING_MATURITY_KEY,
|
|
458
|
-
waitlistKey: WAITLIST_KEY,
|
|
459
|
-
payload: { budget: "large" },
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
expect(result).toEqual({ ok: true, tier: "level_1" });
|
|
463
|
-
expect(prisma.waitlistScore.upsert).toHaveBeenCalledTimes(1);
|
|
464
|
-
expect(mockLoggerError).toHaveBeenCalledWith(
|
|
465
|
-
expect.objectContaining({
|
|
466
|
-
siteId: SITE_ID,
|
|
467
|
-
experimentKey: TARGETING_MATURITY_KEY,
|
|
468
|
-
leadId: LEAD_ID,
|
|
469
|
-
kind: "playbook_compile_failed",
|
|
470
|
-
}),
|
|
471
|
-
expect.any(String),
|
|
472
|
-
);
|
|
473
|
-
});
|
|
474
|
-
});
|