create-ailk 0.22.0 → 0.24.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/README.md +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the
|
|
2
|
+
* Tests for the capture_lead MCP tool handler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The tool takes the body `POST /v1/leads/submit` takes and returns the
|
|
5
|
+
* route's 201 body (#7171 AC-11 — the tool adopted the API). Verified here:
|
|
5
6
|
* 1. Malformed input → { ok: false, error: { kind: 'invalid_input', issues } }, api-client never called
|
|
6
|
-
* 2. Happy path →
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 2. Happy path → posts exactly the parsed body to apiClient.leads.submit —
|
|
8
|
+
* no `siteId`, whatever AILK_SITE_ID says, and no stamped `source` — and
|
|
9
|
+
* returns the route's body unchanged, `email` included
|
|
10
|
+
* 3. API 400 / 503 / 429 → { ok: false, error: { kind: 'api_error', status } }
|
|
11
|
+
* 4. #7397 — the spoke marker `payload.site` from AILK_LEAD_SITE, spread last
|
|
12
|
+
* 5. #8073 — forwards the trusted caller address + AILK_VISITOR_FORWARD_KEY
|
|
11
13
|
* as the two visitor-forwarding headers only when both are present
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
LEAD_SITE_MARKER_KEY,
|
|
18
|
+
leadSubmitBodySchema,
|
|
19
|
+
} from "@working-theory/validation";
|
|
15
20
|
import { ailkCaptureLead } from "../src/tools/capture_lead.js";
|
|
16
21
|
import { trustedCallerAddress } from "../src/request-context.js";
|
|
17
22
|
|
|
@@ -30,222 +35,141 @@ jest.mock("@working-theory/api-client", () => {
|
|
|
30
35
|
};
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
const { apiClient } = require("@working-theory/api-client") as {
|
|
35
40
|
apiClient: { leads: { submit: jest.Mock } };
|
|
36
41
|
};
|
|
37
42
|
|
|
43
|
+
/** The route body, as an agent sends it. */
|
|
38
44
|
const VALID_INPUT = {
|
|
39
|
-
|
|
45
|
+
source: "mcp",
|
|
40
46
|
email: "jane@example.com",
|
|
41
|
-
message: "I am interested in your services.",
|
|
47
|
+
payload: { name: "Jane Smith", message: "I am interested in your services." },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The route's 201 body, as the api-client hands it back. */
|
|
51
|
+
const WIRE = {
|
|
52
|
+
id: "lead_00000000-0000-7000-8000-0000000000d1",
|
|
53
|
+
persisted: "db",
|
|
54
|
+
email: { ok: true, messageId: "msg-1" },
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
beforeEach(() => {
|
|
45
58
|
jest.clearAllMocks();
|
|
46
59
|
// The exact-body assertions below assume no spoke marker (#7397).
|
|
47
60
|
delete process.env.AILK_LEAD_SITE;
|
|
61
|
+
delete process.env.AILK_SITE_ID;
|
|
48
62
|
});
|
|
49
63
|
|
|
50
64
|
describe("ailkCaptureLead — input validation", () => {
|
|
51
|
-
it("rejects malformed input (
|
|
65
|
+
it("rejects malformed input (no source) without calling api-client", async () => {
|
|
52
66
|
const result = await ailkCaptureLead({ email: "not-an-email" } as never);
|
|
53
67
|
|
|
54
|
-
expect(result
|
|
55
|
-
if (!result.ok) {
|
|
56
|
-
expect(result.error.
|
|
57
|
-
if (result.error.kind === "invalid_input") {
|
|
58
|
-
expect(Array.isArray(result.error.issues)).toBe(true);
|
|
59
|
-
expect(result.error.issues.length).toBeGreaterThan(0);
|
|
60
|
-
}
|
|
68
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "invalid_input" } });
|
|
69
|
+
if (!result.ok && result.error.kind === "invalid_input") {
|
|
70
|
+
expect(result.error.issues.length).toBeGreaterThan(0);
|
|
61
71
|
}
|
|
62
72
|
expect(apiClient.leads.submit).not.toHaveBeenCalled();
|
|
63
73
|
});
|
|
64
|
-
});
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ok: true,
|
|
70
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d1", persisted: "db" },
|
|
71
|
-
});
|
|
75
|
+
it("admits the StepFlow envelope — email inside payload, none at the top level — as the route does", async () => {
|
|
76
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
77
|
+
const envelope = { source: "step-flow", payload: { email: "env@example.com" } };
|
|
72
78
|
|
|
73
|
-
|
|
79
|
+
await ailkCaptureLead(envelope);
|
|
74
80
|
|
|
75
|
-
expect(apiClient.leads.submit).toHaveBeenCalledWith(
|
|
76
|
-
siteId: "default",
|
|
77
|
-
source: "mcp",
|
|
78
|
-
email: "jane@example.com",
|
|
79
|
-
payload: { name: "Jane Smith", message: "I am interested in your services." },
|
|
80
|
-
});
|
|
81
|
-
expect(result.ok).toBe(true);
|
|
82
|
-
if (result.ok) {
|
|
83
|
-
expect(result.id).toBe("lead_00000000-0000-7000-8000-0000000000d1");
|
|
84
|
-
expect(result.persisted).toBe("db");
|
|
85
|
-
}
|
|
81
|
+
expect(apiClient.leads.submit).toHaveBeenCalledWith(envelope);
|
|
86
82
|
});
|
|
87
83
|
});
|
|
88
84
|
|
|
89
|
-
describe("ailkCaptureLead —
|
|
90
|
-
it("
|
|
91
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
92
|
-
ok: false,
|
|
93
|
-
status: 400,
|
|
94
|
-
body: { error: "bad_request" },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const result = await ailkCaptureLead(VALID_INPUT);
|
|
98
|
-
|
|
99
|
-
expect(result.ok).toBe(false);
|
|
100
|
-
if (!result.ok) {
|
|
101
|
-
expect(result.error.kind).toBe("api_error");
|
|
102
|
-
if (result.error.kind === "api_error") {
|
|
103
|
-
expect(result.error.status).toBe(400);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("maps API 503 to { ok: false, error: { kind: 'api_error', status: 503 } }", async () => {
|
|
109
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
110
|
-
ok: false,
|
|
111
|
-
status: 503,
|
|
112
|
-
body: { error: "service_unavailable" },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const result = await ailkCaptureLead(VALID_INPUT);
|
|
116
|
-
|
|
117
|
-
expect(result.ok).toBe(false);
|
|
118
|
-
if (!result.ok) {
|
|
119
|
-
expect(result.error.kind).toBe("api_error");
|
|
120
|
-
if (result.error.kind === "api_error") {
|
|
121
|
-
expect(result.error.status).toBe(503);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("maps API 429 to { ok: false, error: { kind: 'api_error', status: 429 } }", async () => {
|
|
127
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
128
|
-
ok: false,
|
|
129
|
-
status: 429,
|
|
130
|
-
body: { error: "rate_limited" },
|
|
131
|
-
});
|
|
85
|
+
describe("ailkCaptureLead — happy path", () => {
|
|
86
|
+
it("posts exactly the parsed body and returns the route's body unchanged, email included", async () => {
|
|
87
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
132
88
|
|
|
133
89
|
const result = await ailkCaptureLead(VALID_INPUT);
|
|
134
90
|
|
|
135
|
-
expect(
|
|
136
|
-
|
|
137
|
-
expect(result.error.kind).toBe("api_error");
|
|
138
|
-
if (result.error.kind === "api_error") {
|
|
139
|
-
expect(result.error.status).toBe(429);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
91
|
+
expect(apiClient.leads.submit).toHaveBeenCalledWith(VALID_INPUT);
|
|
92
|
+
expect(result).toEqual(WIRE);
|
|
142
93
|
});
|
|
143
|
-
});
|
|
144
94
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
149
|
-
ok: true,
|
|
150
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d2", persisted: "db" },
|
|
151
|
-
});
|
|
95
|
+
it("never sends a siteId, whatever AILK_SITE_ID says, and strips one the agent sent", async () => {
|
|
96
|
+
process.env.AILK_SITE_ID = "site-abc";
|
|
97
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
152
98
|
|
|
153
|
-
await ailkCaptureLead(VALID_INPUT);
|
|
99
|
+
await ailkCaptureLead({ ...VALID_INPUT, siteId: "site-xyz" } as never);
|
|
154
100
|
|
|
155
|
-
|
|
156
|
-
expect(call.siteId).toBe("default");
|
|
101
|
+
expect(apiClient.leads.submit.mock.calls[0][0]).toEqual(VALID_INPUT);
|
|
157
102
|
});
|
|
158
103
|
});
|
|
159
104
|
|
|
160
|
-
describe("ailkCaptureLead —
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(call.siteId).toBe("site-abc");
|
|
176
|
-
});
|
|
105
|
+
describe("ailkCaptureLead — API error mapping", () => {
|
|
106
|
+
it.each([400, 503, 429])(
|
|
107
|
+
"maps API %i to { ok: false, error: { kind: 'api_error', status } }",
|
|
108
|
+
async (status) => {
|
|
109
|
+
apiClient.leads.submit.mockResolvedValueOnce({
|
|
110
|
+
ok: false,
|
|
111
|
+
status,
|
|
112
|
+
body: { error: "refused" },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await ailkCaptureLead(VALID_INPUT);
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({ ok: false, error: { kind: "api_error", status } });
|
|
118
|
+
},
|
|
119
|
+
);
|
|
177
120
|
});
|
|
178
121
|
|
|
179
122
|
// #7397 — a spoke of a hub-and-spokes deployment is marked in the lead
|
|
180
|
-
// PAYLOAD (`payload.site`)
|
|
181
|
-
//
|
|
182
|
-
//
|
|
123
|
+
// PAYLOAD (`payload.site`). The marker comes from the MCP server's own env,
|
|
124
|
+
// so an MCP-origin lead reaches the hub inbox attributed; it is the one key
|
|
125
|
+
// the tool adds to the body the agent sent.
|
|
183
126
|
describe("ailkCaptureLead — payload site marker (#7397)", () => {
|
|
184
|
-
afterEach(() => {
|
|
185
|
-
delete process.env.AILK_LEAD_SITE;
|
|
186
|
-
});
|
|
187
|
-
|
|
188
127
|
it("names the marker key 'site', the key the issue's hub-and-spokes design names", () => {
|
|
189
128
|
expect(LEAD_SITE_MARKER_KEY).toBe("site");
|
|
190
129
|
});
|
|
191
130
|
|
|
192
|
-
it("
|
|
131
|
+
it("spreads payload.site from AILK_LEAD_SITE LAST, over an agent's own value", async () => {
|
|
193
132
|
process.env.AILK_LEAD_SITE = "spoke-a.example";
|
|
194
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
195
|
-
ok: true,
|
|
196
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d4", persisted: "db" },
|
|
197
|
-
});
|
|
133
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
198
134
|
|
|
199
|
-
await ailkCaptureLead(
|
|
135
|
+
await ailkCaptureLead({
|
|
136
|
+
...VALID_INPUT,
|
|
137
|
+
payload: { ...VALID_INPUT.payload, site: "agent-said" },
|
|
138
|
+
});
|
|
200
139
|
|
|
201
140
|
expect(apiClient.leads.submit).toHaveBeenCalledWith({
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
email: "jane@example.com",
|
|
205
|
-
payload: {
|
|
206
|
-
name: "Jane Smith",
|
|
207
|
-
message: "I am interested in your services.",
|
|
208
|
-
site: "spoke-a.example",
|
|
209
|
-
},
|
|
141
|
+
...VALID_INPUT,
|
|
142
|
+
payload: { ...VALID_INPUT.payload, site: "spoke-a.example" },
|
|
210
143
|
});
|
|
211
144
|
});
|
|
212
145
|
|
|
213
|
-
it("
|
|
146
|
+
it("marks a body the agent sent without a payload", async () => {
|
|
214
147
|
process.env.AILK_LEAD_SITE = "spoke-a.example";
|
|
215
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
216
|
-
ok: true,
|
|
217
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d5", persisted: "db" },
|
|
218
|
-
});
|
|
148
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
219
149
|
|
|
220
|
-
await ailkCaptureLead(
|
|
150
|
+
await ailkCaptureLead({ source: "mcp", email: "jane@example.com" });
|
|
221
151
|
|
|
222
152
|
const body = apiClient.leads.submit.mock.calls[0][0] as Record<string, unknown>;
|
|
223
|
-
expect(
|
|
153
|
+
expect(body["payload"]).toEqual({ site: "spoke-a.example" });
|
|
154
|
+
expect(leadSubmitBodySchema.safeParse(body).success).toBe(true);
|
|
224
155
|
});
|
|
225
156
|
|
|
226
157
|
it.each([
|
|
227
158
|
["unset", undefined],
|
|
228
159
|
["empty", ""],
|
|
229
160
|
["whitespace only", " "],
|
|
230
|
-
])("
|
|
161
|
+
])("sends the payload as it came when AILK_LEAD_SITE is %s", async (_label, value) => {
|
|
231
162
|
if (value !== undefined) process.env.AILK_LEAD_SITE = value;
|
|
232
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
233
|
-
ok: true,
|
|
234
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d6", persisted: "db" },
|
|
235
|
-
});
|
|
163
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
236
164
|
|
|
237
165
|
await ailkCaptureLead(VALID_INPUT);
|
|
238
166
|
|
|
239
|
-
|
|
240
|
-
expect(call.payload).not.toHaveProperty("site");
|
|
167
|
+
expect(apiClient.leads.submit.mock.calls[0][0]).toEqual(VALID_INPUT);
|
|
241
168
|
});
|
|
242
169
|
|
|
243
170
|
it("trims surrounding whitespace from AILK_LEAD_SITE", async () => {
|
|
244
171
|
process.env.AILK_LEAD_SITE = " spoke-a.example\n";
|
|
245
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
246
|
-
ok: true,
|
|
247
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000d7", persisted: "db" },
|
|
248
|
-
});
|
|
172
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
249
173
|
|
|
250
174
|
await ailkCaptureLead(VALID_INPUT);
|
|
251
175
|
|
|
@@ -260,10 +184,7 @@ describe("ailkCaptureLead — visitor-forwarding headers (#8073)", () => {
|
|
|
260
184
|
});
|
|
261
185
|
|
|
262
186
|
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
263
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
264
|
-
ok: true,
|
|
265
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000e1", persisted: "db" },
|
|
266
|
-
});
|
|
187
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
267
188
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
268
189
|
|
|
269
190
|
await ailkCaptureLead(VALID_INPUT);
|
|
@@ -272,10 +193,7 @@ describe("ailkCaptureLead — visitor-forwarding headers (#8073)", () => {
|
|
|
272
193
|
});
|
|
273
194
|
|
|
274
195
|
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
275
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
276
|
-
ok: true,
|
|
277
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000e2", persisted: "db" },
|
|
278
|
-
});
|
|
196
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
279
197
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
280
198
|
|
|
281
199
|
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
@@ -292,10 +210,7 @@ describe("ailkCaptureLead — visitor-forwarding headers (#8073)", () => {
|
|
|
292
210
|
});
|
|
293
211
|
|
|
294
212
|
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
295
|
-
apiClient.leads.submit.mockResolvedValueOnce({
|
|
296
|
-
ok: true,
|
|
297
|
-
body: { id: "lead_00000000-0000-7000-8000-0000000000e3", persisted: "db" },
|
|
298
|
-
});
|
|
213
|
+
apiClient.leads.submit.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
299
214
|
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
300
215
|
|
|
301
216
|
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the subscribe_newsletter MCP tool handler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The tool takes the body `POST /v1/leads/subscribe` takes — `{ email,
|
|
5
|
+
* source }` — and returns the route's 201 body (#7172 AC-11 — the tool
|
|
6
|
+
* adopted the API). Verified here:
|
|
5
7
|
* 1. Malformed input → { ok: false, error: { kind: 'invalid_input', issues } }, api-client never called
|
|
6
|
-
* 2. Happy path →
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* 2. Happy path → posts exactly the parsed body to apiClient.leads.subscribe —
|
|
9
|
+
* no `siteId`, whatever AILK_SITE_ID says, no stamped `source`, no
|
|
10
|
+
* `firstName` — and returns the route's body unchanged, `email` included
|
|
11
|
+
* 3. API 400 / 503 / 429 → { ok: false, error: { kind: 'api_error', status } }
|
|
12
|
+
* 4. #8073 — forwards the trusted caller address + AILK_VISITOR_FORWARD_KEY
|
|
11
13
|
* as the two visitor-forwarding headers only when both are present
|
|
12
14
|
*/
|
|
13
15
|
|
|
@@ -29,145 +31,81 @@ jest.mock("@working-theory/api-client", () => {
|
|
|
29
31
|
};
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
|
|
33
35
|
const { apiClient } = require("@working-theory/api-client") as {
|
|
34
36
|
apiClient: { leads: { subscribe: jest.Mock } };
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
/** The route body, as an agent sends it. */
|
|
40
|
+
const VALID_INPUT = { email: "jane@example.com", source: "mcp" };
|
|
41
|
+
|
|
42
|
+
/** The route's 201 body, as the api-client hands it back. */
|
|
43
|
+
const WIRE = {
|
|
44
|
+
id: "subscr_00000000-0000-7000-8000-0000000000d1",
|
|
45
|
+
persisted: "db",
|
|
46
|
+
email: { skipped: true, reason: "no sender configured" },
|
|
39
47
|
};
|
|
40
48
|
|
|
41
49
|
beforeEach(() => {
|
|
42
50
|
jest.clearAllMocks();
|
|
51
|
+
delete process.env.AILK_SITE_ID;
|
|
43
52
|
});
|
|
44
53
|
|
|
45
54
|
describe("ailkSubscribeNewsletter — input validation", () => {
|
|
46
|
-
it(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
it.each([
|
|
56
|
+
["an invalid email", { email: "not-an-email", source: "mcp" }],
|
|
57
|
+
["no source", { email: "jane@example.com" }],
|
|
58
|
+
["a source with a control character", { email: "jane@example.com", source: "a\nb" }],
|
|
59
|
+
])("rejects %s without calling api-client", async (_label, input) => {
|
|
60
|
+
const result = await ailkSubscribeNewsletter(input as never);
|
|
61
|
+
|
|
62
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "invalid_input" } });
|
|
63
|
+
if (!result.ok && result.error.kind === "invalid_input") {
|
|
64
|
+
expect(result.error.issues.length).toBeGreaterThan(0);
|
|
56
65
|
}
|
|
57
66
|
expect(apiClient.leads.subscribe).not.toHaveBeenCalled();
|
|
58
67
|
});
|
|
59
68
|
});
|
|
60
69
|
|
|
61
70
|
describe("ailkSubscribeNewsletter — happy path", () => {
|
|
62
|
-
it("
|
|
63
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
64
|
-
ok: true,
|
|
65
|
-
body: { id: "subscr_00000000-0000-7000-8000-0000000000d1", persisted: "db" },
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const result = await ailkSubscribeNewsletter(VALID_INPUT);
|
|
69
|
-
|
|
70
|
-
expect(apiClient.leads.subscribe).toHaveBeenCalledWith({
|
|
71
|
-
siteId: "default",
|
|
72
|
-
email: "jane@example.com",
|
|
73
|
-
source: "mcp",
|
|
74
|
-
});
|
|
75
|
-
expect(result.ok).toBe(true);
|
|
76
|
-
if (result.ok) {
|
|
77
|
-
expect(result.id).toBe("subscr_00000000-0000-7000-8000-0000000000d1");
|
|
78
|
-
expect(result.persisted).toBe("db");
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("ailkSubscribeNewsletter — API error mapping", () => {
|
|
84
|
-
it("maps API 400 to { ok: false, error: { kind: 'api_error', status: 400 } }", async () => {
|
|
85
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
86
|
-
ok: false,
|
|
87
|
-
status: 400,
|
|
88
|
-
body: { error: "bad_request" },
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const result = await ailkSubscribeNewsletter(VALID_INPUT);
|
|
92
|
-
|
|
93
|
-
expect(result.ok).toBe(false);
|
|
94
|
-
if (!result.ok) {
|
|
95
|
-
expect(result.error.kind).toBe("api_error");
|
|
96
|
-
if (result.error.kind === "api_error") {
|
|
97
|
-
expect(result.error.status).toBe(400);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("maps API 503 to { ok: false, error: { kind: 'api_error', status: 503 } }", async () => {
|
|
103
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
104
|
-
ok: false,
|
|
105
|
-
status: 503,
|
|
106
|
-
body: { error: "service_unavailable" },
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const result = await ailkSubscribeNewsletter(VALID_INPUT);
|
|
110
|
-
|
|
111
|
-
expect(result.ok).toBe(false);
|
|
112
|
-
if (!result.ok) {
|
|
113
|
-
expect(result.error.kind).toBe("api_error");
|
|
114
|
-
if (result.error.kind === "api_error") {
|
|
115
|
-
expect(result.error.status).toBe(503);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("maps API 429 to { ok: false, error: { kind: 'api_error', status: 429 } }", async () => {
|
|
121
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
122
|
-
ok: false,
|
|
123
|
-
status: 429,
|
|
124
|
-
body: { error: "rate_limited" },
|
|
125
|
-
});
|
|
71
|
+
it("posts exactly the parsed body and returns the route's body unchanged, email included", async () => {
|
|
72
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
126
73
|
|
|
127
74
|
const result = await ailkSubscribeNewsletter(VALID_INPUT);
|
|
128
75
|
|
|
129
|
-
expect(
|
|
130
|
-
|
|
131
|
-
expect(result.error.kind).toBe("api_error");
|
|
132
|
-
if (result.error.kind === "api_error") {
|
|
133
|
-
expect(result.error.status).toBe(429);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
76
|
+
expect(apiClient.leads.subscribe).toHaveBeenCalledWith(VALID_INPUT);
|
|
77
|
+
expect(result).toEqual(WIRE);
|
|
136
78
|
});
|
|
137
|
-
});
|
|
138
79
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
143
|
-
ok: true,
|
|
144
|
-
body: { id: "subscr_00000000-0000-7000-8000-0000000000d2", persisted: "db" },
|
|
145
|
-
});
|
|
80
|
+
it("never sends a siteId or a firstName, whatever AILK_SITE_ID says or the agent sent", async () => {
|
|
81
|
+
process.env.AILK_SITE_ID = "site-abc";
|
|
82
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
146
83
|
|
|
147
|
-
await ailkSubscribeNewsletter(
|
|
84
|
+
await ailkSubscribeNewsletter({
|
|
85
|
+
...VALID_INPUT,
|
|
86
|
+
siteId: "site-xyz",
|
|
87
|
+
firstName: "Jane",
|
|
88
|
+
} as never);
|
|
148
89
|
|
|
149
|
-
|
|
150
|
-
expect(call.siteId).toBe("default");
|
|
90
|
+
expect(apiClient.leads.subscribe.mock.calls[0][0]).toEqual(VALID_INPUT);
|
|
151
91
|
});
|
|
152
92
|
});
|
|
153
93
|
|
|
154
|
-
describe("ailkSubscribeNewsletter —
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
expect(call.siteId).toBe("site-abc");
|
|
170
|
-
});
|
|
94
|
+
describe("ailkSubscribeNewsletter — API error mapping", () => {
|
|
95
|
+
it.each([400, 503, 429])(
|
|
96
|
+
"maps API %i to { ok: false, error: { kind: 'api_error', status } }",
|
|
97
|
+
async (status) => {
|
|
98
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
99
|
+
ok: false,
|
|
100
|
+
status,
|
|
101
|
+
body: { error: "refused" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await ailkSubscribeNewsletter(VALID_INPUT);
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({ ok: false, error: { kind: "api_error", status } });
|
|
107
|
+
},
|
|
108
|
+
);
|
|
171
109
|
});
|
|
172
110
|
|
|
173
111
|
describe("ailkSubscribeNewsletter — visitor-forwarding headers (#8073)", () => {
|
|
@@ -176,10 +114,7 @@ describe("ailkSubscribeNewsletter — visitor-forwarding headers (#8073)", () =>
|
|
|
176
114
|
});
|
|
177
115
|
|
|
178
116
|
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
179
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
180
|
-
ok: true,
|
|
181
|
-
body: { id: "subscr_00000000-0000-7000-8000-0000000000e1", persisted: "db" },
|
|
182
|
-
});
|
|
117
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
183
118
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
184
119
|
|
|
185
120
|
await ailkSubscribeNewsletter(VALID_INPUT);
|
|
@@ -188,10 +123,7 @@ describe("ailkSubscribeNewsletter — visitor-forwarding headers (#8073)", () =>
|
|
|
188
123
|
});
|
|
189
124
|
|
|
190
125
|
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
191
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
192
|
-
ok: true,
|
|
193
|
-
body: { id: "subscr_00000000-0000-7000-8000-0000000000e2", persisted: "db" },
|
|
194
|
-
});
|
|
126
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
195
127
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
196
128
|
|
|
197
129
|
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
@@ -208,10 +140,7 @@ describe("ailkSubscribeNewsletter — visitor-forwarding headers (#8073)", () =>
|
|
|
208
140
|
});
|
|
209
141
|
|
|
210
142
|
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
211
|
-
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
212
|
-
ok: true,
|
|
213
|
-
body: { id: "subscr_00000000-0000-7000-8000-0000000000e3", persisted: "db" },
|
|
214
|
-
});
|
|
143
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
215
144
|
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
216
145
|
|
|
217
146
|
await trustedCallerAddress.run("203.0.113.9", () =>
|