create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handler tests: GET /blog (catch-all)
|
|
3
|
+
*
|
|
4
|
+
* Validates the empty-state contract: with `content/blog/` empty, the index
|
|
5
|
+
* renders the BLOG_EMPTY_SENTINEL placeholder. Also validates the populated
|
|
6
|
+
* branch and the detail-route branch. The Fumadocs source is injected via
|
|
7
|
+
* the optional `blogSource` prop instead of mocked at the module level,
|
|
8
|
+
* so these tests don't depend on the on-disk content tree or lib/source.
|
|
9
|
+
*/
|
|
10
|
+
import { render, screen } from "@testing-library/react";
|
|
11
|
+
|
|
12
|
+
jest.mock("next/navigation", () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
notFound: jest.fn(() => {
|
|
15
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
|
|
21
|
+
import BlogRoute, {
|
|
22
|
+
BLOG_EMPTY_SENTINEL,
|
|
23
|
+
} from "../../app/[locale]/blog/[[...slug]]/page";
|
|
24
|
+
|
|
25
|
+
/** Minimal blog-source stub matching the BlogSource interface. */
|
|
26
|
+
function makeStubBlogSource(overrides: {
|
|
27
|
+
pages?: Array<{ url: string; data: unknown }>;
|
|
28
|
+
page?: { data: unknown } | undefined;
|
|
29
|
+
}) {
|
|
30
|
+
return {
|
|
31
|
+
getPages: jest.fn().mockReturnValue(overrides.pages ?? []),
|
|
32
|
+
getPage: jest.fn().mockReturnValue(overrides.page),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function renderAsync(
|
|
37
|
+
element: Promise<React.ReactElement>,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const resolved = await element;
|
|
40
|
+
render(resolved);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("apps/web — /blog catch-all route", () => {
|
|
44
|
+
describe("index route (no slug) — empty content/blog/", () => {
|
|
45
|
+
it("renders the empty-state placeholder with the known sentinel", async () => {
|
|
46
|
+
const stubSource = makeStubBlogSource({ pages: [] });
|
|
47
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
48
|
+
const placeholder = screen.getByTestId("blog-empty-placeholder");
|
|
49
|
+
expect(placeholder.getAttribute("data-sentinel")).toBe(
|
|
50
|
+
BLOG_EMPTY_SENTINEL,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders the Blog heading", async () => {
|
|
55
|
+
const stubSource = makeStubBlogSource({ pages: [] });
|
|
56
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
57
|
+
const heading = screen.getByRole("heading", { level: 1 });
|
|
58
|
+
expect(heading).toHaveTextContent("Blog");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("emits a Blog JSON-LD <script> alongside the placeholder", async () => {
|
|
62
|
+
const stubSource = makeStubBlogSource({ pages: [] });
|
|
63
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
64
|
+
const script = screen.getByTestId("json-ld");
|
|
65
|
+
const parsed = JSON.parse(script.innerHTML) as Record<string, unknown>;
|
|
66
|
+
expect(parsed["@context"]).toBe("https://schema.org");
|
|
67
|
+
expect(parsed["@type"]).toBe("Blog");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("index route (no slug) — populated content/blog/", () => {
|
|
72
|
+
const stubPages = [
|
|
73
|
+
{
|
|
74
|
+
url: "/blog/launching",
|
|
75
|
+
data: {
|
|
76
|
+
title: "Launching AILK",
|
|
77
|
+
description: "First post.",
|
|
78
|
+
date: "2026-05-06",
|
|
79
|
+
image: "/img/launching.png",
|
|
80
|
+
author: { name: "The AILK Team" },
|
|
81
|
+
tags: ["Announcements", "AX"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
url: "/blog/second",
|
|
86
|
+
data: {
|
|
87
|
+
title: "Second Post",
|
|
88
|
+
description: "Another post.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
it("renders the BlogList section (no bare placeholder list)", async () => {
|
|
94
|
+
const stubSource = makeStubBlogSource({ pages: stubPages });
|
|
95
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
96
|
+
expect(screen.getByTestId("blog-list")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders one linked title per post with the /blog/ prefix re-applied", async () => {
|
|
100
|
+
const stubSource = makeStubBlogSource({ pages: stubPages });
|
|
101
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
102
|
+
const links = screen.getAllByRole("link");
|
|
103
|
+
expect(links).toHaveLength(2);
|
|
104
|
+
const first = links[0];
|
|
105
|
+
if (!first) throw new Error("expected at least one link");
|
|
106
|
+
expect(first).toHaveTextContent("Launching AILK");
|
|
107
|
+
expect(first.getAttribute("href")).toBe("/blog/launching");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("maps author and tags onto the rendered post preview", async () => {
|
|
111
|
+
const stubSource = makeStubBlogSource({ pages: stubPages });
|
|
112
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
113
|
+
expect(screen.getByText("The AILK Team")).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText("Announcements")).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByText("AX")).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not render the empty-state placeholder", async () => {
|
|
119
|
+
const stubSource = makeStubBlogSource({ pages: stubPages });
|
|
120
|
+
await renderAsync(BlogRoute({ params: Promise.resolve({ locale: "en" }), blogSource: stubSource }));
|
|
121
|
+
expect(screen.queryByTestId("blog-empty-placeholder")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("detail route (slug provided)", () => {
|
|
126
|
+
it("renders the BlogPost section and emits a BlogPosting JSON-LD <script>", async () => {
|
|
127
|
+
const stubSource = makeStubBlogSource({
|
|
128
|
+
page: {
|
|
129
|
+
data: {
|
|
130
|
+
title: "Hello, World",
|
|
131
|
+
description: "First post.",
|
|
132
|
+
date: "2026-05-01",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await renderAsync(
|
|
138
|
+
BlogRoute({ params: Promise.resolve({ locale: "en", slug: ["hello-world"] }), blogSource: stubSource }),
|
|
139
|
+
);
|
|
140
|
+
expect(screen.getByTestId("blog-post")).toBeInTheDocument();
|
|
141
|
+
const heading = screen.getByRole("heading", { level: 1 });
|
|
142
|
+
expect(heading).toHaveTextContent("Hello, World");
|
|
143
|
+
|
|
144
|
+
const script = screen.getByTestId("json-ld");
|
|
145
|
+
const parsed = JSON.parse(script.innerHTML) as Record<string, unknown>;
|
|
146
|
+
expect(parsed["@context"]).toBe("https://schema.org");
|
|
147
|
+
expect(parsed["@type"]).toBe("WebPage");
|
|
148
|
+
const mainEntity = parsed.mainEntity as Record<string, unknown>;
|
|
149
|
+
expect(mainEntity["@type"]).toBe("BlogPosting");
|
|
150
|
+
expect(mainEntity.headline).toBe("Hello, World");
|
|
151
|
+
expect(parsed.datePublished).toBe("2026-05-01");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("renders the compiled MDX body through the section's children slot", async () => {
|
|
155
|
+
const Body = () => <p data-testid="mdx-body">Compiled MDX paragraph.</p>;
|
|
156
|
+
const stubSource = makeStubBlogSource({
|
|
157
|
+
page: {
|
|
158
|
+
data: {
|
|
159
|
+
title: "Hello, World",
|
|
160
|
+
description: "First post.",
|
|
161
|
+
date: "2026-05-01",
|
|
162
|
+
author: { name: "The AILK Team" },
|
|
163
|
+
tags: ["Engineering"],
|
|
164
|
+
body: Body,
|
|
165
|
+
toc: [{ title: "Intro", url: "#intro", depth: 2 }],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await renderAsync(
|
|
171
|
+
BlogRoute({ params: Promise.resolve({ locale: "en", slug: ["hello-world"] }), blogSource: stubSource }),
|
|
172
|
+
);
|
|
173
|
+
expect(screen.getByTestId("mdx-body")).toHaveTextContent(
|
|
174
|
+
"Compiled MDX paragraph.",
|
|
175
|
+
);
|
|
176
|
+
expect(screen.getByText("The AILK Team")).toBeInTheDocument();
|
|
177
|
+
expect(screen.getByText("Engineering")).toBeInTheDocument();
|
|
178
|
+
expect(screen.getByTestId("blog-post-toc")).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("calls notFound() when the slug does not resolve", async () => {
|
|
182
|
+
const stubSource = makeStubBlogSource({ page: undefined });
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
renderAsync(
|
|
186
|
+
BlogRoute({ params: Promise.resolve({ locale: "en", slug: ["missing"] }), blogSource: stubSource }),
|
|
187
|
+
),
|
|
188
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route handler tests: GET /llms.txt
|
|
7
|
+
*
|
|
8
|
+
* Validates: 200 status, text/plain Content-Type, non-empty body listing
|
|
9
|
+
* content seeds. Both the MDX adapter and blog pages are injected via
|
|
10
|
+
* createLlmsHandler rather than mocked at the module level.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createLlmsHandler } from "../../app/llms.txt/route";
|
|
14
|
+
import type { ContentAdapter } from "@working-theory/content-adapters";
|
|
15
|
+
|
|
16
|
+
function makeStubAdapter(): ContentAdapter {
|
|
17
|
+
// eslint-disable-next-line no-restricted-syntax -- jest.fn() stubs are not assignable to ContentAdapter's generic return types; partial stub is intentional, tracked in #1073
|
|
18
|
+
return {
|
|
19
|
+
listPages: jest.fn().mockResolvedValue([
|
|
20
|
+
{
|
|
21
|
+
type: "HomePage",
|
|
22
|
+
slug: "home",
|
|
23
|
+
title: "Home",
|
|
24
|
+
description: "AX-first OSS template for shipping software.",
|
|
25
|
+
},
|
|
26
|
+
]),
|
|
27
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
28
|
+
getPageBySlug: jest.fn().mockResolvedValue(null),
|
|
29
|
+
getDetailBySlug: jest.fn().mockResolvedValue(null),
|
|
30
|
+
searchPages: jest.fn().mockResolvedValue([]),
|
|
31
|
+
extractStructuredData: jest.fn().mockResolvedValue(null),
|
|
32
|
+
createPage: jest.fn(),
|
|
33
|
+
updatePage: jest.fn(),
|
|
34
|
+
deletePage: jest.fn(),
|
|
35
|
+
getPageVersion: jest.fn(),
|
|
36
|
+
} as unknown as ContentAdapter;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stubBlogPages = [
|
|
40
|
+
{
|
|
41
|
+
url: "/blog",
|
|
42
|
+
data: {
|
|
43
|
+
title: "Blog",
|
|
44
|
+
description: "Writing on AEO and AX-first software.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
url: "/blog/launching-ailk",
|
|
49
|
+
data: {
|
|
50
|
+
title: "Launching AI Launch Kit",
|
|
51
|
+
description: "How we built and shipped AILK.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
describe("GET /llms.txt", () => {
|
|
57
|
+
let GET: () => Promise<Response>;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
const stubAdapter = makeStubAdapter();
|
|
61
|
+
GET = createLlmsHandler({
|
|
62
|
+
getContentAdapter: () => stubAdapter,
|
|
63
|
+
getBlogPages: () => stubBlogPages,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
jest.resetAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns 200", async () => {
|
|
72
|
+
const response = await GET();
|
|
73
|
+
expect(response.status).toBe(200);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns text/plain content-type", async () => {
|
|
77
|
+
const response = await GET();
|
|
78
|
+
expect(response.headers.get("content-type")).toMatch(/text\/plain/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("body contains content titles", async () => {
|
|
82
|
+
const response = await GET();
|
|
83
|
+
const text = await response.text();
|
|
84
|
+
expect(text).toContain("Home");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("body is non-empty", async () => {
|
|
88
|
+
const response = await GET();
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
expect(text.trim().length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("body contains blog index entry", async () => {
|
|
94
|
+
const response = await GET();
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
const lines = text.split("\n");
|
|
97
|
+
const titleIdx = lines.findIndex((l) => l.includes("Title: Blog"));
|
|
98
|
+
expect(titleIdx).toBeGreaterThan(-1);
|
|
99
|
+
const urlLine = lines.slice(titleIdx).find((l) => l.startsWith(" URL:"));
|
|
100
|
+
expect(urlLine).toMatch(/\/blog/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("body contains migrated post entry", async () => {
|
|
104
|
+
const response = await GET();
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
const lines = text.split("\n");
|
|
107
|
+
const titleIdx = lines.findIndex((l) =>
|
|
108
|
+
l.includes("Title: Launching AI Launch Kit"),
|
|
109
|
+
);
|
|
110
|
+
expect(titleIdx).toBeGreaterThan(-1);
|
|
111
|
+
const urlLine = lines.slice(titleIdx).find((l) => l.startsWith(" URL:"));
|
|
112
|
+
expect(urlLine).toMatch(/\/blog\/launching-ailk/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("blog entries appear after section-catalog entries", async () => {
|
|
116
|
+
const response = await GET();
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
const homeIdx = text.indexOf("Title: Home");
|
|
119
|
+
const blogIdx = text.indexOf("Title: Blog");
|
|
120
|
+
expect(homeIdx).toBeGreaterThan(-1);
|
|
121
|
+
expect(blogIdx).toBeGreaterThan(homeIdx);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route handler tests: GET /robots.txt
|
|
7
|
+
*
|
|
8
|
+
* Validates: 200 status, text/plain Content-Type, sensible defaults, and
|
|
9
|
+
* optional AILK_ROBOTS_DISALLOW env-var override.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
describe("GET /robots.txt", () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
delete process.env["AILK_ROBOTS_DISALLOW"];
|
|
15
|
+
jest.resetModules();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns 200", async () => {
|
|
19
|
+
const { GET } = await import("../../app/robots.txt/route");
|
|
20
|
+
const response = await GET();
|
|
21
|
+
expect(response.status).toBe(200);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns text/plain content-type", async () => {
|
|
25
|
+
const { GET } = await import("../../app/robots.txt/route");
|
|
26
|
+
const response = await GET();
|
|
27
|
+
expect(response.headers.get("content-type")).toMatch(/text\/plain/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("body contains User-agent directive", async () => {
|
|
31
|
+
const { GET } = await import("../../app/robots.txt/route");
|
|
32
|
+
const response = await GET();
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
expect(text).toContain("User-agent:");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("body contains Sitemap directive by default", async () => {
|
|
38
|
+
const { GET } = await import("../../app/robots.txt/route");
|
|
39
|
+
const response = await GET();
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
expect(text).toContain("Sitemap:");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("honours AILK_ROBOTS_DISALLOW env var", async () => {
|
|
45
|
+
process.env["AILK_ROBOTS_DISALLOW"] = "/private/";
|
|
46
|
+
jest.resetModules();
|
|
47
|
+
const { GET } = await import("../../app/robots.txt/route");
|
|
48
|
+
const response = await GET();
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
expect(text).toContain("Disallow: /private/");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route handler tests: GET /sitemap.xml
|
|
7
|
+
*
|
|
8
|
+
* Validates: 200 status, application/xml Content-Type, body contains <urlset>
|
|
9
|
+
* and covers content seeds. MDX adapter is injected via createSitemapHandler
|
|
10
|
+
* rather than mocked at the module level.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createSitemapHandler } from "../../app/sitemap.xml/route";
|
|
14
|
+
import type { ContentAdapter } from "@working-theory/content-adapters";
|
|
15
|
+
|
|
16
|
+
function makeStubAdapter(): ContentAdapter {
|
|
17
|
+
// eslint-disable-next-line no-restricted-syntax -- jest.fn() stubs are not assignable to ContentAdapter's generic return types; partial stub is intentional, tracked in #1073
|
|
18
|
+
return {
|
|
19
|
+
listPages: jest.fn().mockResolvedValue([
|
|
20
|
+
{
|
|
21
|
+
type: "BlogPosting",
|
|
22
|
+
slug: "launching-ailk",
|
|
23
|
+
title: "Launching AI Launch Kit",
|
|
24
|
+
description: "A short note on why we built.",
|
|
25
|
+
},
|
|
26
|
+
]),
|
|
27
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
28
|
+
getPageBySlug: jest.fn().mockResolvedValue(null),
|
|
29
|
+
getDetailBySlug: jest.fn().mockResolvedValue(null),
|
|
30
|
+
searchPages: jest.fn().mockResolvedValue([]),
|
|
31
|
+
extractStructuredData: jest.fn().mockResolvedValue(null),
|
|
32
|
+
createPage: jest.fn(),
|
|
33
|
+
updatePage: jest.fn(),
|
|
34
|
+
deletePage: jest.fn(),
|
|
35
|
+
getPageVersion: jest.fn(),
|
|
36
|
+
} as unknown as ContentAdapter;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("GET /sitemap.xml", () => {
|
|
40
|
+
let GET: () => Promise<Response>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
process.env.NEXT_PUBLIC_ENABLED_LOCALES = "en";
|
|
44
|
+
process.env.NEXT_PUBLIC_DEFAULT_LOCALE = "en";
|
|
45
|
+
const stubAdapter = makeStubAdapter();
|
|
46
|
+
GET = createSitemapHandler({ getContentAdapter: () => stubAdapter });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
delete process.env.NEXT_PUBLIC_ENABLED_LOCALES;
|
|
51
|
+
delete process.env.NEXT_PUBLIC_DEFAULT_LOCALE;
|
|
52
|
+
jest.resetAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns 200", async () => {
|
|
56
|
+
const response = await GET();
|
|
57
|
+
expect(response.status).toBe(200);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns application/xml content-type", async () => {
|
|
61
|
+
const response = await GET();
|
|
62
|
+
expect(response.headers.get("content-type")).toMatch(/application\/xml/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("body is a valid XML sitemap with <urlset>", async () => {
|
|
66
|
+
const response = await GET();
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
expect(text).toContain("<urlset");
|
|
69
|
+
expect(text).toContain("</urlset>");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("body contains <url> entries for content seeds", async () => {
|
|
73
|
+
const response = await GET();
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
expect(text).toContain("<url>");
|
|
76
|
+
expect(text).toContain("<loc>");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route handler + builder tests: GET /.well-known/mcp.json
|
|
7
|
+
*
|
|
8
|
+
* Covers (issue #209 initial, #300 rename + extensions):
|
|
9
|
+
* - GET returns 200 + JSON with schemaVersion, name, description,
|
|
10
|
+
* mcpEndpoint, tools, transport, ailk
|
|
11
|
+
* - tools array enumerates all tools by name (derived from catalog, not
|
|
12
|
+
* hardcoded — tolerant of catalog growth when #299 adds ailkScaffold)
|
|
13
|
+
* - Each tool entry has name, description, inputSchema, outputSchema
|
|
14
|
+
* - CORS headers present on GET and OPTIONS responses
|
|
15
|
+
* - schemaVersion is '1'
|
|
16
|
+
* - cache-control matches the /.well-known/ai-plugin.json precedent
|
|
17
|
+
* - buildMcpManifest is a pure function (no I/O)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getMcpToolCatalog } from "@working-theory/metadata";
|
|
21
|
+
import { buildMcpManifest } from "../../app/.well-known/mcp.json/build-manifest";
|
|
22
|
+
|
|
23
|
+
describe("buildMcpManifest", () => {
|
|
24
|
+
it("returns an object with required top-level fields", () => {
|
|
25
|
+
const manifest = buildMcpManifest({
|
|
26
|
+
baseUrl: "https://example.com",
|
|
27
|
+
ailkVersion: "0.1.0",
|
|
28
|
+
});
|
|
29
|
+
expect(manifest).toHaveProperty("schemaVersion", "1");
|
|
30
|
+
expect(manifest).toHaveProperty("name");
|
|
31
|
+
expect(manifest).toHaveProperty("description");
|
|
32
|
+
expect(manifest).toHaveProperty("mcpEndpoint");
|
|
33
|
+
expect(manifest).toHaveProperty("tools");
|
|
34
|
+
expect(manifest).toHaveProperty("transport");
|
|
35
|
+
expect(manifest).toHaveProperty("ailk");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("name and description are non-empty strings", () => {
|
|
39
|
+
const manifest = buildMcpManifest({
|
|
40
|
+
baseUrl: "https://example.com",
|
|
41
|
+
ailkVersion: "0.1.0",
|
|
42
|
+
});
|
|
43
|
+
expect(typeof manifest.name).toBe("string");
|
|
44
|
+
expect(manifest.name.length).toBeGreaterThan(0);
|
|
45
|
+
expect(typeof manifest.description).toBe("string");
|
|
46
|
+
expect(manifest.description.length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("mcpEndpoint is built from the baseUrl", () => {
|
|
50
|
+
const manifest = buildMcpManifest({
|
|
51
|
+
baseUrl: "https://example.com",
|
|
52
|
+
ailkVersion: "0.1.0",
|
|
53
|
+
});
|
|
54
|
+
expect(manifest.mcpEndpoint).toContain("https://example.com");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("transport block has type 'http' and a version string", () => {
|
|
58
|
+
const manifest = buildMcpManifest({
|
|
59
|
+
baseUrl: "https://example.com",
|
|
60
|
+
ailkVersion: "0.1.0",
|
|
61
|
+
});
|
|
62
|
+
expect(manifest.transport).toHaveProperty("type", "http");
|
|
63
|
+
expect(typeof manifest.transport.version).toBe("string");
|
|
64
|
+
expect(manifest.transport.version.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("ailk block carries the version", () => {
|
|
68
|
+
const manifest = buildMcpManifest({
|
|
69
|
+
baseUrl: "https://example.com",
|
|
70
|
+
ailkVersion: "0.42.0",
|
|
71
|
+
});
|
|
72
|
+
expect(manifest.ailk).toEqual({ version: "0.42.0" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("tools array is consistent with the catalog (name-for-name match)", () => {
|
|
76
|
+
const catalog = getMcpToolCatalog();
|
|
77
|
+
const manifest = buildMcpManifest({
|
|
78
|
+
baseUrl: "https://example.com",
|
|
79
|
+
ailkVersion: "0.1.0",
|
|
80
|
+
});
|
|
81
|
+
const manifestNames = manifest.tools.map((t) => t.name).sort();
|
|
82
|
+
const catalogNames = catalog.map((t) => t.name).sort();
|
|
83
|
+
expect(manifestNames).toEqual(catalogNames);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("every tool entry has name, description, inputSchema, outputSchema", () => {
|
|
87
|
+
const manifest = buildMcpManifest({
|
|
88
|
+
baseUrl: "https://example.com",
|
|
89
|
+
ailkVersion: "0.1.0",
|
|
90
|
+
});
|
|
91
|
+
for (const tool of manifest.tools) {
|
|
92
|
+
expect(typeof tool.name).toBe("string");
|
|
93
|
+
expect(tool.name.length).toBeGreaterThan(0);
|
|
94
|
+
expect(typeof tool.description).toBe("string");
|
|
95
|
+
expect(tool.description.length).toBeGreaterThan(0);
|
|
96
|
+
expect(tool.inputSchema).toBeDefined();
|
|
97
|
+
expect(typeof tool.inputSchema).toBe("object");
|
|
98
|
+
expect(tool.outputSchema).toBeDefined();
|
|
99
|
+
expect(typeof tool.outputSchema).toBe("object");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("GET /.well-known/mcp.json", () => {
|
|
105
|
+
it("returns 200", async () => {
|
|
106
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
107
|
+
const response = await GET();
|
|
108
|
+
expect(response.status).toBe(200);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns application/json content-type", async () => {
|
|
112
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
113
|
+
const response = await GET();
|
|
114
|
+
expect(response.headers.get("content-type")).toMatch(/application\/json/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("body has required top-level fields", async () => {
|
|
118
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
119
|
+
const response = await GET();
|
|
120
|
+
const body = await response.json();
|
|
121
|
+
expect(body).toHaveProperty("schemaVersion", "1");
|
|
122
|
+
expect(body).toHaveProperty("name");
|
|
123
|
+
expect(body).toHaveProperty("description");
|
|
124
|
+
expect(body).toHaveProperty("mcpEndpoint");
|
|
125
|
+
expect(body).toHaveProperty("tools");
|
|
126
|
+
expect(body).toHaveProperty("transport");
|
|
127
|
+
expect(body).toHaveProperty("ailk");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("transport block has type 'http' and a version string", async () => {
|
|
131
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
132
|
+
const response = await GET();
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
expect(body.transport).toHaveProperty("type", "http");
|
|
135
|
+
expect(typeof body.transport.version).toBe("string");
|
|
136
|
+
expect(body.transport.version.length).toBeGreaterThan(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("tools array is consistent with the catalog (name-for-name match)", async () => {
|
|
140
|
+
const catalog = getMcpToolCatalog();
|
|
141
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
142
|
+
const response = await GET();
|
|
143
|
+
const body = await response.json();
|
|
144
|
+
const manifestNames = (body.tools as Array<{ name: string }>)
|
|
145
|
+
.map((t) => t.name)
|
|
146
|
+
.sort();
|
|
147
|
+
const catalogNames = catalog.map((t) => t.name).sort();
|
|
148
|
+
expect(manifestNames).toEqual(catalogNames);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("each tool entry has name, description, inputSchema, outputSchema", async () => {
|
|
152
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
153
|
+
const response = await GET();
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
for (const tool of body.tools as Array<Record<string, unknown>>) {
|
|
156
|
+
expect(typeof tool["name"]).toBe("string");
|
|
157
|
+
expect(typeof tool["description"]).toBe("string");
|
|
158
|
+
expect(tool["inputSchema"]).toBeDefined();
|
|
159
|
+
expect(typeof tool["inputSchema"]).toBe("object");
|
|
160
|
+
expect(tool["outputSchema"]).toBeDefined();
|
|
161
|
+
expect(typeof tool["outputSchema"]).toBe("object");
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("CORS: Access-Control-Allow-Origin is *", async () => {
|
|
166
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
167
|
+
const response = await GET();
|
|
168
|
+
expect(response.headers.get("access-control-allow-origin")).toBe("*");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("CORS: Access-Control-Allow-Methods includes GET", async () => {
|
|
172
|
+
const { GET } = await import("../../app/.well-known/mcp.json/route");
|
|
173
|
+
const response = await GET();
|
|
174
|
+
const methods = response.headers.get("access-control-allow-methods") ?? "";
|
|
175
|
+
expect(methods).toContain("GET");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("cache-control matches the /.well-known/ai-plugin.json precedent", async () => {
|
|
179
|
+
const { GET: getMcp } = await import(
|
|
180
|
+
"../../app/.well-known/mcp.json/route"
|
|
181
|
+
);
|
|
182
|
+
const { GET: getAiPlugin } = await import(
|
|
183
|
+
"../../app/.well-known/ai-plugin.json/route"
|
|
184
|
+
);
|
|
185
|
+
const mcpRes = await getMcp();
|
|
186
|
+
const aiPluginRes = getAiPlugin();
|
|
187
|
+
expect(mcpRes.headers.get("cache-control")).toBe(
|
|
188
|
+
aiPluginRes.headers.get("cache-control"),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("OPTIONS /.well-known/mcp.json", () => {
|
|
194
|
+
it("returns 204 with CORS headers", async () => {
|
|
195
|
+
const { OPTIONS } = await import("../../app/.well-known/mcp.json/route");
|
|
196
|
+
const response = await OPTIONS();
|
|
197
|
+
expect(response.status).toBe(204);
|
|
198
|
+
expect(response.headers.get("access-control-allow-origin")).toBe("*");
|
|
199
|
+
expect(response.headers.get("access-control-allow-methods")).toContain(
|
|
200
|
+
"GET",
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|