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,475 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file project-listing-flow.test.ts
|
|
3
|
-
* @description Unit tests for the approve-time projection (#5001 AC1/AC2/AC3):
|
|
4
|
-
* the template default's `questions` block becomes a `FlowDefinition` with the
|
|
5
|
-
* six-category step order and a live `WaitlistExperiment` keyed `{slug}`, the
|
|
6
|
-
* row's `experimentId` is written back, the thank-you shows the confirmation
|
|
7
|
-
* line and never the tier, and two different listings carry the same scoring
|
|
8
|
-
* RULE over their own option values (#5036 D4).
|
|
9
|
-
*
|
|
10
|
-
* The configs the projection builds are asserted against the REAL
|
|
11
|
-
* `flowConfigSchema` / `waitlistExperimentConfigSchema` as they arrive at the
|
|
12
|
-
* (mocked) Prisma upsert, so a projection that could not actually persist
|
|
13
|
-
* fails here rather than at approve time.
|
|
14
|
-
*
|
|
15
|
-
* External dependency boundaries mocked: @working-theory/database (auto-mock
|
|
16
|
-
* via `../../lib/__mocks__/prisma.ts`), plus the same
|
|
17
|
-
* `@working-theory/email` / `@working-theory/license` / `../aeo-score.js`
|
|
18
|
-
* boundary mocks `waitlist-experiments.test.ts` uses — this service composes
|
|
19
|
-
* `upsertFlowDefinition` from `flow-engine.ts`, whose import chain reaches all
|
|
20
|
-
* three.
|
|
21
|
-
*/
|
|
22
|
-
// 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)
|
|
23
|
-
jest.mock("../../lib/prisma.js");
|
|
24
|
-
|
|
25
|
-
jest.mock("@working-theory/email", () => ({
|
|
26
|
-
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
27
|
-
sendDeliverableEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-deliverable-test" }),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
jest.mock("@working-theory/license", () => ({
|
|
31
|
-
checkLicense: jest.fn().mockResolvedValue({
|
|
32
|
-
tier: "free",
|
|
33
|
-
plan: null,
|
|
34
|
-
features: [],
|
|
35
|
-
expiresAt: null,
|
|
36
|
-
revoked: false,
|
|
37
|
-
}),
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// eslint-disable-next-line no-restricted-syntax -- aeo-score.ts is a network boundary (external crawl), same class as the DB mock above
|
|
41
|
-
jest.mock("../aeo-score.js", () => ({
|
|
42
|
-
scoreUrl: jest.fn(),
|
|
43
|
-
loadExampleScore: jest.fn(),
|
|
44
|
-
resolveScoreBudgetMs: () => 6000,
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
import { buildListingDiagnosticScoring } from "@working-theory/database/flows/listing-diagnostic";
|
|
48
|
-
import {
|
|
49
|
-
flowConfigSchema,
|
|
50
|
-
projectListingQuestionsSchema,
|
|
51
|
-
waitlistExperimentConfigSchema,
|
|
52
|
-
type FlowConfig,
|
|
53
|
-
type ProjectListingQuestions,
|
|
54
|
-
} from "@working-theory/validation";
|
|
55
|
-
|
|
56
|
-
import { prisma } from "../../lib/prisma.js";
|
|
57
|
-
import {
|
|
58
|
-
generateListingFlow,
|
|
59
|
-
listingAnswerLabels,
|
|
60
|
-
listingWaitlistKey,
|
|
61
|
-
type ListingFlowSource,
|
|
62
|
-
} from "../project-listing-flow";
|
|
63
|
-
|
|
64
|
-
const SITE_ID = "site-abc";
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The template default (#4998's `projectListingQuestionsSchema`): Q2's
|
|
68
|
-
* founder options terminated by the fixed `just-curious` bail-out, Q3/Q4 at
|
|
69
|
-
* four founder options each, Q5's three fixed urgency values, Q6's four fixed
|
|
70
|
-
* bands, Q7 wording only, no extras.
|
|
71
|
-
*/
|
|
72
|
-
const TEMPLATE_DEFAULT: ProjectListingQuestions = projectListingQuestionsSchema.parse({
|
|
73
|
-
q2: {
|
|
74
|
-
prompt: "What brings you here?",
|
|
75
|
-
options: [
|
|
76
|
-
{ value: "shipping-now", label: "I'm shipping something now" },
|
|
77
|
-
{ value: "planning", label: "I'm planning a build" },
|
|
78
|
-
{ value: "just-curious", label: "Just curious" },
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
q3: {
|
|
82
|
-
prompt: "How painful is this problem today?",
|
|
83
|
-
options: [
|
|
84
|
-
{ value: "none", label: "Not a problem" },
|
|
85
|
-
{ value: "mild", label: "Mildly annoying" },
|
|
86
|
-
{ value: "real", label: "A real drag" },
|
|
87
|
-
{ value: "blocking", label: "It's blocking me" },
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
q4: {
|
|
91
|
-
prompt: "How close is this to what you need?",
|
|
92
|
-
options: [
|
|
93
|
-
{ value: "off", label: "Not what I need" },
|
|
94
|
-
{ value: "adjacent", label: "Adjacent" },
|
|
95
|
-
{ value: "close", label: "Close" },
|
|
96
|
-
{ value: "exact", label: "Exactly it" },
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
q5: {
|
|
100
|
-
prompt: "When would you use this?",
|
|
101
|
-
options: [
|
|
102
|
-
{ value: "urgent", label: "Right away" },
|
|
103
|
-
{ value: "planning", label: "Next quarter" },
|
|
104
|
-
{ value: "exploring", label: "Just looking" },
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
q6: {
|
|
108
|
-
prompt: "What would you pay?",
|
|
109
|
-
options: [
|
|
110
|
-
{ value: "low", label: "Nothing yet" },
|
|
111
|
-
{ value: "medium", label: "A little" },
|
|
112
|
-
{ value: "high", label: "A real budget" },
|
|
113
|
-
{ value: "critical", label: "Whatever it takes" },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
q7: { prompt: "Anything else we should know?" },
|
|
117
|
-
extras: [],
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
function listing(overrides: Partial<ListingFlowSource> = {}): ListingFlowSource {
|
|
121
|
-
return {
|
|
122
|
-
id: "prjl_1",
|
|
123
|
-
siteId: SITE_ID,
|
|
124
|
-
slug: "orbit-notes",
|
|
125
|
-
name: "Orbit Notes",
|
|
126
|
-
oneLiner: "Meeting notes that file themselves.",
|
|
127
|
-
questions: TEMPLATE_DEFAULT,
|
|
128
|
-
...overrides,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The `FlowConfig` handed to `flowDefinition.upsert`, as it was persisted. */
|
|
133
|
-
function upsertedFlow(): FlowConfig {
|
|
134
|
-
const call = (prisma.flowDefinition.upsert as jest.Mock).mock.calls[0]?.[0];
|
|
135
|
-
return call.create.config as FlowConfig;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** The experiment config handed to `waitlistExperiment.upsert`. */
|
|
139
|
-
function upsertedExperiment(): Record<string, unknown> {
|
|
140
|
-
const call = (prisma.waitlistExperiment.upsert as jest.Mock).mock.calls[0]?.[0];
|
|
141
|
-
return call.create.config as Record<string, unknown>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
beforeEach(() => {
|
|
145
|
-
jest.clearAllMocks();
|
|
146
|
-
process.env.DATABASE_URL = "postgres://test";
|
|
147
|
-
(prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({ id: "flow_1" });
|
|
148
|
-
(prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({ id: "wlexp_1" });
|
|
149
|
-
(prisma.projectListing.update as jest.Mock).mockResolvedValue({ id: "prjl_1" });
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe("generateListingFlow — the flow (#5001 AC1)", () => {
|
|
153
|
-
// #5036 D6d: the write-back is the experiment's KEY, and the key IS the
|
|
154
|
-
// listing's slug (`buildExperimentConfig`). Storing the id meant a reader
|
|
155
|
-
// going from a listing to its `WaitlistScore` rows had to fetch the
|
|
156
|
-
// experiment first purely to learn that key; storing the key is one hop and
|
|
157
|
-
// the same string on both sides of the join.
|
|
158
|
-
it("writes a servable flow, a live experiment, and the row's experimentKey", async () => {
|
|
159
|
-
const result = await generateListingFlow(listing());
|
|
160
|
-
|
|
161
|
-
expect(result).toEqual({
|
|
162
|
-
ok: true,
|
|
163
|
-
flowSlug: "orbit-notes",
|
|
164
|
-
experimentKey: "orbit-notes",
|
|
165
|
-
});
|
|
166
|
-
expect(prisma.projectListing.update).toHaveBeenCalledWith({
|
|
167
|
-
where: { id: "prjl_1" },
|
|
168
|
-
data: { experimentKey: "orbit-notes" },
|
|
169
|
-
});
|
|
170
|
-
expect(
|
|
171
|
-
(prisma.flowDefinition.upsert as jest.Mock).mock.calls[0]?.[0].where,
|
|
172
|
-
).toEqual({ siteId_slug: { siteId: SITE_ID, slug: "orbit-notes" } });
|
|
173
|
-
expect(
|
|
174
|
-
(prisma.waitlistExperiment.upsert as jest.Mock).mock.calls[0]?.[0].where,
|
|
175
|
-
).toEqual({ siteId_key: { siteId: SITE_ID, key: "orbit-notes" } });
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("declares the six-category step order, then the confirmation", async () => {
|
|
179
|
-
await generateListingFlow(listing());
|
|
180
|
-
|
|
181
|
-
expect(upsertedFlow().steps.map((s) => s.key)).toEqual([
|
|
182
|
-
"contact",
|
|
183
|
-
"segment",
|
|
184
|
-
"problem",
|
|
185
|
-
"fit",
|
|
186
|
-
"urgency",
|
|
187
|
-
"price",
|
|
188
|
-
"open",
|
|
189
|
-
"done",
|
|
190
|
-
]);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("appends an extras step only when the row carries extras", async () => {
|
|
194
|
-
const questions = projectListingQuestionsSchema.parse({
|
|
195
|
-
...TEMPLATE_DEFAULT,
|
|
196
|
-
extras: [
|
|
197
|
-
{
|
|
198
|
-
prompt: "Which stack?",
|
|
199
|
-
options: [
|
|
200
|
-
{ value: "a", label: "TypeScript" },
|
|
201
|
-
{ value: "b", label: "Python" },
|
|
202
|
-
],
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await generateListingFlow(listing({ questions }));
|
|
208
|
-
|
|
209
|
-
const steps = upsertedFlow().steps;
|
|
210
|
-
expect(steps.map((s) => s.key)).toEqual([
|
|
211
|
-
"contact",
|
|
212
|
-
"segment",
|
|
213
|
-
"problem",
|
|
214
|
-
"fit",
|
|
215
|
-
"urgency",
|
|
216
|
-
"price",
|
|
217
|
-
"open",
|
|
218
|
-
"extras",
|
|
219
|
-
"done",
|
|
220
|
-
]);
|
|
221
|
-
const extras = steps.find((s) => s.key === "extras");
|
|
222
|
-
expect(extras?.type).toBe("field");
|
|
223
|
-
if (extras?.type !== "field") throw new Error("extras step must be a field step");
|
|
224
|
-
expect(extras.fields.map((f) => f.name)).toEqual(["extra_1"]);
|
|
225
|
-
expect(extras.fields[0]?.options?.map((o) => o.value)).toEqual(["x1:0of2", "x1:1of2"]);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("renders a scored question under the founder's OWN values, in order (#5036 D4)", async () => {
|
|
229
|
-
await generateListingFlow(listing());
|
|
230
|
-
|
|
231
|
-
const problem = upsertedFlow().steps.find((s) => s.key === "problem");
|
|
232
|
-
if (problem?.type !== "field") throw new Error("problem step must be a field step");
|
|
233
|
-
expect(problem.fields[0]?.options).toEqual([
|
|
234
|
-
{ value: "none", label: { en: "Not a problem" } },
|
|
235
|
-
{ value: "mild", label: { en: "Mildly annoying" } },
|
|
236
|
-
{ value: "real", label: { en: "A real drag" } },
|
|
237
|
-
{ value: "blocking", label: { en: "It's blocking me" } },
|
|
238
|
-
]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("keeps Q2's founder values and pins `just-curious` LAST", async () => {
|
|
242
|
-
await generateListingFlow(listing());
|
|
243
|
-
|
|
244
|
-
const segment = upsertedFlow().steps.find((s) => s.key === "segment");
|
|
245
|
-
if (segment?.type !== "field") throw new Error("segment step must be a field step");
|
|
246
|
-
const values = segment.fields[0]?.options?.map((o) => o.value) ?? [];
|
|
247
|
-
expect(values).toEqual(["shipping-now", "planning", "just-curious"]);
|
|
248
|
-
expect(values[values.length - 1]).toBe("just-curious");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("makes Q7 an optional textarea", async () => {
|
|
252
|
-
await generateListingFlow(listing());
|
|
253
|
-
|
|
254
|
-
const open = upsertedFlow().steps.find((s) => s.key === "open");
|
|
255
|
-
if (open?.type !== "field") throw new Error("open step must be a field step");
|
|
256
|
-
expect(open.fields[0]?.inputType).toBe("textarea");
|
|
257
|
-
expect(open.fields[0]?.required).toBeUndefined();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("binds the flow to its experiment so completion scores", async () => {
|
|
261
|
-
await generateListingFlow(listing());
|
|
262
|
-
|
|
263
|
-
expect(upsertedFlow().experiment).toEqual({
|
|
264
|
-
key: "orbit-notes",
|
|
265
|
-
waitlist: "listing_orbit_notes",
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("persists a config that validates against flowConfigSchema", async () => {
|
|
270
|
-
await generateListingFlow(listing());
|
|
271
|
-
|
|
272
|
-
expect(flowConfigSchema.safeParse(upsertedFlow()).success).toBe(true);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe("generateListingFlow — the thank-you (#5001 AC2)", () => {
|
|
277
|
-
it("shows the confirmation line and the founder's own line, never the tier", async () => {
|
|
278
|
-
await generateListingFlow(listing());
|
|
279
|
-
|
|
280
|
-
const flow = upsertedFlow();
|
|
281
|
-
const done = flow.steps[flow.steps.length - 1];
|
|
282
|
-
expect(done?.type).toBe("message");
|
|
283
|
-
expect(done?.copy).toEqual({
|
|
284
|
-
title: { en: "You're on the Orbit Notes waitlist" },
|
|
285
|
-
body: { en: "Meeting notes that file themselves." },
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// No tier vocabulary anywhere on the visitor's path, and no thankYou /
|
|
289
|
-
// deliverables block that could route one in.
|
|
290
|
-
const serialized = JSON.stringify(flow);
|
|
291
|
-
for (const tier of ["now", "soon", "later", "curious"]) {
|
|
292
|
-
expect(serialized).not.toContain(`"${tier}"`);
|
|
293
|
-
}
|
|
294
|
-
expect(flow.thankYou).toBeUndefined();
|
|
295
|
-
expect(flow.deliverables).toBeUndefined();
|
|
296
|
-
expect(flow.scoring).toBeUndefined();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("omits the body when the founder's line is blank", async () => {
|
|
300
|
-
await generateListingFlow(listing({ oneLiner: " " }));
|
|
301
|
-
|
|
302
|
-
const flow = upsertedFlow();
|
|
303
|
-
const done = flow.steps[flow.steps.length - 1];
|
|
304
|
-
expect(done?.copy).toEqual({ title: { en: "You're on the Orbit Notes waitlist" } });
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe("generateListingFlow — the experiment (#5001 AC1/AC3)", () => {
|
|
309
|
-
it("is a live, qualifying experiment keyed by the listing slug", async () => {
|
|
310
|
-
await generateListingFlow(listing());
|
|
311
|
-
|
|
312
|
-
const config = upsertedExperiment();
|
|
313
|
-
expect(config.key).toBe("orbit-notes");
|
|
314
|
-
expect(config.mode).toBe("qualifying");
|
|
315
|
-
expect(config.status).toBe("live");
|
|
316
|
-
expect(config.waitlists).toEqual([
|
|
317
|
-
{ key: "listing_orbit_notes", flowSlug: "orbit-notes" },
|
|
318
|
-
]);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("carries the register the shared rule builds for THIS listing", async () => {
|
|
322
|
-
await generateListingFlow(listing());
|
|
323
|
-
|
|
324
|
-
// Deep, not identity: `upsertWaitlistExperiment` persists Zod's parsed
|
|
325
|
-
// clone, so the register arrives as a structural copy of what
|
|
326
|
-
// `buildListingDiagnosticScoring` returns for this row's questions.
|
|
327
|
-
expect(upsertedExperiment().scoring).toEqual(
|
|
328
|
-
buildListingDiagnosticScoring(TEMPLATE_DEFAULT),
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("declares each scored question's OWN options in the question set (#5036 D4)", async () => {
|
|
333
|
-
await generateListingFlow(listing());
|
|
334
|
-
|
|
335
|
-
const questionSet = upsertedExperiment().questionSet as {
|
|
336
|
-
questions: { id: string; options?: { value: string }[] }[];
|
|
337
|
-
};
|
|
338
|
-
const q3 = questionSet.questions.find((q) => q.id === "q3");
|
|
339
|
-
expect(q3?.options?.map((o) => o.value)).toEqual(["none", "mild", "real", "blocking"]);
|
|
340
|
-
|
|
341
|
-
// Which is what makes the weights keys legal at the config level — the
|
|
342
|
-
// finite `q{n}:{i}of{k}` value universe the old shared constant forced
|
|
343
|
-
// the question set to declare is gone.
|
|
344
|
-
const scoring = upsertedExperiment().scoring as {
|
|
345
|
-
points: { question: string; weights: Record<string, number> }[];
|
|
346
|
-
};
|
|
347
|
-
expect(scoring.points.find((p) => p.question === "q3")?.weights).toEqual({
|
|
348
|
-
none: 0,
|
|
349
|
-
mild: 1,
|
|
350
|
-
real: 2,
|
|
351
|
-
blocking: 3,
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("persists a config that validates against waitlistExperimentConfigSchema", async () => {
|
|
356
|
-
await generateListingFlow(listing());
|
|
357
|
-
|
|
358
|
-
const parsed = waitlistExperimentConfigSchema.safeParse(upsertedExperiment());
|
|
359
|
-
expect(parsed.success).toBe(true);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("gives two DIFFERENT listings the same scoring RULE (AC3, #5036 D4)", async () => {
|
|
363
|
-
await generateListingFlow(listing());
|
|
364
|
-
const first = upsertedExperiment().scoring as Record<string, unknown>;
|
|
365
|
-
|
|
366
|
-
jest.clearAllMocks();
|
|
367
|
-
(prisma.flowDefinition.upsert as jest.Mock).mockResolvedValue({ id: "flow_2" });
|
|
368
|
-
(prisma.waitlistExperiment.upsert as jest.Mock).mockResolvedValue({ id: "wlexp_2" });
|
|
369
|
-
(prisma.projectListing.update as jest.Mock).mockResolvedValue({ id: "prjl_2" });
|
|
370
|
-
|
|
371
|
-
// A second listing with DIFFERENT wording AND a different Q3 arity.
|
|
372
|
-
const otherQuestions = projectListingQuestionsSchema.parse({
|
|
373
|
-
...TEMPLATE_DEFAULT,
|
|
374
|
-
q3: {
|
|
375
|
-
prompt: "How badly does this hurt?",
|
|
376
|
-
options: [
|
|
377
|
-
{ value: "x", label: "Not at all" },
|
|
378
|
-
{ value: "y", label: "Somewhat" },
|
|
379
|
-
{ value: "z", label: "Badly" },
|
|
380
|
-
],
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
await generateListingFlow(
|
|
384
|
-
listing({ id: "prjl_2", slug: "ledger-lite", name: "Ledger Lite", questions: otherQuestions }),
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
const second = upsertedExperiment().scoring as Record<string, unknown>;
|
|
388
|
-
|
|
389
|
-
// Same shape, same ladder, same 0-12 range — only the founder-authored
|
|
390
|
-
// Q3/Q4 weight KEYS differ, because since #5036 D4 those keys are the
|
|
391
|
-
// founder's own option values rather than positional tokens.
|
|
392
|
-
expect(second.scores).toEqual(first.scores);
|
|
393
|
-
expect(second.tiers).toEqual(first.tiers);
|
|
394
|
-
expect(second.default).toEqual(first.default);
|
|
395
|
-
const names = (s: Record<string, unknown>) =>
|
|
396
|
-
(s.points as { name: string }[]).map((p) => p.name);
|
|
397
|
-
expect(names(second)).toEqual(names(first));
|
|
398
|
-
const weights = (s: Record<string, unknown>, question: string) =>
|
|
399
|
-
(s.points as { question: string; weights: Record<string, number> }[]).find(
|
|
400
|
-
(p) => p.question === question,
|
|
401
|
-
)?.weights;
|
|
402
|
-
expect(weights(second, "q5")).toEqual(weights(first, "q5"));
|
|
403
|
-
expect(weights(second, "q6")).toEqual(weights(first, "q6"));
|
|
404
|
-
expect(weights(second, "q3")).toEqual({ x: 0, y: 2, z: 3 });
|
|
405
|
-
expect(weights(second, "q3")).not.toEqual(weights(first, "q3"));
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
describe("generateListingFlow — idempotency + failure", () => {
|
|
410
|
-
it("re-approving upserts the same keys and rewrites the same experimentKey", async () => {
|
|
411
|
-
await generateListingFlow(listing());
|
|
412
|
-
await generateListingFlow(listing());
|
|
413
|
-
|
|
414
|
-
const flowCalls = (prisma.flowDefinition.upsert as jest.Mock).mock.calls;
|
|
415
|
-
const experimentCalls = (prisma.waitlistExperiment.upsert as jest.Mock).mock.calls;
|
|
416
|
-
expect(flowCalls).toHaveLength(2);
|
|
417
|
-
expect(flowCalls[0]?.[0]).toEqual(flowCalls[1]?.[0]);
|
|
418
|
-
expect(experimentCalls[0]?.[0]).toEqual(experimentCalls[1]?.[0]);
|
|
419
|
-
expect(prisma.projectListing.update).toHaveBeenNthCalledWith(2, {
|
|
420
|
-
where: { id: "prjl_1" },
|
|
421
|
-
data: { experimentKey: "orbit-notes" },
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it("refuses a slug the flow grammar rejects, and writes nothing", async () => {
|
|
426
|
-
// A listing slug may carry a doubled hyphen (#4998's `^[a-z0-9-]{3,48}$`);
|
|
427
|
-
// `flowSlugSchema` may not.
|
|
428
|
-
const result = await generateListingFlow(listing({ slug: "orbit--notes" }));
|
|
429
|
-
|
|
430
|
-
expect(result.ok).toBe(false);
|
|
431
|
-
if (result.ok) throw new Error("expected a rejected slug");
|
|
432
|
-
expect(result.error.kind).toBe("invalid_input");
|
|
433
|
-
expect(prisma.waitlistExperiment.upsert).not.toHaveBeenCalled();
|
|
434
|
-
expect(prisma.projectListing.update).not.toHaveBeenCalled();
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
describe("listingWaitlistKey", () => {
|
|
439
|
-
it("prefixes so a digit-leading slug still yields a legal identifier", () => {
|
|
440
|
-
expect(listingWaitlistKey("3d-printer")).toBe("listing_3d_printer");
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
describe("listingAnswerLabels — the CSV's inverse", () => {
|
|
445
|
-
it("maps every emitted value back to the founder's own wording", () => {
|
|
446
|
-
const labels = listingAnswerLabels(TEMPLATE_DEFAULT);
|
|
447
|
-
|
|
448
|
-
// Q2-Q6 are all plain value->label now: the flow renders the founder's
|
|
449
|
-
// own values, so the inverse is their own option list read backwards.
|
|
450
|
-
expect(labels.q3?.blocking).toBe("It's blocking me");
|
|
451
|
-
expect(labels.q5?.urgent).toBe("Right away");
|
|
452
|
-
expect(labels.q6?.critical).toBe("Whatever it takes");
|
|
453
|
-
expect(labels.q2?.["just-curious"]).toBe("Just curious");
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it("keeps the positional encoding for the unscored extras", () => {
|
|
457
|
-
const questions = projectListingQuestionsSchema.parse({
|
|
458
|
-
...TEMPLATE_DEFAULT,
|
|
459
|
-
extras: [
|
|
460
|
-
{
|
|
461
|
-
prompt: "Which stack?",
|
|
462
|
-
options: [
|
|
463
|
-
{ value: "a", label: "TypeScript" },
|
|
464
|
-
{ value: "b", label: "Python" },
|
|
465
|
-
],
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
expect(listingAnswerLabels(questions).extra_1).toEqual({
|
|
471
|
-
"x1:0of2": "TypeScript",
|
|
472
|
-
"x1:1of2": "Python",
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
});
|