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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the list_pages MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - listPagesInputSchema validates correct inputs (optional type filter)
|
|
6
|
+
* - listPagesInputSchema rejects unknown type values
|
|
7
|
+
* - ailkListPages with a type arg returns pages for that type
|
|
8
|
+
* - ailkListPages with no type arg returns pages across all SCHEMA_ORG_TYPES
|
|
9
|
+
* - Output validates against listPagesOutputSchema
|
|
10
|
+
* - No console.* calls in handler (structural test)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { resolve, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ailkListPages,
|
|
18
|
+
listPagesInputSchema,
|
|
19
|
+
listPagesOutputSchema,
|
|
20
|
+
} from "../src/tools/list_pages.js";
|
|
21
|
+
|
|
22
|
+
// Content root for real-filesystem tests — repo-relative
|
|
23
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
24
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
25
|
+
|
|
26
|
+
describe("listPagesInputSchema", () => {
|
|
27
|
+
it("accepts empty input (type is optional)", () => {
|
|
28
|
+
const result = listPagesInputSchema.safeParse({});
|
|
29
|
+
expect(result.success).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts a valid SchemaOrgType", () => {
|
|
33
|
+
const result = listPagesInputSchema.safeParse({ type: "HomePage" });
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts HomePage", () => {
|
|
38
|
+
const result = listPagesInputSchema.safeParse({ type: "HomePage" });
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects an unknown type string", () => {
|
|
43
|
+
const result = listPagesInputSchema.safeParse({ type: "NotAType" });
|
|
44
|
+
expect(result.success).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects a numeric type", () => {
|
|
48
|
+
const result = listPagesInputSchema.safeParse({ type: 42 });
|
|
49
|
+
expect(result.success).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("ailkListPages handler", () => {
|
|
54
|
+
it("returns pages array for a specific type", async () => {
|
|
55
|
+
const result = await ailkListPages({ type: "HomePage" }, CONTENT_ROOT);
|
|
56
|
+
|
|
57
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
58
|
+
// The real content/ tree has at least one HomePage
|
|
59
|
+
expect(result.pages.length).toBeGreaterThan(0);
|
|
60
|
+
for (const page of result.pages) {
|
|
61
|
+
expect(page.type).toBe("HomePage");
|
|
62
|
+
expect(typeof page.slug).toBe("string");
|
|
63
|
+
expect(typeof page.title).toBe("string");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns empty pages array for a type with no content", async () => {
|
|
68
|
+
// FAQPage has no files in content/ by default
|
|
69
|
+
const result = await ailkListPages({ type: "FAQPage" }, CONTENT_ROOT);
|
|
70
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns pages across all SCHEMA_ORG_TYPES when type is omitted", async () => {
|
|
74
|
+
const result = await ailkListPages({}, CONTENT_ROOT);
|
|
75
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
76
|
+
// Should include at least one page (we know HomePage has content)
|
|
77
|
+
expect(result.pages.length).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("output shape has required slug + type + title fields", async () => {
|
|
81
|
+
const result = await ailkListPages({ type: "HomePage" }, CONTENT_ROOT);
|
|
82
|
+
|
|
83
|
+
for (const page of result.pages) {
|
|
84
|
+
expect(typeof page.slug).toBe("string");
|
|
85
|
+
expect(typeof page.type).toBe("string");
|
|
86
|
+
expect(typeof page.title).toBe("string");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("output validates against listPagesOutputSchema", async () => {
|
|
91
|
+
const result = await ailkListPages({ type: "HomePage" }, CONTENT_ROOT);
|
|
92
|
+
const parsed = listPagesOutputSchema.safeParse(result);
|
|
93
|
+
expect(parsed.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("listPagesInputSchema — Zod boundary security", () => {
|
|
98
|
+
it("rejects input with path traversal characters in type", () => {
|
|
99
|
+
const result = listPagesInputSchema.safeParse({ type: "../etc/passwd" });
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("architectural guard — no console calls", () => {
|
|
105
|
+
it("apps/mcp/src/tools/list_pages.ts does not call console.*", () => {
|
|
106
|
+
const src = readFileSync(
|
|
107
|
+
resolve(__dirname, "../src/tools/list_pages.ts"),
|
|
108
|
+
"utf-8",
|
|
109
|
+
);
|
|
110
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the locale parameter on content read MCP tools (Phase 3 S3).
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - read_content accepts an optional `locale` parameter
|
|
6
|
+
* - list_pages accepts an optional `locale` parameter
|
|
7
|
+
* - search_content accepts an optional `locale` parameter
|
|
8
|
+
* - Tool schemas accept BCP-47 tags and reject malformed values
|
|
9
|
+
* - Tool handlers return data when valid locale is passed (back-compat)
|
|
10
|
+
* - Invalid/unknown locales silently fall back to default (AC-i18n-07)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolve, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
listPagesInputSchema,
|
|
17
|
+
readContentInputSchema,
|
|
18
|
+
searchContentInputSchema,
|
|
19
|
+
} from "@working-theory/validation";
|
|
20
|
+
|
|
21
|
+
import { ailkListPages } from "../src/tools/list_pages.js";
|
|
22
|
+
import { ailkReadContent } from "../src/tools/read_content.js";
|
|
23
|
+
import { ailkSearchContent } from "../src/tools/search_content.js";
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
26
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
27
|
+
|
|
28
|
+
describe("locale parameter — input schemas", () => {
|
|
29
|
+
it("readContentInputSchema accepts a valid BCP-47 tag", () => {
|
|
30
|
+
const r = readContentInputSchema.safeParse({
|
|
31
|
+
type: "HomePage",
|
|
32
|
+
slug: "home",
|
|
33
|
+
locale: "en",
|
|
34
|
+
});
|
|
35
|
+
expect(r.success).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("readContentInputSchema accepts region-tagged BCP-47 (zh-CN)", () => {
|
|
39
|
+
const r = readContentInputSchema.safeParse({
|
|
40
|
+
type: "HomePage",
|
|
41
|
+
slug: "home",
|
|
42
|
+
locale: "zh-CN",
|
|
43
|
+
});
|
|
44
|
+
expect(r.success).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("readContentInputSchema rejects malformed locale (uppercase)", () => {
|
|
48
|
+
const r = readContentInputSchema.safeParse({
|
|
49
|
+
type: "HomePage",
|
|
50
|
+
slug: "home",
|
|
51
|
+
locale: "EN",
|
|
52
|
+
});
|
|
53
|
+
expect(r.success).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("readContentInputSchema rejects path-traversal locale", () => {
|
|
57
|
+
const r = readContentInputSchema.safeParse({
|
|
58
|
+
type: "HomePage",
|
|
59
|
+
slug: "home",
|
|
60
|
+
locale: "../../etc/passwd",
|
|
61
|
+
});
|
|
62
|
+
expect(r.success).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("readContentInputSchema accepts absent locale (optional)", () => {
|
|
66
|
+
const r = readContentInputSchema.safeParse({
|
|
67
|
+
type: "HomePage",
|
|
68
|
+
slug: "home",
|
|
69
|
+
});
|
|
70
|
+
expect(r.success).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("listPagesInputSchema accepts a valid locale", () => {
|
|
74
|
+
const r = listPagesInputSchema.safeParse({
|
|
75
|
+
type: "HomePage",
|
|
76
|
+
locale: "en",
|
|
77
|
+
});
|
|
78
|
+
expect(r.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("searchContentInputSchema accepts a valid locale", () => {
|
|
82
|
+
const r = searchContentInputSchema.safeParse({
|
|
83
|
+
query: "foo",
|
|
84
|
+
locale: "en",
|
|
85
|
+
});
|
|
86
|
+
expect(r.success).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("locale parameter — handler behavior (real adapter)", () => {
|
|
91
|
+
it("ailkReadContent returns the page when locale=en", async () => {
|
|
92
|
+
const result = await ailkReadContent(
|
|
93
|
+
{ type: "HomePage", slug: "home", locale: "en" },
|
|
94
|
+
CONTENT_ROOT,
|
|
95
|
+
);
|
|
96
|
+
expect(result).not.toBeNull();
|
|
97
|
+
expect(result!.slug).toBe("home");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ailkReadContent silently falls back when locale=zz (unknown)", async () => {
|
|
101
|
+
const result = await ailkReadContent(
|
|
102
|
+
{ type: "HomePage", slug: "home", locale: "zz" },
|
|
103
|
+
CONTENT_ROOT,
|
|
104
|
+
);
|
|
105
|
+
// AC-i18n-07: silent fallback to default locale's content.
|
|
106
|
+
expect(result).not.toBeNull();
|
|
107
|
+
expect(result!.slug).toBe("home");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("ailkListPages returns pages when locale=en", async () => {
|
|
111
|
+
const result = await ailkListPages(
|
|
112
|
+
{ type: "HomePage", locale: "en" },
|
|
113
|
+
CONTENT_ROOT,
|
|
114
|
+
);
|
|
115
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
116
|
+
expect(result.pages.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("ailkListPages silently falls back when locale=zz", async () => {
|
|
120
|
+
const result = await ailkListPages(
|
|
121
|
+
{ type: "HomePage", locale: "zz" },
|
|
122
|
+
CONTENT_ROOT,
|
|
123
|
+
);
|
|
124
|
+
// Falls back to default-locale list.
|
|
125
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ailkSearchContent returns results when locale=en", async () => {
|
|
129
|
+
const result = await ailkSearchContent(
|
|
130
|
+
{ query: "AI", locale: "en" },
|
|
131
|
+
CONTENT_ROOT,
|
|
132
|
+
);
|
|
133
|
+
expect(Array.isArray(result.results)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP ↔ Server-Action parity test.
|
|
3
|
+
*
|
|
4
|
+
* Today: trivially passes (no Server Actions in apps/web, no MCP tools yet).
|
|
5
|
+
* Phase 3: every Server Action in apps/web has a matching MCP tool.
|
|
6
|
+
*
|
|
7
|
+
* Convention (locked in spec §3):
|
|
8
|
+
* - Server Actions live at `apps/web/**\/*.actions.ts` and export a symbol
|
|
9
|
+
* named `^[a-z][a-zA-Z0-9]*Action$` (e.g. `captureLeadAction`).
|
|
10
|
+
* - MCP tools live at `apps/mcp/src/tools/<tool>.ts` and export a symbol
|
|
11
|
+
* named `^ailk[A-Z][a-zA-Z0-9]*$` (e.g. `ailkCaptureLead`).
|
|
12
|
+
* - Mapping: `<name>Action` ↔ `ailk<Name>` (PascalCase of the action prefix).
|
|
13
|
+
*
|
|
14
|
+
* The harness exposes `computeParityFailures(actions, tools)` as a pure helper
|
|
15
|
+
* so the populated/empty/divergent cases can be exercised without filesystem.
|
|
16
|
+
* The integration case enumerates the real filesystem and runs the same helper.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
|
|
20
|
+
import { resolve, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
23
|
+
const WEB_ROOT = join(REPO_ROOT, "apps/web");
|
|
24
|
+
const MCP_TOOLS_ROOT = join(REPO_ROOT, "apps/mcp/src/tools");
|
|
25
|
+
|
|
26
|
+
const ACTION_FILE_RE = /\.actions\.ts$/;
|
|
27
|
+
const ACTION_EXPORT_RE =
|
|
28
|
+
/export\s+(?:async\s+)?(?:function|const)\s+([a-z][a-zA-Z0-9]*Action)\b/g;
|
|
29
|
+
const TOOL_EXPORT_RE =
|
|
30
|
+
/export\s+(?:async\s+)?(?:function|const)\s+(ailk[A-Z][a-zA-Z0-9]*)\b/g;
|
|
31
|
+
|
|
32
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
33
|
+
if (!existsSync(dir)) return out;
|
|
34
|
+
for (const entry of readdirSync(dir)) {
|
|
35
|
+
if (entry === "node_modules" || entry === "dist" || entry === ".next")
|
|
36
|
+
continue;
|
|
37
|
+
const p = join(dir, entry);
|
|
38
|
+
const s = statSync(p);
|
|
39
|
+
if (s.isDirectory()) walk(p, out);
|
|
40
|
+
else out.push(p);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function discoverServerActions(webRoot: string = WEB_ROOT): Set<string> {
|
|
46
|
+
const found = new Set<string>();
|
|
47
|
+
for (const file of walk(webRoot)) {
|
|
48
|
+
if (!ACTION_FILE_RE.test(file)) continue;
|
|
49
|
+
const content = readFileSync(file, "utf-8");
|
|
50
|
+
for (const m of content.matchAll(ACTION_EXPORT_RE)) {
|
|
51
|
+
found.add(m[1]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function discoverMcpTools(
|
|
58
|
+
toolsRoot: string = MCP_TOOLS_ROOT,
|
|
59
|
+
): Set<string> {
|
|
60
|
+
const found = new Set<string>();
|
|
61
|
+
if (!existsSync(toolsRoot)) return found;
|
|
62
|
+
for (const file of walk(toolsRoot)) {
|
|
63
|
+
if (!file.endsWith(".ts")) continue;
|
|
64
|
+
const content = readFileSync(file, "utf-8");
|
|
65
|
+
for (const m of content.matchAll(TOOL_EXPORT_RE)) {
|
|
66
|
+
found.add(m[1]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Map an action symbol (e.g. `captureLeadAction`) to its expected MCP tool
|
|
74
|
+
* symbol (`ailkCaptureLead`).
|
|
75
|
+
*/
|
|
76
|
+
export function actionToToolName(action: string): string {
|
|
77
|
+
// Strip trailing "Action" and capitalize first char.
|
|
78
|
+
const base = action.replace(/Action$/, "");
|
|
79
|
+
if (base.length === 0) return "ailk";
|
|
80
|
+
return `ailk${base[0].toUpperCase()}${base.slice(1)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* MCP-only tools — tools that have no Web Server Action equivalent because
|
|
85
|
+
* they are pure-read content tools. Adding a Server Action wrapper would be
|
|
86
|
+
* meaningless: these tools exist to serve agent consumers, not web UI flows.
|
|
87
|
+
*
|
|
88
|
+
* Each entry is the camelCase MCP handler name (e.g. ailkListPages).
|
|
89
|
+
* Extend this list in subsequent issues (I3–I5) as more read-only tools land.
|
|
90
|
+
*
|
|
91
|
+
* Note: this allow-list uses the symbol name convention (`ailk<PascalVerb>`),
|
|
92
|
+
* not the MCP tool name convention (`list_pages`). The parity harness operates
|
|
93
|
+
* on symbol names discovered via regex from tool source files.
|
|
94
|
+
*/
|
|
95
|
+
const MCP_ONLY_TOOLS = new Set([
|
|
96
|
+
"ailkListPages", // list_pages — pure read, no action equivalent (I2)
|
|
97
|
+
"ailkReadContent", // read_content — pure read, no action equivalent (I2)
|
|
98
|
+
"ailkSearchContent", // search_content — pure read, no action equivalent (I4)
|
|
99
|
+
"ailkGetSiteInfo", // get_site_info — pure read, no action equivalent (I4)
|
|
100
|
+
"ailkGetBusinessHours", // get_business_hours — pure read, no action equivalent (I4)
|
|
101
|
+
"ailkGetLocations", // get_locations — pure read, no action equivalent (I4)
|
|
102
|
+
"ailkGetOfferings", // get_offerings — pure read, no action equivalent (I4)
|
|
103
|
+
"ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
|
|
104
|
+
"ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
|
|
105
|
+
"ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
|
|
106
|
+
"ailkSubscribeNewsletter", // subscribe_newsletter — anonymous write via MCP; no Server Action (B7)
|
|
107
|
+
"ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
|
|
108
|
+
"ailkScaffold", // scaffold — agent-only scaffolder; no Server Action equivalent (#299)
|
|
109
|
+
"ailkCreatePage", // create_page — agent-first Pro content write; HTTP twin is apps/api, not a Server Action (D14a #55)
|
|
110
|
+
"ailkUpdatePage", // update_page — agent-first Pro content write; HTTP twin is apps/api, not a Server Action (D14a #55)
|
|
111
|
+
"ailkDeletePage", // delete_page — agent-first Pro content write; HTTP twin is apps/api, not a Server Action (D14a #55)
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pure parity computation. Given the set of action symbols and the set of
|
|
116
|
+
* MCP tool symbols, return the missing-tool and orphan-tool sets.
|
|
117
|
+
*
|
|
118
|
+
* Tools in MCP_ONLY_TOOLS are excluded from orphan reporting — they are
|
|
119
|
+
* intentionally MCP-only and have no Server Action counterpart.
|
|
120
|
+
*/
|
|
121
|
+
export function computeParityFailures(
|
|
122
|
+
actions: Set<string>,
|
|
123
|
+
tools: Set<string>,
|
|
124
|
+
): { missingTools: string[]; orphanTools: string[] } {
|
|
125
|
+
const expected = new Set<string>();
|
|
126
|
+
for (const a of actions) expected.add(actionToToolName(a));
|
|
127
|
+
|
|
128
|
+
const missingTools: string[] = [];
|
|
129
|
+
for (const e of expected) if (!tools.has(e)) missingTools.push(e);
|
|
130
|
+
|
|
131
|
+
const orphanTools: string[] = [];
|
|
132
|
+
for (const t of tools) {
|
|
133
|
+
if (!expected.has(t) && !MCP_ONLY_TOOLS.has(t)) orphanTools.push(t);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
missingTools.sort();
|
|
137
|
+
orphanTools.sort();
|
|
138
|
+
return { missingTools, orphanTools };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe("MCP ↔ Server-Action parity (helper)", () => {
|
|
144
|
+
it("trivially passes when both sides are empty", () => {
|
|
145
|
+
const r = computeParityFailures(new Set(), new Set());
|
|
146
|
+
expect(r.missingTools).toEqual([]);
|
|
147
|
+
expect(r.orphanTools).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("reports missing MCP tools when an action has no matching tool", () => {
|
|
151
|
+
const actions = new Set(["captureLeadAction"]);
|
|
152
|
+
const tools = new Set<string>();
|
|
153
|
+
const r = computeParityFailures(actions, tools);
|
|
154
|
+
expect(r.missingTools).toEqual(["ailkCaptureLead"]);
|
|
155
|
+
expect(r.orphanTools).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reports orphan MCP tools when a tool has no matching action", () => {
|
|
159
|
+
const actions = new Set<string>();
|
|
160
|
+
const tools = new Set(["ailkPurchase"]);
|
|
161
|
+
const r = computeParityFailures(actions, tools);
|
|
162
|
+
expect(r.missingTools).toEqual([]);
|
|
163
|
+
expect(r.orphanTools).toEqual(["ailkPurchase"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("passes when actions and tools are paired", () => {
|
|
167
|
+
const actions = new Set(["captureLeadAction", "purchaseAction"]);
|
|
168
|
+
const tools = new Set(["ailkCaptureLead", "ailkPurchase"]);
|
|
169
|
+
const r = computeParityFailures(actions, tools);
|
|
170
|
+
expect(r.missingTools).toEqual([]);
|
|
171
|
+
expect(r.orphanTools).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("maps action names to tool names by stripping Action and prefixing ailk", () => {
|
|
175
|
+
expect(actionToToolName("captureLeadAction")).toBe("ailkCaptureLead");
|
|
176
|
+
expect(actionToToolName("purchaseAction")).toBe("ailkPurchase");
|
|
177
|
+
expect(actionToToolName("auditAction")).toBe("ailkAudit");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("MCP ↔ Server-Action parity (filesystem)", () => {
|
|
182
|
+
it("D14a Stage 1 Unit C state: two Server Actions (purchaseAction + captureLeadAction), eighteen MCP tools, zero orphans", () => {
|
|
183
|
+
const actions = discoverServerActions();
|
|
184
|
+
const tools = discoverMcpTools();
|
|
185
|
+
const r = computeParityFailures(actions, tools);
|
|
186
|
+
|
|
187
|
+
// After D14a Stage 1 Unit C (#55): eighteen tools + two paired Server Actions.
|
|
188
|
+
// - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
|
|
189
|
+
// - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
|
|
190
|
+
// - ailkListPages, ailkReadContent: MCP-only (pure read; I2)
|
|
191
|
+
// - ailkSearchContent, ailkGetSiteInfo, ailkGetBusinessHours,
|
|
192
|
+
// ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
|
|
193
|
+
// MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
|
|
194
|
+
// - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
|
|
195
|
+
// - ailkSubscribeNewsletter, ailkRequestCallback:
|
|
196
|
+
// MCP-only anonymous-write tools (B7; in MCP_ONLY_TOOLS allow-list).
|
|
197
|
+
// - ailkScaffold: MCP-only agent scaffolder (#299; in MCP_ONLY_TOOLS).
|
|
198
|
+
// - ailkCreatePage, ailkUpdatePage, ailkDeletePage: MCP-only agent-first
|
|
199
|
+
// Pro content writes; HTTP twin is apps/api, not Server Actions (D14a #55).
|
|
200
|
+
expect(actions.size).toBe(2);
|
|
201
|
+
expect(tools.size).toBe(18);
|
|
202
|
+
expect(r.missingTools).toEqual([]);
|
|
203
|
+
expect(r.orphanTools).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for S15a — purchase tool disabled at OSS catalog.
|
|
3
|
+
*
|
|
4
|
+
* Issue #610 / spec
|
|
5
|
+
* `docs/internal/specs/ailk-pre-oss-security-phase-3-s15a-disable-purchase-spec.md`.
|
|
6
|
+
*
|
|
7
|
+
* Acceptance criterion verified here: `start_purchase` is NOT in the
|
|
8
|
+
* registered MCP tool catalog when the server is constructed by
|
|
9
|
+
* `createMcpServer()`. If a future PR re-registers the tool (by removing
|
|
10
|
+
* `'start_purchase'` from `OSS_DISABLED_TOOLS` in `apps/mcp/src/server.ts`),
|
|
11
|
+
* this test fails RED, forcing a deliberate review of the Phase 3
|
|
12
|
+
* re-enablement criteria in
|
|
13
|
+
* `docs/internal/roadmaps/phase-3-readiness.md`.
|
|
14
|
+
*
|
|
15
|
+
* `purchase.ts` source is preserved — only the registration is filtered.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createMcpServer, listRegisteredTools } from "../src/server.js";
|
|
19
|
+
|
|
20
|
+
describe("purchase tool — disabled at OSS catalog (S15a)", () => {
|
|
21
|
+
it("start_purchase is NOT in listRegisteredTools()", () => {
|
|
22
|
+
const tools = listRegisteredTools();
|
|
23
|
+
const names = tools.map((t) => t.name);
|
|
24
|
+
expect(names).not.toContain("start_purchase");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("start_purchase is NOT in createMcpServer()._registeredTools", () => {
|
|
28
|
+
const server = createMcpServer();
|
|
29
|
+
// _registeredTools is a plain object (Record<string, ...>) in SDK v1.x.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors apps/mcp/__tests__/server.test.ts
|
|
31
|
+
const tools = (server as any)._registeredTools as Record<string, unknown>;
|
|
32
|
+
expect("start_purchase" in tools).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the read_content MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - readContentInputSchema validates correct inputs (type + slug)
|
|
6
|
+
* - readContentInputSchema rejects unknown type values and malformed slugs
|
|
7
|
+
* - ailkReadContent returns { slug, type, frontmatter, body } for a known slug
|
|
8
|
+
* - ailkReadContent returns null (not throws) for an unknown slug
|
|
9
|
+
* - Output validates against readContentOutputSchema
|
|
10
|
+
* - No console.* calls in handler (structural test)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { resolve, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ailkReadContent,
|
|
18
|
+
readContentInputSchema,
|
|
19
|
+
readContentOutputSchema,
|
|
20
|
+
} from "../src/tools/read_content.js";
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
23
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
24
|
+
|
|
25
|
+
describe("readContentInputSchema", () => {
|
|
26
|
+
it("accepts a valid type + slug", () => {
|
|
27
|
+
const result = readContentInputSchema.safeParse({
|
|
28
|
+
type: "HomePage",
|
|
29
|
+
slug: "home",
|
|
30
|
+
});
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("accepts a HomePage slug", () => {
|
|
35
|
+
const result = readContentInputSchema.safeParse({
|
|
36
|
+
type: "HomePage",
|
|
37
|
+
slug: "home",
|
|
38
|
+
});
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects missing slug", () => {
|
|
43
|
+
const result = readContentInputSchema.safeParse({ type: "HomePage" });
|
|
44
|
+
expect(result.success).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects missing type", () => {
|
|
48
|
+
const result = readContentInputSchema.safeParse({
|
|
49
|
+
slug: "launching-ailk",
|
|
50
|
+
});
|
|
51
|
+
expect(result.success).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rejects an unknown type", () => {
|
|
55
|
+
const result = readContentInputSchema.safeParse({
|
|
56
|
+
type: "NotAType",
|
|
57
|
+
slug: "some-slug",
|
|
58
|
+
});
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("rejects empty slug", () => {
|
|
63
|
+
const result = readContentInputSchema.safeParse({
|
|
64
|
+
type: "HomePage",
|
|
65
|
+
slug: "",
|
|
66
|
+
});
|
|
67
|
+
expect(result.success).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects slug with path traversal characters", () => {
|
|
71
|
+
const result = readContentInputSchema.safeParse({
|
|
72
|
+
type: "HomePage",
|
|
73
|
+
slug: "../etc/passwd",
|
|
74
|
+
});
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("rejects slug with forward slashes", () => {
|
|
79
|
+
const result = readContentInputSchema.safeParse({
|
|
80
|
+
type: "HomePage",
|
|
81
|
+
slug: "some/path",
|
|
82
|
+
});
|
|
83
|
+
expect(result.success).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("ailkReadContent handler", () => {
|
|
88
|
+
it("returns slug, type, frontmatter, body for a known HomePage", async () => {
|
|
89
|
+
const result = await ailkReadContent(
|
|
90
|
+
{ type: "HomePage", slug: "home" },
|
|
91
|
+
CONTENT_ROOT,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result).not.toBeNull();
|
|
95
|
+
expect(result!.slug).toBe("home");
|
|
96
|
+
expect(result!.type).toBe("HomePage");
|
|
97
|
+
expect(typeof result!.frontmatter).toBe("object");
|
|
98
|
+
expect(typeof result!.body).toBe("string");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns null (not throws) for an unknown slug", async () => {
|
|
102
|
+
const result = await ailkReadContent(
|
|
103
|
+
{ type: "HomePage", slug: "does-not-exist" },
|
|
104
|
+
CONTENT_ROOT,
|
|
105
|
+
);
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns null for a slug that exists in a different type", async () => {
|
|
110
|
+
// home.mdx exists under HomePage, not AboutPage
|
|
111
|
+
const result = await ailkReadContent(
|
|
112
|
+
{ type: "AboutPage", slug: "home" },
|
|
113
|
+
CONTENT_ROOT,
|
|
114
|
+
);
|
|
115
|
+
expect(result).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns null for a valid type with no files when given an unknown slug", async () => {
|
|
119
|
+
const result = await ailkReadContent(
|
|
120
|
+
{ type: "FAQPage", slug: "nonexistent" },
|
|
121
|
+
CONTENT_ROOT,
|
|
122
|
+
);
|
|
123
|
+
expect(result).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("output shape validates against readContentOutputSchema when non-null", async () => {
|
|
127
|
+
const result = await ailkReadContent(
|
|
128
|
+
{ type: "HomePage", slug: "home" },
|
|
129
|
+
CONTENT_ROOT,
|
|
130
|
+
);
|
|
131
|
+
const parsed = readContentOutputSchema.safeParse(result);
|
|
132
|
+
expect(parsed.success).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("null validates against readContentOutputSchema", async () => {
|
|
136
|
+
const result = await ailkReadContent(
|
|
137
|
+
{ type: "HomePage", slug: "does-not-exist" },
|
|
138
|
+
CONTENT_ROOT,
|
|
139
|
+
);
|
|
140
|
+
const parsed = readContentOutputSchema.safeParse(result);
|
|
141
|
+
expect(parsed.success).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("architectural guard — no console calls", () => {
|
|
146
|
+
it("apps/mcp/src/tools/read_content.ts does not call console.*", () => {
|
|
147
|
+
const src = readFileSync(
|
|
148
|
+
resolve(__dirname, "../src/tools/read_content.ts"),
|
|
149
|
+
"utf-8",
|
|
150
|
+
);
|
|
151
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
152
|
+
});
|
|
153
|
+
});
|