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,15 +2,19 @@
|
|
|
2
2
|
* Tests for the search_content MCP tool handler.
|
|
3
3
|
*
|
|
4
4
|
* Acceptance criteria verified here:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* - ailkSearchContent happy path returns
|
|
10
|
-
* - ailkSearchContent returns empty
|
|
5
|
+
* - contentSearchQuerySchema validates correct inputs (q required, type + limit optional)
|
|
6
|
+
* - contentSearchQuerySchema rejects q longer than 200 chars
|
|
7
|
+
* - contentSearchQuerySchema rejects limit > 50
|
|
8
|
+
* - contentSearchQuerySchema rejects empty q
|
|
9
|
+
* - ailkSearchContent happy path returns the route's ItemList with items of the correct shape
|
|
10
|
+
* - ailkSearchContent returns an empty ItemList for a query with no matches
|
|
11
11
|
* - Malformed input causes Zod rejection
|
|
12
|
-
* - #5831:
|
|
12
|
+
* - #5831: contentSearchQuerySchema accepts a registry category slug and
|
|
13
13
|
* rejects one outside the registry; the handler forwards it to the adapter
|
|
14
|
+
*
|
|
15
|
+
* #7167 (the operator's decision of 2026-10-19): the tool adopted its route's
|
|
16
|
+
* arguments (`q`, not `query`) and result (the `ItemList`, not `{ results }`),
|
|
17
|
+
* so the cases below read the new names.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -19,102 +23,108 @@ import { join, resolve } from "node:path";
|
|
|
19
23
|
|
|
20
24
|
import {
|
|
21
25
|
ailkSearchContent,
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
contentSearchQuerySchema,
|
|
27
|
+
contentSearchResultSchema,
|
|
24
28
|
} from "../src/tools/search_content.js";
|
|
25
29
|
|
|
26
30
|
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
27
31
|
const CONTENT_ROOT = `${REPO_ROOT}/content`;
|
|
28
32
|
|
|
29
|
-
describe("
|
|
30
|
-
it("accepts a valid
|
|
31
|
-
const result =
|
|
33
|
+
describe("contentSearchQuerySchema", () => {
|
|
34
|
+
it("accepts a valid q", () => {
|
|
35
|
+
const result = contentSearchQuerySchema.safeParse({ q: "hello" });
|
|
32
36
|
expect(result.success).toBe(true);
|
|
33
37
|
});
|
|
34
38
|
|
|
35
|
-
it("accepts
|
|
36
|
-
const result =
|
|
37
|
-
|
|
39
|
+
it("accepts q with optional type filter", () => {
|
|
40
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
41
|
+
q: "hello",
|
|
38
42
|
type: "HomePage",
|
|
39
43
|
});
|
|
40
44
|
expect(result.success).toBe(true);
|
|
41
45
|
});
|
|
42
46
|
|
|
43
|
-
it("accepts
|
|
44
|
-
const result =
|
|
45
|
-
|
|
47
|
+
it("accepts q with optional limit", () => {
|
|
48
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
49
|
+
q: "hello",
|
|
46
50
|
limit: 10,
|
|
47
51
|
});
|
|
48
52
|
expect(result.success).toBe(true);
|
|
49
53
|
});
|
|
50
54
|
|
|
51
|
-
it("rejects an empty
|
|
52
|
-
const result =
|
|
55
|
+
it("rejects an empty q", () => {
|
|
56
|
+
const result = contentSearchQuerySchema.safeParse({ q: "" });
|
|
53
57
|
expect(result.success).toBe(false);
|
|
54
58
|
});
|
|
55
59
|
|
|
56
|
-
it("rejects a
|
|
57
|
-
const result =
|
|
58
|
-
|
|
60
|
+
it("rejects a q longer than 200 chars", () => {
|
|
61
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
62
|
+
q: "a".repeat(201),
|
|
59
63
|
});
|
|
60
64
|
expect(result.success).toBe(false);
|
|
61
65
|
});
|
|
62
66
|
|
|
63
|
-
it("accepts a
|
|
64
|
-
const result =
|
|
65
|
-
|
|
67
|
+
it("accepts a q exactly 200 chars", () => {
|
|
68
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
69
|
+
q: "a".repeat(200),
|
|
66
70
|
});
|
|
67
71
|
expect(result.success).toBe(true);
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
it("rejects limit of 0", () => {
|
|
71
|
-
const result =
|
|
72
|
-
|
|
75
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
76
|
+
q: "hello",
|
|
73
77
|
limit: 0,
|
|
74
78
|
});
|
|
75
79
|
expect(result.success).toBe(false);
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
it("rejects limit > 50", () => {
|
|
79
|
-
const result =
|
|
80
|
-
|
|
83
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
84
|
+
q: "hello",
|
|
81
85
|
limit: 51,
|
|
82
86
|
});
|
|
83
87
|
expect(result.success).toBe(false);
|
|
84
88
|
});
|
|
85
89
|
|
|
86
90
|
it("accepts limit of 50", () => {
|
|
87
|
-
const result =
|
|
88
|
-
|
|
91
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
92
|
+
q: "hello",
|
|
89
93
|
limit: 50,
|
|
90
94
|
});
|
|
91
95
|
expect(result.success).toBe(true);
|
|
92
96
|
});
|
|
93
97
|
|
|
98
|
+
it("coerces a string limit, as the route's query string parses (#7167 DD1)", () => {
|
|
99
|
+
const result = contentSearchQuerySchema.safeParse({ q: "hello", limit: "5" });
|
|
100
|
+
expect(result.success).toBe(true);
|
|
101
|
+
expect(result.data!.limit).toBe(5);
|
|
102
|
+
});
|
|
103
|
+
|
|
94
104
|
it("rejects an unknown type string", () => {
|
|
95
|
-
const result =
|
|
96
|
-
|
|
105
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
106
|
+
q: "hello",
|
|
97
107
|
type: "NotAType",
|
|
98
108
|
});
|
|
99
109
|
expect(result.success).toBe(false);
|
|
100
110
|
});
|
|
101
111
|
|
|
102
|
-
it("rejects missing
|
|
103
|
-
const result =
|
|
112
|
+
it("rejects missing q (malformed input)", () => {
|
|
113
|
+
const result = contentSearchQuerySchema.safeParse({});
|
|
104
114
|
expect(result.success).toBe(false);
|
|
105
115
|
});
|
|
106
116
|
|
|
107
117
|
it("accepts a category slug from the registry (#5831)", () => {
|
|
108
|
-
const result =
|
|
109
|
-
|
|
118
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
119
|
+
q: "roof",
|
|
110
120
|
category: "roofing",
|
|
111
121
|
});
|
|
112
122
|
expect(result.success).toBe(true);
|
|
113
123
|
});
|
|
114
124
|
|
|
115
125
|
it("rejects a category slug outside the registry with an enum error (#5831)", () => {
|
|
116
|
-
const result =
|
|
117
|
-
|
|
126
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
127
|
+
q: "roof",
|
|
118
128
|
category: "not-a-real-category",
|
|
119
129
|
});
|
|
120
130
|
expect(result.success).toBe(false);
|
|
@@ -125,8 +135,8 @@ describe("searchContentInputSchema", () => {
|
|
|
125
135
|
});
|
|
126
136
|
|
|
127
137
|
it("rejects a GROUP slug — groups are derived, never tagged (#5831)", () => {
|
|
128
|
-
const result =
|
|
129
|
-
|
|
138
|
+
const result = contentSearchQuerySchema.safeParse({
|
|
139
|
+
q: "roof",
|
|
130
140
|
category: "professional-services",
|
|
131
141
|
});
|
|
132
142
|
expect(result.success).toBe(false);
|
|
@@ -182,15 +192,15 @@ describe("ailkSearchContent — category filter (#5831)", () => {
|
|
|
182
192
|
|
|
183
193
|
it("forwards category to the adapter — only the tagged page comes back", async () => {
|
|
184
194
|
const result = await ailkSearchContent(
|
|
185
|
-
{
|
|
195
|
+
{ q: "roof", category: "roofing" },
|
|
186
196
|
root,
|
|
187
197
|
);
|
|
188
|
-
expect(result.
|
|
198
|
+
expect(result.items.map((r) => r.slug)).toEqual(["roof-replacement"]);
|
|
189
199
|
});
|
|
190
200
|
|
|
191
201
|
it("returns every match when no category is passed", async () => {
|
|
192
|
-
const result = await ailkSearchContent({
|
|
193
|
-
expect(result.
|
|
202
|
+
const result = await ailkSearchContent({ q: "roof" }, root);
|
|
203
|
+
expect(result.items.map((r) => r.slug).sort()).toEqual([
|
|
194
204
|
"attic-ventilation",
|
|
195
205
|
"roof-replacement",
|
|
196
206
|
]);
|
|
@@ -198,22 +208,26 @@ describe("ailkSearchContent — category filter (#5831)", () => {
|
|
|
198
208
|
|
|
199
209
|
it("returns [] for a registry slug nothing is tagged with", async () => {
|
|
200
210
|
const result = await ailkSearchContent(
|
|
201
|
-
{
|
|
211
|
+
{ q: "roof", category: "legal" },
|
|
202
212
|
root,
|
|
203
213
|
);
|
|
204
|
-
expect(result.
|
|
214
|
+
expect(result.items).toEqual([]);
|
|
205
215
|
});
|
|
206
216
|
});
|
|
207
217
|
|
|
208
218
|
describe("ailkSearchContent handler", () => {
|
|
209
|
-
it("returns
|
|
210
|
-
const result = await ailkSearchContent({
|
|
211
|
-
expect(
|
|
219
|
+
it("returns the route's ItemList on a known query", async () => {
|
|
220
|
+
const result = await ailkSearchContent({ q: "launch" }, CONTENT_ROOT);
|
|
221
|
+
expect(result["@context"]).toBe("https://schema.org");
|
|
222
|
+
expect(result["@type"]).toBe("ItemList");
|
|
223
|
+
expect(result.query).toBe("launch");
|
|
224
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
225
|
+
expect(result.totalItems).toBe(result.items.length);
|
|
212
226
|
});
|
|
213
227
|
|
|
214
228
|
it("result items have required shape fields", async () => {
|
|
215
|
-
const result = await ailkSearchContent({
|
|
216
|
-
for (const item of result.
|
|
229
|
+
const result = await ailkSearchContent({ q: "launch" }, CONTENT_ROOT);
|
|
230
|
+
for (const item of result.items) {
|
|
217
231
|
expect(typeof item.slug).toBe("string");
|
|
218
232
|
expect(typeof item.type).toBe("string");
|
|
219
233
|
expect(typeof item.title).toBe("string");
|
|
@@ -222,25 +236,26 @@ describe("ailkSearchContent handler", () => {
|
|
|
222
236
|
}
|
|
223
237
|
});
|
|
224
238
|
|
|
225
|
-
it("returns empty
|
|
239
|
+
it("returns an empty ItemList for a query that matches nothing", async () => {
|
|
226
240
|
const result = await ailkSearchContent(
|
|
227
|
-
{
|
|
241
|
+
{ q: "xqzxqzxqzxqz_no_match_ever" },
|
|
228
242
|
CONTENT_ROOT,
|
|
229
243
|
);
|
|
230
|
-
expect(result.
|
|
244
|
+
expect(result.items).toEqual([]);
|
|
245
|
+
expect(result.totalItems).toBe(0);
|
|
231
246
|
});
|
|
232
247
|
|
|
233
248
|
it("respects limit option", async () => {
|
|
234
249
|
const result = await ailkSearchContent(
|
|
235
|
-
{
|
|
250
|
+
{ q: "a", limit: 1 },
|
|
236
251
|
CONTENT_ROOT,
|
|
237
252
|
);
|
|
238
|
-
expect(result.
|
|
253
|
+
expect(result.items.length).toBeLessThanOrEqual(1);
|
|
239
254
|
});
|
|
240
255
|
|
|
241
|
-
it("output validates against
|
|
242
|
-
const result = await ailkSearchContent({
|
|
243
|
-
const parsed =
|
|
256
|
+
it("output validates against contentSearchResultSchema", async () => {
|
|
257
|
+
const result = await ailkSearchContent({ q: "launch" }, CONTENT_ROOT);
|
|
258
|
+
const parsed = contentSearchResultSchema.safeParse(result);
|
|
244
259
|
expect(parsed.success).toBe(true);
|
|
245
260
|
});
|
|
246
261
|
});
|
|
@@ -231,19 +231,21 @@ describe("create_project on a spoke — the seam's UNSERVED_MCP_TOOLS (#8070 D7,
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
describe("the
|
|
234
|
+
describe("the six licence-feature tools — one-release warning cycle (#7136)", () => {
|
|
235
235
|
const FLOW_PAIR = ["get_flow", "submit_flow_step"];
|
|
236
|
-
const
|
|
236
|
+
const BOOKING_TOOLS = [
|
|
237
237
|
"list_availability",
|
|
238
238
|
"get_event_meta",
|
|
239
|
+
"list_meetings",
|
|
239
240
|
"create_booking",
|
|
240
241
|
];
|
|
241
|
-
const
|
|
242
|
+
const SIX = [...FLOW_PAIR, ...BOOKING_TOOLS];
|
|
242
243
|
const FEATURE_OF: Record<string, string> = {
|
|
243
244
|
get_flow: "flows-deliverables",
|
|
244
245
|
submit_flow_step: "flows-deliverables",
|
|
245
246
|
list_availability: "scheduling",
|
|
246
247
|
get_event_meta: "scheduling",
|
|
248
|
+
list_meetings: "scheduling",
|
|
247
249
|
create_booking: "scheduling",
|
|
248
250
|
};
|
|
249
251
|
|
|
@@ -271,12 +273,12 @@ describe("the five licence-feature tools — one-release warning cycle (#7136)",
|
|
|
271
273
|
return Object.keys((createMcpServer() as any)._registeredTools);
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
it("warn mode, no feature held: all
|
|
276
|
+
it("warn mode, no feature held: all six still register and serve", () => {
|
|
275
277
|
__setMcpHeldFeaturesForTest([]);
|
|
276
278
|
|
|
277
279
|
const listed = listRegisteredTools().map((t) => t.name);
|
|
278
280
|
const registered = registeredKeys();
|
|
279
|
-
for (const name of
|
|
281
|
+
for (const name of SIX) {
|
|
280
282
|
expect(listed).toContain(name);
|
|
281
283
|
expect(registered).toContain(name);
|
|
282
284
|
}
|
|
@@ -288,9 +290,9 @@ describe("the five licence-feature tools — one-release warning cycle (#7136)",
|
|
|
288
290
|
|
|
289
291
|
const names = reportUnlicensedProTools(warn);
|
|
290
292
|
|
|
291
|
-
expect(names.slice().sort()).toEqual(
|
|
292
|
-
expect(warn).toHaveBeenCalledTimes(
|
|
293
|
-
for (const name of
|
|
293
|
+
expect(names.slice().sort()).toEqual(SIX.slice().sort());
|
|
294
|
+
expect(warn).toHaveBeenCalledTimes(6);
|
|
295
|
+
for (const name of SIX) {
|
|
294
296
|
const message = warn.mock.calls
|
|
295
297
|
.map(([m]) => m as string)
|
|
296
298
|
.find((m) => m.includes(`"${name}"`));
|
|
@@ -326,7 +328,7 @@ describe("the five licence-feature tools — one-release warning cycle (#7136)",
|
|
|
326
328
|
expect(warn).toHaveBeenCalledTimes(2);
|
|
327
329
|
});
|
|
328
330
|
|
|
329
|
-
it("refuse mode, no feature held: none of the
|
|
331
|
+
it("refuse mode, no feature held: none of the six registers, and nothing is warned", () => {
|
|
330
332
|
__setMcpFeatureGateForTest("refuse");
|
|
331
333
|
__setMcpHeldFeaturesForTest([]);
|
|
332
334
|
const warn = jest.fn();
|
|
@@ -335,7 +337,7 @@ describe("the five licence-feature tools — one-release warning cycle (#7136)",
|
|
|
335
337
|
expect(warn).not.toHaveBeenCalled();
|
|
336
338
|
const listed = listRegisteredTools().map((t) => t.name);
|
|
337
339
|
const registered = registeredKeys();
|
|
338
|
-
for (const name of
|
|
340
|
+
for (const name of SIX) {
|
|
339
341
|
expect(listed).not.toContain(name);
|
|
340
342
|
expect(registered).not.toContain(name);
|
|
341
343
|
}
|
|
@@ -346,7 +348,7 @@ describe("the five licence-feature tools — one-release warning cycle (#7136)",
|
|
|
346
348
|
__setMcpHeldFeaturesForTest(["scheduling"]);
|
|
347
349
|
|
|
348
350
|
const registered = registeredKeys();
|
|
349
|
-
for (const name of
|
|
351
|
+
for (const name of BOOKING_TOOLS) expect(registered).toContain(name);
|
|
350
352
|
for (const name of FLOW_PAIR) expect(registered).not.toContain(name);
|
|
351
353
|
});
|
|
352
354
|
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { checkout } from "@working-theory/api-client";
|
|
35
|
+
import { getMcpToolCatalog } from "@working-theory/metadata";
|
|
36
|
+
import { purchaseMcpInputSchema } from "@working-theory/validation";
|
|
35
37
|
|
|
36
38
|
import {
|
|
37
39
|
__setMcpCommerceClaimForTest,
|
|
@@ -72,13 +74,13 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
|
|
|
72
74
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
73
75
|
|
|
74
76
|
await ailkPurchase({
|
|
75
|
-
productCode: "
|
|
77
|
+
productCode: "growth_monthly",
|
|
76
78
|
quantity: 2,
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
expect(createSession).toHaveBeenCalledTimes(1);
|
|
80
82
|
expect(createSession).toHaveBeenCalledWith({
|
|
81
|
-
productCode: "
|
|
83
|
+
productCode: "growth_monthly",
|
|
82
84
|
quantity: 2,
|
|
83
85
|
successUrl:
|
|
84
86
|
"https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
|
|
@@ -89,7 +91,7 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
|
|
|
89
91
|
it("omits quantity entirely when it is not supplied", async () => {
|
|
90
92
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
91
93
|
|
|
92
|
-
await ailkPurchase({ productCode: "
|
|
94
|
+
await ailkPurchase({ productCode: "growth_monthly" });
|
|
93
95
|
|
|
94
96
|
const payload = createSession.mock.calls[0]![0];
|
|
95
97
|
expect(payload).not.toHaveProperty("quantity");
|
|
@@ -99,10 +101,10 @@ describe("start_purchase — parity with the payload POST /v1/checkout/sessions
|
|
|
99
101
|
it("derives both redirect URLs from AILK_BASE_URL — always, not only when omitted (#6001)", async () => {
|
|
100
102
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
101
103
|
|
|
102
|
-
await ailkPurchase({ productCode: "
|
|
104
|
+
await ailkPurchase({ productCode: "growth_monthly" });
|
|
103
105
|
|
|
104
106
|
expect(createSession).toHaveBeenCalledWith({
|
|
105
|
-
productCode: "
|
|
107
|
+
productCode: "growth_monthly",
|
|
106
108
|
successUrl:
|
|
107
109
|
"https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
|
|
108
110
|
cancelUrl: "https://example.com/purchase/cancel",
|
|
@@ -114,7 +116,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
|
|
|
114
116
|
it("returns { url } deep-equal to the seam's url, and nothing else", async () => {
|
|
115
117
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
116
118
|
|
|
117
|
-
const result = await ailkPurchase({ productCode: "
|
|
119
|
+
const result = await ailkPurchase({ productCode: "growth_monthly" });
|
|
118
120
|
|
|
119
121
|
expect(result).toEqual({ url: HOSTED_URL });
|
|
120
122
|
});
|
|
@@ -122,7 +124,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
|
|
|
122
124
|
it("does NOT surface the route's sessionId (D5 — it identifies a buyer's session)", async () => {
|
|
123
125
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
124
126
|
|
|
125
|
-
const result = await ailkPurchase({ productCode: "
|
|
127
|
+
const result = await ailkPurchase({ productCode: "growth_monthly" });
|
|
126
128
|
|
|
127
129
|
expect(result).not.toHaveProperty("sessionId");
|
|
128
130
|
});
|
|
@@ -130,7 +132,7 @@ describe("start_purchase — FR-1: the hosted Checkout URL", () => {
|
|
|
130
132
|
it("the URL it returns is Stripe-hosted — nothing is charged in-tool", async () => {
|
|
131
133
|
createSession.mockResolvedValueOnce(sessionCreated());
|
|
132
134
|
|
|
133
|
-
const result = await ailkPurchase({ productCode: "
|
|
135
|
+
const result = await ailkPurchase({ productCode: "growth_monthly" });
|
|
134
136
|
|
|
135
137
|
expect((result as { url: string }).url).toMatch(
|
|
136
138
|
/^https:\/\/checkout\.stripe\.com\//,
|
|
@@ -146,7 +148,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
|
|
|
146
148
|
body: { error: "unknown-product-code" },
|
|
147
149
|
});
|
|
148
150
|
|
|
149
|
-
const result = await ailkPurchase({ productCode: "no-such-
|
|
151
|
+
const result = await ailkPurchase({ productCode: "no-such-offering_monthly" });
|
|
150
152
|
|
|
151
153
|
// The HTTP twin states and tests "Error responses never echo user input";
|
|
152
154
|
// parity means the pair answers alike (#5971 security review, Low).
|
|
@@ -160,7 +162,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
|
|
|
160
162
|
body: { error: "unknown-product-code" },
|
|
161
163
|
});
|
|
162
164
|
|
|
163
|
-
const result = await ailkPurchase({ productCode: "no-such-
|
|
165
|
+
const result = await ailkPurchase({ productCode: "no-such-offering_monthly" });
|
|
164
166
|
|
|
165
167
|
expect(result).toMatchObject({ error: "not_found" });
|
|
166
168
|
});
|
|
@@ -172,7 +174,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
|
|
|
172
174
|
body: { error: "unknown-product-code" },
|
|
173
175
|
});
|
|
174
176
|
|
|
175
|
-
await ailkPurchase({ productCode: "no-such-
|
|
177
|
+
await ailkPurchase({ productCode: "no-such-offering_monthly" });
|
|
176
178
|
|
|
177
179
|
expect(createSession).toHaveBeenCalledTimes(1);
|
|
178
180
|
});
|
|
@@ -184,7 +186,7 @@ describe("start_purchase — FR-2: an unknown offering", () => {
|
|
|
184
186
|
body: { error: "checkout-unavailable" },
|
|
185
187
|
});
|
|
186
188
|
|
|
187
|
-
const result = await ailkPurchase({ productCode: "
|
|
189
|
+
const result = await ailkPurchase({ productCode: "growth_monthly" });
|
|
188
190
|
|
|
189
191
|
expect(result).toMatchObject({ error: "validation_failed" });
|
|
190
192
|
});
|
|
@@ -250,3 +252,197 @@ describe("start_purchase — the redirect fix (#6001): a caller-supplied redirec
|
|
|
250
252
|
expect(createSession).not.toHaveBeenCalled();
|
|
251
253
|
});
|
|
252
254
|
});
|
|
255
|
+
|
|
256
|
+
// ── #8935 — the full Offerings lookup_key ───────────────────────────────────
|
|
257
|
+
//
|
|
258
|
+
// The tool takes a price by its full lookup_key (contract §3.7) and refuses an
|
|
259
|
+
// ungrammatical one before any HTTP call, with the same not-found body every
|
|
260
|
+
// route 404 maps to (A3, A5). The route decides the workspace and whether the
|
|
261
|
+
// key is shown and org-enabled (A1, #8931); the mocked seam below answers with
|
|
262
|
+
// the route's Offerings-path `201` body, `reply.status(201).send({ sessionId,
|
|
263
|
+
// url })`. Spec: `docs/specs/8935-start-purchase-sells-a-named-spec.md`.
|
|
264
|
+
|
|
265
|
+
const OFFERINGS_URL = "https://checkout.stripe.com/c/pay/cs_test_offerings";
|
|
266
|
+
|
|
267
|
+
function offeringsSessionCreated() {
|
|
268
|
+
return {
|
|
269
|
+
ok: true as const,
|
|
270
|
+
body: { sessionId: "cs_test_offerings", url: OFFERINGS_URL },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const NOT_FOUND = {
|
|
275
|
+
error: "not_found",
|
|
276
|
+
message: "No offering matches that product code.",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
describe("start_purchase — a shown, org-enabled lookup_key gets the route's hosted URL (#8935 AC-1)", () => {
|
|
280
|
+
const keys = [
|
|
281
|
+
"growth_monthly",
|
|
282
|
+
"growth_yearly",
|
|
283
|
+
"basic_once",
|
|
284
|
+
"samhenry-coaching_monthly",
|
|
285
|
+
`${"a".repeat(40)}_monthly`,
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
it.each(keys)(
|
|
289
|
+
"%s reaches the route byte for byte and returns { url } only",
|
|
290
|
+
async (key) => {
|
|
291
|
+
createSession.mockResolvedValueOnce(offeringsSessionCreated());
|
|
292
|
+
|
|
293
|
+
const result = await ailkPurchase({ productCode: key });
|
|
294
|
+
|
|
295
|
+
expect(result).toEqual({ url: OFFERINGS_URL });
|
|
296
|
+
expect(result).not.toHaveProperty("sessionId");
|
|
297
|
+
expect(createSession).toHaveBeenCalledTimes(1);
|
|
298
|
+
// One argument, the payload — no init, so no header of the tool's own.
|
|
299
|
+
expect(createSession.mock.calls[0]).toHaveLength(1);
|
|
300
|
+
expect(createSession.mock.calls[0]![0]).toEqual({
|
|
301
|
+
productCode: key,
|
|
302
|
+
successUrl:
|
|
303
|
+
"https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
|
|
304
|
+
cancelUrl: "https://example.com/purchase/cancel",
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
it("the 48-character key is the longest §3.7 allows", () => {
|
|
310
|
+
expect(keys[4]).toHaveLength(48);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("forwards quantity alongside the key", async () => {
|
|
314
|
+
createSession.mockResolvedValueOnce(offeringsSessionCreated());
|
|
315
|
+
|
|
316
|
+
await ailkPurchase({ productCode: "growth_monthly", quantity: 2 });
|
|
317
|
+
|
|
318
|
+
expect(createSession.mock.calls[0]![0]).toEqual({
|
|
319
|
+
productCode: "growth_monthly",
|
|
320
|
+
quantity: 2,
|
|
321
|
+
successUrl:
|
|
322
|
+
"https://example.com/purchase/success?session_id={CHECKOUT_SESSION_ID}",
|
|
323
|
+
cancelUrl: "https://example.com/purchase/cancel",
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("start_purchase — an ungrammatical key is refused before any call, with the route-404 body (#8935 AC-2)", () => {
|
|
329
|
+
const ungrammatical = [
|
|
330
|
+
"pro-plan",
|
|
331
|
+
"Growth_Monthly",
|
|
332
|
+
"growth_pro_monthly",
|
|
333
|
+
"wendi_retainer",
|
|
334
|
+
"growth_quarterly",
|
|
335
|
+
"growth_",
|
|
336
|
+
"_monthly",
|
|
337
|
+
`${"a".repeat(41)}_monthly`,
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
it.each(ungrammatical)(
|
|
341
|
+
"%s passes the shared schema but answers not_found with no call and no echo",
|
|
342
|
+
async (key) => {
|
|
343
|
+
expect(purchaseMcpInputSchema.safeParse({ productCode: key }).success).toBe(
|
|
344
|
+
true,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const result = await ailkPurchase({ productCode: key });
|
|
348
|
+
|
|
349
|
+
expect(result).toEqual(NOT_FOUND);
|
|
350
|
+
expect(createSession).not.toHaveBeenCalled();
|
|
351
|
+
expect(JSON.stringify(result)).not.toContain(key);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
it("the 49-character key is one past the §3.7 bound", () => {
|
|
356
|
+
expect(ungrammatical[7]).toHaveLength(49);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it.each([
|
|
360
|
+
["{ error: unknown-product-code }", { error: "unknown-product-code" }],
|
|
361
|
+
["null", null],
|
|
362
|
+
["{}", {}],
|
|
363
|
+
])(
|
|
364
|
+
"every route 404 maps to the same body, whatever the route's body (%s)",
|
|
365
|
+
async (_label, body) => {
|
|
366
|
+
createSession.mockResolvedValueOnce({
|
|
367
|
+
ok: false as const,
|
|
368
|
+
status: 404,
|
|
369
|
+
body,
|
|
370
|
+
} as never);
|
|
371
|
+
|
|
372
|
+
const result = await ailkPurchase({ productCode: "growth_monthly" });
|
|
373
|
+
|
|
374
|
+
expect(result).toEqual(NOT_FOUND);
|
|
375
|
+
expect(JSON.stringify(result)).not.toContain("growth_monthly");
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
it("the pre-call refusal and the route-404 mapping are one value", async () => {
|
|
380
|
+
const refused = await ailkPurchase({ productCode: "pro-plan" });
|
|
381
|
+
createSession.mockResolvedValueOnce({
|
|
382
|
+
ok: false as const,
|
|
383
|
+
status: 404,
|
|
384
|
+
body: { error: "unknown-product-code" },
|
|
385
|
+
});
|
|
386
|
+
const mapped = await ailkPurchase({ productCode: "growth_monthly" });
|
|
387
|
+
|
|
388
|
+
expect(refused).toEqual(mapped);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("a code the route's regex refuses still answers validation_failed, with no call", async () => {
|
|
392
|
+
const result = await ailkPurchase({ productCode: "has spaces" });
|
|
393
|
+
|
|
394
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
395
|
+
expect(createSession).not.toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("start_purchase — the claim and the strict schema still answer first (#8935 AC-3)", () => {
|
|
400
|
+
it("an unclaimed site answers the module-disabled body for an ungrammatical key, with no call", async () => {
|
|
401
|
+
__setMcpCommerceClaimForTest("unclaimed");
|
|
402
|
+
|
|
403
|
+
const result = await ailkPurchase({ productCode: "Growth_Monthly" });
|
|
404
|
+
|
|
405
|
+
expect(result).toEqual({ error: "not_found", message: "Not found" });
|
|
406
|
+
expect(createSession).not.toHaveBeenCalled();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it.each(["workspaceId", "organizationId", "site", "metadata"])(
|
|
410
|
+
"a direct call carrying %s fails validation, with no call",
|
|
411
|
+
async (field) => {
|
|
412
|
+
const result = await ailkPurchase({
|
|
413
|
+
productCode: "growth_monthly",
|
|
414
|
+
[field]: "attacker-chosen",
|
|
415
|
+
} as never);
|
|
416
|
+
|
|
417
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
418
|
+
expect(createSession).not.toHaveBeenCalled();
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
it("the input schema has exactly productCode and quantity", () => {
|
|
423
|
+
expect(Object.keys(purchaseMcpInputSchema.shape)).toEqual([
|
|
424
|
+
"productCode",
|
|
425
|
+
"quantity",
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("start_purchase — the catalog description names the lookup key (#8935 AC-5)", () => {
|
|
431
|
+
it("tells an agent to pass the lookupKey get_pricing or get_offerings returns", () => {
|
|
432
|
+
const entry = getMcpToolCatalog({ addons: { commerce: true } }).find(
|
|
433
|
+
(tool) => tool.name === "start_purchase",
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
expect(entry?.description).toBe(
|
|
437
|
+
"Start a purchase for an offering on this site. Provide the price's full lookup key as productCode — the lookupKey that get_pricing or get_offerings returns, such as growth_monthly — and an optional quantity; returns a Stripe-hosted Checkout URL for the buyer to complete payment on. Nothing is charged by this tool.",
|
|
438
|
+
);
|
|
439
|
+
for (const phrase of [
|
|
440
|
+
"lookupKey",
|
|
441
|
+
"get_pricing",
|
|
442
|
+
"get_offerings",
|
|
443
|
+
"Nothing is charged by this tool",
|
|
444
|
+
]) {
|
|
445
|
+
expect(entry?.description).toContain(phrase);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|