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
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Tests for the read_content MCP tool handler.
|
|
3
3
|
*
|
|
4
4
|
* Acceptance criteria verified here:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
* - ailkReadContent returns
|
|
9
|
-
*
|
|
5
|
+
* - contentPageParamsSchema validates correct inputs (type + slug)
|
|
6
|
+
* - contentPageParamsSchema rejects unknown type values and malformed slugs,
|
|
7
|
+
* a leading or trailing hyphen included (the route's grammar)
|
|
8
|
+
* - ailkReadContent returns the route's document — the page's JSON-LD at the
|
|
9
|
+
* root, `page`, and `body` only with `include: "body"` — for a known slug
|
|
10
|
+
* - ailkReadContent returns the not-found body (not throws) for an unknown slug
|
|
11
|
+
* - Output validates against contentPageResultSchema
|
|
10
12
|
* - No console.* calls in handler (structural test)
|
|
13
|
+
*
|
|
14
|
+
* #7169 (the operator's decision of 2026-10-19): the tool adopted its route's
|
|
15
|
+
* arguments (`include`) and result (the document, not
|
|
16
|
+
* `{ slug, type, frontmatter, body } | null`), so the cases below read
|
|
17
|
+
* `page`/`body` and expect the not-found body where they expected `null`.
|
|
11
18
|
* - #6030 AC-1: BlogPosting and DocsDetail — the two types this issue admits —
|
|
12
19
|
* are readable through the adapter on both the MDX leg and the database
|
|
13
20
|
* leg, and both are asserted against the shared adapter seam
|
|
@@ -26,9 +33,11 @@ import {
|
|
|
26
33
|
|
|
27
34
|
import {
|
|
28
35
|
ailkReadContent,
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
contentPageParamsSchema,
|
|
37
|
+
contentPageResultSchema,
|
|
38
|
+
type ContentPageResult,
|
|
31
39
|
} from "../src/tools/read_content.js";
|
|
40
|
+
import { notFoundError } from "../src/lib/errors.js";
|
|
32
41
|
import {
|
|
33
42
|
__setMcpPublicWorkspaceForTest,
|
|
34
43
|
resetMcpPublicWorkspace,
|
|
@@ -38,6 +47,19 @@ const REPO_ROOT = resolve(__dirname, "../../..");
|
|
|
38
47
|
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
39
48
|
const WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
40
49
|
|
|
50
|
+
/** The body a missing page answers — the route's 404 message, the MCP code. */
|
|
51
|
+
const NOT_FOUND = notFoundError("Page not found");
|
|
52
|
+
|
|
53
|
+
/** Narrows a tool answer to the document, failing on the not-found body. */
|
|
54
|
+
function document(
|
|
55
|
+
result: Awaited<ReturnType<typeof ailkReadContent>>,
|
|
56
|
+
): ContentPageResult {
|
|
57
|
+
if (!("page" in result)) {
|
|
58
|
+
throw new Error(`read_content answered ${JSON.stringify(result)}`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
/**
|
|
42
64
|
* A structural `ArtifactPrismaClient` stub (#5876 spec D11) — `artifact` and
|
|
43
65
|
* `artifactVersion` jest.fn()s, injected directly, no database.
|
|
@@ -86,9 +108,9 @@ function makeArtifactPrismaClientStub(overrides: {
|
|
|
86
108
|
};
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
describe("
|
|
111
|
+
describe("contentPageParamsSchema", () => {
|
|
90
112
|
it("accepts a valid type + slug", () => {
|
|
91
|
-
const result =
|
|
113
|
+
const result = contentPageParamsSchema.safeParse({
|
|
92
114
|
type: "HomePage",
|
|
93
115
|
slug: "home",
|
|
94
116
|
});
|
|
@@ -96,7 +118,7 @@ describe("readContentInputSchema", () => {
|
|
|
96
118
|
});
|
|
97
119
|
|
|
98
120
|
it("accepts a HomePage slug", () => {
|
|
99
|
-
const result =
|
|
121
|
+
const result = contentPageParamsSchema.safeParse({
|
|
100
122
|
type: "HomePage",
|
|
101
123
|
slug: "home",
|
|
102
124
|
});
|
|
@@ -104,19 +126,19 @@ describe("readContentInputSchema", () => {
|
|
|
104
126
|
});
|
|
105
127
|
|
|
106
128
|
it("rejects missing slug", () => {
|
|
107
|
-
const result =
|
|
129
|
+
const result = contentPageParamsSchema.safeParse({ type: "HomePage" });
|
|
108
130
|
expect(result.success).toBe(false);
|
|
109
131
|
});
|
|
110
132
|
|
|
111
133
|
it("rejects missing type", () => {
|
|
112
|
-
const result =
|
|
134
|
+
const result = contentPageParamsSchema.safeParse({
|
|
113
135
|
slug: "launching-ailk",
|
|
114
136
|
});
|
|
115
137
|
expect(result.success).toBe(false);
|
|
116
138
|
});
|
|
117
139
|
|
|
118
140
|
it("rejects an unknown type", () => {
|
|
119
|
-
const result =
|
|
141
|
+
const result = contentPageParamsSchema.safeParse({
|
|
120
142
|
type: "NotAType",
|
|
121
143
|
slug: "some-slug",
|
|
122
144
|
});
|
|
@@ -124,7 +146,7 @@ describe("readContentInputSchema", () => {
|
|
|
124
146
|
});
|
|
125
147
|
|
|
126
148
|
it("rejects empty slug", () => {
|
|
127
|
-
const result =
|
|
149
|
+
const result = contentPageParamsSchema.safeParse({
|
|
128
150
|
type: "HomePage",
|
|
129
151
|
slug: "",
|
|
130
152
|
});
|
|
@@ -132,7 +154,7 @@ describe("readContentInputSchema", () => {
|
|
|
132
154
|
});
|
|
133
155
|
|
|
134
156
|
it("rejects slug with path traversal characters", () => {
|
|
135
|
-
const result =
|
|
157
|
+
const result = contentPageParamsSchema.safeParse({
|
|
136
158
|
type: "HomePage",
|
|
137
159
|
slug: "../etc/passwd",
|
|
138
160
|
});
|
|
@@ -140,68 +162,84 @@ describe("readContentInputSchema", () => {
|
|
|
140
162
|
});
|
|
141
163
|
|
|
142
164
|
it("rejects slug with forward slashes", () => {
|
|
143
|
-
const result =
|
|
165
|
+
const result = contentPageParamsSchema.safeParse({
|
|
144
166
|
type: "HomePage",
|
|
145
167
|
slug: "some/path",
|
|
146
168
|
});
|
|
147
169
|
expect(result.success).toBe(false);
|
|
148
170
|
});
|
|
171
|
+
|
|
172
|
+
// The route's grammar (`SLUG_PATTERN`): no leading or trailing hyphen —
|
|
173
|
+
// refused here too since #7169 (the tool adopted the API's arguments).
|
|
174
|
+
it.each(["-a", "a-", "-"])("rejects slug %p (leading or trailing hyphen)", (slug) => {
|
|
175
|
+
const result = contentPageParamsSchema.safeParse({ type: "HomePage", slug });
|
|
176
|
+
expect(result.success).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("accepts an interior hyphen", () => {
|
|
180
|
+
const result = contentPageParamsSchema.safeParse({
|
|
181
|
+
type: "HomePage",
|
|
182
|
+
slug: "a-b",
|
|
183
|
+
});
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
});
|
|
149
186
|
});
|
|
150
187
|
|
|
151
188
|
describe("ailkReadContent handler", () => {
|
|
152
|
-
it("returns
|
|
153
|
-
const result =
|
|
154
|
-
{ type: "HomePage", slug: "home" },
|
|
155
|
-
CONTENT_ROOT,
|
|
189
|
+
it("returns the HomePage document — the @graph envelope and page, no body without include", async () => {
|
|
190
|
+
const result = document(
|
|
191
|
+
await ailkReadContent({ type: "HomePage", slug: "home" }, CONTENT_ROOT),
|
|
156
192
|
);
|
|
157
193
|
|
|
158
|
-
expect(result).
|
|
159
|
-
expect(result
|
|
160
|
-
expect(result
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
194
|
+
expect(result["@context"]).toBe("https://schema.org");
|
|
195
|
+
expect(Array.isArray(result["@graph"])).toBe(true);
|
|
196
|
+
expect(result.page.slug).toBe("home");
|
|
197
|
+
expect(result.page.type).toBe("HomePage");
|
|
198
|
+
expect(result).not.toHaveProperty("body");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("attaches the raw MDX body only with include: body (#5890)", async () => {
|
|
202
|
+
const result = document(
|
|
203
|
+
await ailkReadContent(
|
|
204
|
+
{ type: "HomePage", slug: "home", include: "body" },
|
|
205
|
+
CONTENT_ROOT,
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
expect(result.page.slug).toBe("home");
|
|
209
|
+
expect(typeof result.body).toBe("string");
|
|
163
210
|
});
|
|
164
211
|
|
|
165
|
-
it("returns
|
|
212
|
+
it("returns the not-found body (not throws) for an unknown slug", async () => {
|
|
166
213
|
const result = await ailkReadContent(
|
|
167
214
|
{ type: "HomePage", slug: "does-not-exist" },
|
|
168
215
|
CONTENT_ROOT,
|
|
169
216
|
);
|
|
170
|
-
expect(result).
|
|
217
|
+
expect(result).toEqual(NOT_FOUND);
|
|
171
218
|
});
|
|
172
219
|
|
|
173
|
-
it("returns
|
|
220
|
+
it("returns the not-found body for a slug that exists in a different type", async () => {
|
|
174
221
|
// home.mdx exists under HomePage, not AboutPage
|
|
175
222
|
const result = await ailkReadContent(
|
|
176
223
|
{ type: "AboutPage", slug: "home" },
|
|
177
224
|
CONTENT_ROOT,
|
|
178
225
|
);
|
|
179
|
-
expect(result).
|
|
226
|
+
expect(result).toEqual(NOT_FOUND);
|
|
180
227
|
});
|
|
181
228
|
|
|
182
|
-
it("returns
|
|
229
|
+
it("returns the not-found body for a valid type with no files when given an unknown slug", async () => {
|
|
183
230
|
const result = await ailkReadContent(
|
|
184
231
|
{ type: "FAQPage", slug: "nonexistent" },
|
|
185
232
|
CONTENT_ROOT,
|
|
186
233
|
);
|
|
187
|
-
expect(result).
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("output shape validates against readContentOutputSchema when non-null", async () => {
|
|
191
|
-
const result = await ailkReadContent(
|
|
192
|
-
{ type: "HomePage", slug: "home" },
|
|
193
|
-
CONTENT_ROOT,
|
|
194
|
-
);
|
|
195
|
-
const parsed = readContentOutputSchema.safeParse(result);
|
|
196
|
-
expect(parsed.success).toBe(true);
|
|
234
|
+
expect(result).toEqual(NOT_FOUND);
|
|
197
235
|
});
|
|
198
236
|
|
|
199
|
-
it("
|
|
237
|
+
it("output shape validates against contentPageResultSchema", async () => {
|
|
200
238
|
const result = await ailkReadContent(
|
|
201
|
-
{ type: "HomePage", slug: "
|
|
239
|
+
{ type: "HomePage", slug: "home", include: "body" },
|
|
202
240
|
CONTENT_ROOT,
|
|
203
241
|
);
|
|
204
|
-
const parsed =
|
|
242
|
+
const parsed = contentPageResultSchema.safeParse(result);
|
|
205
243
|
expect(parsed.success).toBe(true);
|
|
206
244
|
});
|
|
207
245
|
});
|
|
@@ -225,7 +263,7 @@ describe("ailkReadContent — CONTENT_ADAPTER=database (#5876)", () => {
|
|
|
225
263
|
resetResolvedContentAdapter();
|
|
226
264
|
});
|
|
227
265
|
|
|
228
|
-
it("AC-1: returns
|
|
266
|
+
it("AC-1: returns the document with page and body for a published page", async () => {
|
|
229
267
|
const stub = makeArtifactPrismaClientStub({
|
|
230
268
|
artifact: { publishedVersionId: "version_1" },
|
|
231
269
|
version: {
|
|
@@ -244,12 +282,13 @@ describe("ailkReadContent — CONTENT_ADAPTER=database (#5876)", () => {
|
|
|
244
282
|
prisma: stub,
|
|
245
283
|
});
|
|
246
284
|
|
|
247
|
-
const result =
|
|
285
|
+
const result = document(
|
|
286
|
+
await ailkReadContent({ type: "HomePage", slug: "home", include: "body" }),
|
|
287
|
+
);
|
|
248
288
|
|
|
249
|
-
expect(result).
|
|
250
|
-
expect(result
|
|
251
|
-
expect(result
|
|
252
|
-
expect(result!.body).toBe("# Hello");
|
|
289
|
+
expect(result.page.slug).toBe("home");
|
|
290
|
+
expect(result.page.type).toBe("HomePage");
|
|
291
|
+
expect(result.body).toBe("# Hello");
|
|
253
292
|
// The reach test (§5.2 AC-1) — the read is bound to the resolved workspace.
|
|
254
293
|
expect(stub.artifact.findFirst).toHaveBeenCalledWith(
|
|
255
294
|
expect.objectContaining({
|
|
@@ -258,7 +297,7 @@ describe("ailkReadContent — CONTENT_ADAPTER=database (#5876)", () => {
|
|
|
258
297
|
);
|
|
259
298
|
});
|
|
260
299
|
|
|
261
|
-
it("AC-1: returns
|
|
300
|
+
it("AC-1: returns the not-found body for a draft-only artifact (no published version)", async () => {
|
|
262
301
|
const stub = makeArtifactPrismaClientStub({
|
|
263
302
|
artifact: { publishedVersionId: null },
|
|
264
303
|
});
|
|
@@ -270,10 +309,10 @@ describe("ailkReadContent — CONTENT_ADAPTER=database (#5876)", () => {
|
|
|
270
309
|
|
|
271
310
|
const result = await ailkReadContent({ type: "HomePage", slug: "home" });
|
|
272
311
|
|
|
273
|
-
expect(result).
|
|
312
|
+
expect(result).toEqual(NOT_FOUND);
|
|
274
313
|
});
|
|
275
314
|
|
|
276
|
-
it("AC-1: returns
|
|
315
|
+
it("AC-1: returns the not-found body for an unknown slug", async () => {
|
|
277
316
|
const stub = makeArtifactPrismaClientStub({ artifact: null });
|
|
278
317
|
__setMcpPublicWorkspaceForTest({
|
|
279
318
|
kind: "resolved",
|
|
@@ -286,16 +325,16 @@ describe("ailkReadContent — CONTENT_ADAPTER=database (#5876)", () => {
|
|
|
286
325
|
slug: "does-not-exist",
|
|
287
326
|
});
|
|
288
327
|
|
|
289
|
-
expect(result).
|
|
328
|
+
expect(result).toEqual(NOT_FOUND);
|
|
290
329
|
});
|
|
291
330
|
|
|
292
|
-
it("AC-2: returns
|
|
331
|
+
it("AC-2: returns the not-found body when the workspace is unresolved, with no content lookup", async () => {
|
|
293
332
|
const stub = makeArtifactPrismaClientStub({});
|
|
294
333
|
__setMcpPublicWorkspaceForTest({ kind: "unresolved", reason: "ambiguous" });
|
|
295
334
|
|
|
296
335
|
const result = await ailkReadContent({ type: "HomePage", slug: "home" });
|
|
297
336
|
|
|
298
|
-
expect(result).
|
|
337
|
+
expect(result).toEqual(NOT_FOUND);
|
|
299
338
|
expect(stub.artifact.findFirst).not.toHaveBeenCalled();
|
|
300
339
|
});
|
|
301
340
|
});
|
|
@@ -334,10 +373,12 @@ describe("ailkReadContent — BlogPosting and DocsDetail, MDX leg (#6030 AC-1)",
|
|
|
334
373
|
});
|
|
335
374
|
|
|
336
375
|
it.each(["BlogPosting", "DocsDetail"] as const)(
|
|
337
|
-
"returns the page for %s,
|
|
376
|
+
"returns the page for %s, with page and body equal to what getPageSource returns (D3)",
|
|
338
377
|
async (type) => {
|
|
339
378
|
const slug = type === "BlogPosting" ? "launch-post" : "getting-started";
|
|
340
|
-
const result =
|
|
379
|
+
const result = document(
|
|
380
|
+
await ailkReadContent({ type, slug, locale: "en", include: "body" }, root),
|
|
381
|
+
);
|
|
341
382
|
|
|
342
383
|
// The shared seam D3 asserts from both ends: read_content's payload is
|
|
343
384
|
// derived from exactly what ContentAdapter.getPageSource(type, slug,
|
|
@@ -345,27 +386,22 @@ describe("ailkReadContent — BlogPosting and DocsDetail, MDX leg (#6030 AC-1)",
|
|
|
345
386
|
const adapter = createMdxAdapter({ contentRoot: root });
|
|
346
387
|
const source = await adapter.getPageSource(type, slug, "en");
|
|
347
388
|
expect(source).not.toBeNull();
|
|
348
|
-
const expected = readContentOutputSchema.parse({
|
|
349
|
-
slug: source!.frontmatter.slug ?? slug,
|
|
350
|
-
type: source!.frontmatter.type ?? type,
|
|
351
|
-
frontmatter: source!.frontmatter,
|
|
352
|
-
body: source!.body,
|
|
353
|
-
});
|
|
354
389
|
|
|
355
|
-
expect(result).toEqual(
|
|
356
|
-
expect(result
|
|
357
|
-
expect(result
|
|
390
|
+
expect(result.page).toEqual(source!.frontmatter);
|
|
391
|
+
expect(result.body).toBe(source!.body);
|
|
392
|
+
expect(result.page.slug).toBe(slug);
|
|
393
|
+
expect(result.page.type).toBe(type);
|
|
358
394
|
},
|
|
359
395
|
);
|
|
360
396
|
|
|
361
397
|
it.each(["BlogPosting", "DocsDetail"] as const)(
|
|
362
|
-
"returns
|
|
398
|
+
"returns the not-found body for an absent slug of %s",
|
|
363
399
|
async (type) => {
|
|
364
400
|
const result = await ailkReadContent(
|
|
365
401
|
{ type, slug: "does-not-exist", locale: "en" },
|
|
366
402
|
root,
|
|
367
403
|
);
|
|
368
|
-
expect(result).
|
|
404
|
+
expect(result).toEqual(NOT_FOUND);
|
|
369
405
|
},
|
|
370
406
|
);
|
|
371
407
|
});
|
|
@@ -386,7 +422,7 @@ describe("ailkReadContent — BlogPosting and DocsDetail, CONTENT_ADAPTER=databa
|
|
|
386
422
|
});
|
|
387
423
|
|
|
388
424
|
it.each(["BlogPosting", "DocsDetail"] as const)(
|
|
389
|
-
"returns
|
|
425
|
+
"returns the document with page and body for a published %s",
|
|
390
426
|
async (type) => {
|
|
391
427
|
const slug = "probe";
|
|
392
428
|
const stub = makeArtifactPrismaClientStub({
|
|
@@ -407,19 +443,20 @@ describe("ailkReadContent — BlogPosting and DocsDetail, CONTENT_ADAPTER=databa
|
|
|
407
443
|
prisma: stub,
|
|
408
444
|
});
|
|
409
445
|
|
|
410
|
-
const result =
|
|
446
|
+
const result = document(
|
|
447
|
+
await ailkReadContent({ type, slug, include: "body" }),
|
|
448
|
+
);
|
|
411
449
|
|
|
412
|
-
expect(result).
|
|
413
|
-
expect(result
|
|
414
|
-
expect(result
|
|
415
|
-
expect(result!.body).toBe("# Published body");
|
|
450
|
+
expect(result.page.slug).toBe(slug);
|
|
451
|
+
expect(result.page.type).toBe(type);
|
|
452
|
+
expect(result.body).toBe("# Published body");
|
|
416
453
|
},
|
|
417
454
|
);
|
|
418
455
|
|
|
419
456
|
// D4 — the publication guard must hold for every newly-admitted type, not
|
|
420
457
|
// only HomePage's existing case.
|
|
421
458
|
it.each(["BlogPosting", "DocsDetail"] as const)(
|
|
422
|
-
"returns
|
|
459
|
+
"returns the not-found body for a draft-only %s artifact (no published version)",
|
|
423
460
|
async (type) => {
|
|
424
461
|
const stub = makeArtifactPrismaClientStub({
|
|
425
462
|
artifact: { publishedVersionId: null },
|
|
@@ -432,13 +469,13 @@ describe("ailkReadContent — BlogPosting and DocsDetail, CONTENT_ADAPTER=databa
|
|
|
432
469
|
|
|
433
470
|
const result = await ailkReadContent({ type, slug: "probe" });
|
|
434
471
|
|
|
435
|
-
expect(result).
|
|
472
|
+
expect(result).toEqual(NOT_FOUND);
|
|
436
473
|
},
|
|
437
474
|
);
|
|
438
475
|
|
|
439
476
|
// D4 — type-as-address: a page stored under a different frontmatter.type
|
|
440
477
|
// than requested reads as absent, never as a cross-type hit.
|
|
441
|
-
it("returns
|
|
478
|
+
it("returns the not-found body when the requested type does not match the stored frontmatter.type", async () => {
|
|
442
479
|
const stub = makeArtifactPrismaClientStub({
|
|
443
480
|
artifact: { publishedVersionId: "version_1" },
|
|
444
481
|
version: {
|
|
@@ -459,7 +496,7 @@ describe("ailkReadContent — BlogPosting and DocsDetail, CONTENT_ADAPTER=databa
|
|
|
459
496
|
|
|
460
497
|
const result = await ailkReadContent({ type: "DocsDetail", slug: "probe" });
|
|
461
498
|
|
|
462
|
-
expect(result).
|
|
499
|
+
expect(result).toEqual(NOT_FOUND);
|
|
463
500
|
});
|
|
464
501
|
});
|
|
465
502
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the request_callback MCP tool handler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The tool takes the body `POST /v1/leads/callback` takes — `{ name, phone,
|
|
5
|
+
* preferredTime?, payload? }` — and returns the route's 201 body (#7173
|
|
6
|
+
* AC-11 — the tool 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.callback —
|
|
9
|
+
* no `siteId`, whatever AILK_SITE_ID says, no stamped `payload: {}` — and
|
|
10
|
+
* 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,147 +31,89 @@ 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: { callback: jest.Mock } };
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
/** The route body, as an agent sends it — the two required fields alone. */
|
|
40
|
+
const VALID_INPUT = { name: "Jane Smith", phone: "+15555550100" };
|
|
41
|
+
|
|
42
|
+
/** The route's 201 body, as the api-client hands it back. */
|
|
43
|
+
const WIRE = {
|
|
44
|
+
id: "fallback-1700000000000-cb1",
|
|
45
|
+
persisted: "file",
|
|
46
|
+
email: { ok: false, error: "provider refused" },
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
beforeEach(() => {
|
|
43
50
|
jest.clearAllMocks();
|
|
51
|
+
delete process.env.AILK_SITE_ID;
|
|
44
52
|
});
|
|
45
53
|
|
|
46
54
|
describe("ailkRequestCallback — input validation", () => {
|
|
47
|
-
it(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
55
|
+
it.each([
|
|
56
|
+
["missing required fields", {}],
|
|
57
|
+
["a phone under the route's bound", { name: "Jane", phone: "12" }],
|
|
58
|
+
])("rejects %s without calling api-client", async (_label, input) => {
|
|
59
|
+
const result = await ailkRequestCallback(input as never);
|
|
60
|
+
|
|
61
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "invalid_input" } });
|
|
62
|
+
if (!result.ok && result.error.kind === "invalid_input") {
|
|
63
|
+
expect(result.error.issues.length).toBeGreaterThan(0);
|
|
57
64
|
}
|
|
58
65
|
expect(apiClient.leads.callback).not.toHaveBeenCalled();
|
|
59
66
|
});
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
describe("ailkRequestCallback — happy path", () => {
|
|
63
|
-
it("
|
|
64
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
65
|
-
ok: true,
|
|
66
|
-
body: { id: "fallback-1700000000000-cb1", persisted: "file" },
|
|
67
|
-
});
|
|
70
|
+
it("posts exactly the parsed body — no stamped payload — and returns the route's body unchanged, email included", async () => {
|
|
71
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
68
72
|
|
|
69
73
|
const result = await ailkRequestCallback(VALID_INPUT);
|
|
70
74
|
|
|
71
|
-
expect(apiClient.leads.callback).toHaveBeenCalledWith(
|
|
72
|
-
|
|
73
|
-
name: "Jane Smith",
|
|
74
|
-
phone: "+15555550100",
|
|
75
|
-
payload: {},
|
|
76
|
-
});
|
|
77
|
-
expect(result.ok).toBe(true);
|
|
78
|
-
if (result.ok) {
|
|
79
|
-
expect(result.id).toBe("fallback-1700000000000-cb1");
|
|
80
|
-
expect(result.persisted).toBe("file");
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("ailkRequestCallback — API error mapping", () => {
|
|
86
|
-
it("maps API 400 to { ok: false, error: { kind: 'api_error', status: 400 } }", async () => {
|
|
87
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
88
|
-
ok: false,
|
|
89
|
-
status: 400,
|
|
90
|
-
body: { error: "bad_request" },
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const result = await ailkRequestCallback(VALID_INPUT);
|
|
94
|
-
|
|
95
|
-
expect(result.ok).toBe(false);
|
|
96
|
-
if (!result.ok) {
|
|
97
|
-
expect(result.error.kind).toBe("api_error");
|
|
98
|
-
if (result.error.kind === "api_error") {
|
|
99
|
-
expect(result.error.status).toBe(400);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("maps API 503 to { ok: false, error: { kind: 'api_error', status: 503 } }", async () => {
|
|
105
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
106
|
-
ok: false,
|
|
107
|
-
status: 503,
|
|
108
|
-
body: { error: "service_unavailable" },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const result = await ailkRequestCallback(VALID_INPUT);
|
|
112
|
-
|
|
113
|
-
expect(result.ok).toBe(false);
|
|
114
|
-
if (!result.ok) {
|
|
115
|
-
expect(result.error.kind).toBe("api_error");
|
|
116
|
-
if (result.error.kind === "api_error") {
|
|
117
|
-
expect(result.error.status).toBe(503);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
75
|
+
expect(apiClient.leads.callback).toHaveBeenCalledWith(VALID_INPUT);
|
|
76
|
+
expect(result).toEqual(WIRE);
|
|
120
77
|
});
|
|
121
78
|
|
|
122
|
-
it("
|
|
123
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
79
|
+
it("passes preferredTime and payload through as sent", async () => {
|
|
80
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
81
|
+
const full = {
|
|
82
|
+
...VALID_INPUT,
|
|
83
|
+
preferredTime: "Tuesday afternoon",
|
|
84
|
+
payload: { notes: "Roof estimate" },
|
|
85
|
+
};
|
|
128
86
|
|
|
129
|
-
|
|
87
|
+
await ailkRequestCallback(full);
|
|
130
88
|
|
|
131
|
-
expect(
|
|
132
|
-
if (!result.ok) {
|
|
133
|
-
expect(result.error.kind).toBe("api_error");
|
|
134
|
-
if (result.error.kind === "api_error") {
|
|
135
|
-
expect(result.error.status).toBe(429);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
89
|
+
expect(apiClient.leads.callback).toHaveBeenCalledWith(full);
|
|
138
90
|
});
|
|
139
|
-
});
|
|
140
91
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
145
|
-
ok: true,
|
|
146
|
-
body: { id: "fallback-1700000000000-cb2", persisted: "file" },
|
|
147
|
-
});
|
|
92
|
+
it("never sends a siteId, whatever AILK_SITE_ID says, and strips one the agent sent", async () => {
|
|
93
|
+
process.env.AILK_SITE_ID = "site-abc";
|
|
94
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
148
95
|
|
|
149
|
-
await ailkRequestCallback(VALID_INPUT);
|
|
96
|
+
await ailkRequestCallback({ ...VALID_INPUT, siteId: "site-xyz" } as never);
|
|
150
97
|
|
|
151
|
-
|
|
152
|
-
expect(call.siteId).toBe("default");
|
|
98
|
+
expect(apiClient.leads.callback.mock.calls[0][0]).toEqual(VALID_INPUT);
|
|
153
99
|
});
|
|
154
100
|
});
|
|
155
101
|
|
|
156
|
-
describe("ailkRequestCallback —
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(call.siteId).toBe("site-abc");
|
|
172
|
-
});
|
|
102
|
+
describe("ailkRequestCallback — API error mapping", () => {
|
|
103
|
+
it.each([400, 503, 429])(
|
|
104
|
+
"maps API %i to { ok: false, error: { kind: 'api_error', status } }",
|
|
105
|
+
async (status) => {
|
|
106
|
+
apiClient.leads.callback.mockResolvedValueOnce({
|
|
107
|
+
ok: false,
|
|
108
|
+
status,
|
|
109
|
+
body: { error: "refused" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await ailkRequestCallback(VALID_INPUT);
|
|
113
|
+
|
|
114
|
+
expect(result).toEqual({ ok: false, error: { kind: "api_error", status } });
|
|
115
|
+
},
|
|
116
|
+
);
|
|
173
117
|
});
|
|
174
118
|
|
|
175
119
|
describe("ailkRequestCallback — visitor-forwarding headers (#8073)", () => {
|
|
@@ -178,10 +122,7 @@ describe("ailkRequestCallback — visitor-forwarding headers (#8073)", () => {
|
|
|
178
122
|
});
|
|
179
123
|
|
|
180
124
|
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
181
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
182
|
-
ok: true,
|
|
183
|
-
body: { id: "fallback-1700000000000-e1", persisted: "file" },
|
|
184
|
-
});
|
|
125
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
185
126
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
186
127
|
|
|
187
128
|
await ailkRequestCallback(VALID_INPUT);
|
|
@@ -190,10 +131,7 @@ describe("ailkRequestCallback — visitor-forwarding headers (#8073)", () => {
|
|
|
190
131
|
});
|
|
191
132
|
|
|
192
133
|
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
193
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
194
|
-
ok: true,
|
|
195
|
-
body: { id: "fallback-1700000000000-e2", persisted: "file" },
|
|
196
|
-
});
|
|
134
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
197
135
|
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
198
136
|
|
|
199
137
|
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
@@ -210,10 +148,7 @@ describe("ailkRequestCallback — visitor-forwarding headers (#8073)", () => {
|
|
|
210
148
|
});
|
|
211
149
|
|
|
212
150
|
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
213
|
-
apiClient.leads.callback.mockResolvedValueOnce({
|
|
214
|
-
ok: true,
|
|
215
|
-
body: { id: "fallback-1700000000000-e3", persisted: "file" },
|
|
216
|
-
});
|
|
151
|
+
apiClient.leads.callback.mockResolvedValueOnce({ ok: true, body: WIRE });
|
|
217
152
|
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
218
153
|
|
|
219
154
|
await trustedCallerAddress.run("203.0.113.9", () =>
|