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,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for abuse/user-agent-deny.ts
|
|
3
|
+
*
|
|
4
|
+
* Verifies built-in bot UA deny list plus env-configured additions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isUserAgentBlocked,
|
|
9
|
+
buildDenyList,
|
|
10
|
+
} from "../../src/abuse/user-agent-deny.js";
|
|
11
|
+
|
|
12
|
+
describe("buildDenyList", () => {
|
|
13
|
+
it("includes built-in bot user agents", () => {
|
|
14
|
+
const list = buildDenyList();
|
|
15
|
+
expect(list.some((r) => r.test("python-requests/2.28"))).toBe(true);
|
|
16
|
+
expect(list.some((r) => r.test("curl/7.88"))).toBe(true);
|
|
17
|
+
expect(list.some((r) => r.test("Wget/1.21"))).toBe(true);
|
|
18
|
+
expect(list.some((r) => r.test("Scrapy/2.8"))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("merges env-configured additions", () => {
|
|
22
|
+
const list = buildDenyList("BadBot/1.0,EvilCrawler");
|
|
23
|
+
expect(list.some((r) => r.test("BadBot/1.0"))).toBe(true);
|
|
24
|
+
expect(list.some((r) => r.test("EvilCrawler/2"))).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("isUserAgentBlocked", () => {
|
|
29
|
+
it("blocks a known bot UA", () => {
|
|
30
|
+
const denyList = buildDenyList();
|
|
31
|
+
expect(isUserAgentBlocked("python-requests/2.28.1", denyList)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("allows a real browser UA", () => {
|
|
35
|
+
const denyList = buildDenyList();
|
|
36
|
+
expect(
|
|
37
|
+
isUserAgentBlocked(
|
|
38
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
39
|
+
denyList,
|
|
40
|
+
),
|
|
41
|
+
).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("blocks missing UA (empty string)", () => {
|
|
45
|
+
const denyList = buildDenyList();
|
|
46
|
+
expect(isUserAgentBlocked("", denyList)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("blocks undefined/null UA", () => {
|
|
50
|
+
const denyList = buildDenyList();
|
|
51
|
+
expect(isUserAgentBlocked(undefined, denyList)).toBe(true);
|
|
52
|
+
expect(isUserAgentBlocked(null, denyList)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AEO CLI ↔ MCP tool parity test.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `ailkAudit` produces per-page output that matches what
|
|
5
|
+
* you'd get by calling `runAudit` directly with the same single-page input
|
|
6
|
+
* that `buildSinglePageAuditInput` produces.
|
|
7
|
+
*
|
|
8
|
+
* Both code paths use only the public `@working-theory/aeo` API — no imports from
|
|
9
|
+
* CLI-internal files (packages/aeo/src/cli/*). The parity claim is:
|
|
10
|
+
* buildSinglePageAuditInput → runAudit(input, RULES)
|
|
11
|
+
* ≡ ailkAudit internally (same adapter, same RULES)
|
|
12
|
+
*
|
|
13
|
+
* Acceptance criteria verified:
|
|
14
|
+
* - Output of audit for a fixture page equals the result of calling
|
|
15
|
+
* runAudit with the same AuditInput (Layer 2 AC: "identical per-page output")
|
|
16
|
+
* - runAudit invocation uses canonical RULES from packages/aeo/src/rules/index.ts
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { resolve, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
runAudit,
|
|
23
|
+
alwaysFails,
|
|
24
|
+
alwaysPasses,
|
|
25
|
+
entityMarkup,
|
|
26
|
+
faqHowtoIncentive,
|
|
27
|
+
jsonldValidity,
|
|
28
|
+
schemaCoverage,
|
|
29
|
+
type Rule,
|
|
30
|
+
} from "@working-theory/aeo";
|
|
31
|
+
|
|
32
|
+
import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
|
|
33
|
+
import { ailkAudit } from "../src/tools/audit.js";
|
|
34
|
+
|
|
35
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
36
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
37
|
+
|
|
38
|
+
// Canonical rule set — mirrors packages/aeo/src/cli/index.ts RULES array.
|
|
39
|
+
// Must stay in sync with that definition; both are sourced from @working-theory/aeo.
|
|
40
|
+
const RULES: Rule[] = [
|
|
41
|
+
alwaysPasses,
|
|
42
|
+
alwaysFails,
|
|
43
|
+
schemaCoverage,
|
|
44
|
+
jsonldValidity,
|
|
45
|
+
faqHowtoIncentive,
|
|
46
|
+
entityMarkup,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const FIXTURE_SLUG = "home";
|
|
50
|
+
const FIXTURE_TYPE = "HomePage" as const;
|
|
51
|
+
|
|
52
|
+
describe("audit ↔ runAudit direct parity", () => {
|
|
53
|
+
it("per-page score matches runAudit(buildSinglePageAuditInput(slug), RULES)", async () => {
|
|
54
|
+
// --- Direct path (mirrors what the tool does internally) ---
|
|
55
|
+
const auditInput = await buildSinglePageAuditInput(
|
|
56
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
57
|
+
CONTENT_ROOT,
|
|
58
|
+
);
|
|
59
|
+
const directReport = runAudit(auditInput, RULES);
|
|
60
|
+
const directPage = directReport.perPage.find(
|
|
61
|
+
(p) => p.slug === FIXTURE_SLUG && p.type === FIXTURE_TYPE,
|
|
62
|
+
);
|
|
63
|
+
expect(directPage).toBeDefined();
|
|
64
|
+
|
|
65
|
+
// --- MCP tool path ---
|
|
66
|
+
const mcpResult = await ailkAudit(
|
|
67
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
68
|
+
CONTENT_ROOT,
|
|
69
|
+
);
|
|
70
|
+
const mcpPage = mcpResult.perPage.find(
|
|
71
|
+
(p) => p.slug === FIXTURE_SLUG && p.type === FIXTURE_TYPE,
|
|
72
|
+
);
|
|
73
|
+
expect(mcpPage).toBeDefined();
|
|
74
|
+
|
|
75
|
+
// Per-page fields must match
|
|
76
|
+
expect(mcpPage!.score).toBe(directPage!.score);
|
|
77
|
+
expect(mcpPage!.slug).toBe(directPage!.slug);
|
|
78
|
+
expect(mcpPage!.type).toBe(directPage!.type);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("overall score matches runAudit called with the same AuditInput", async () => {
|
|
82
|
+
const auditInput = await buildSinglePageAuditInput(
|
|
83
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
84
|
+
CONTENT_ROOT,
|
|
85
|
+
);
|
|
86
|
+
const directReport = runAudit(auditInput, RULES);
|
|
87
|
+
|
|
88
|
+
const mcpResult = await ailkAudit(
|
|
89
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
90
|
+
CONTENT_ROOT,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(mcpResult.score).toBe(directReport.score);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("perRule count matches — tool uses the same RULES array", async () => {
|
|
97
|
+
const auditInput = await buildSinglePageAuditInput(
|
|
98
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
99
|
+
CONTENT_ROOT,
|
|
100
|
+
);
|
|
101
|
+
const directReport = runAudit(auditInput, RULES);
|
|
102
|
+
|
|
103
|
+
const mcpResult = await ailkAudit(
|
|
104
|
+
{ slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
|
|
105
|
+
CONTENT_ROOT,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(mcpResult.perRule).toHaveLength(directReport.perRule.length);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for apps/mcp/src/lib/audit-single-page.ts
|
|
3
|
+
*
|
|
4
|
+
* TDD: URL validation cases (security boundary) first, then happy paths.
|
|
5
|
+
*
|
|
6
|
+
* Acceptance criteria verified here:
|
|
7
|
+
* - Rejects URLs not on the AILK installation's own domain (SSRF guard)
|
|
8
|
+
* - Rejects malformed URLs
|
|
9
|
+
* - Rejects when both slug and url are provided
|
|
10
|
+
* - Rejects when neither slug nor url is provided
|
|
11
|
+
* - Accepts slug-only input and builds AuditInput
|
|
12
|
+
* - Accepts same-origin url input and builds AuditInput
|
|
13
|
+
* - Strips query strings + fragments before slug extraction
|
|
14
|
+
* - Cross-domain error message references ailk audit CLI
|
|
15
|
+
* - No fetch() call (structural test)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { resolve, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
24
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
25
|
+
|
|
26
|
+
// Set env to a known base URL for tests that exercise same-origin logic.
|
|
27
|
+
const TEST_BASE_URL = "https://example.ailk.test";
|
|
28
|
+
|
|
29
|
+
function withBaseUrl(
|
|
30
|
+
url: string,
|
|
31
|
+
fn: () => Promise<unknown>,
|
|
32
|
+
): Promise<unknown> {
|
|
33
|
+
const prev = process.env.AILK_BASE_URL;
|
|
34
|
+
process.env.AILK_BASE_URL = url;
|
|
35
|
+
return fn().finally(() => {
|
|
36
|
+
if (prev === undefined) delete process.env.AILK_BASE_URL;
|
|
37
|
+
else process.env.AILK_BASE_URL = prev;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── URL-validation tests (security boundary — RED first) ──────────────────────
|
|
42
|
+
|
|
43
|
+
describe("buildSinglePageAuditInput — URL validation (security boundary)", () => {
|
|
44
|
+
it("rejects a cross-domain URL", async () => {
|
|
45
|
+
await expect(
|
|
46
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
47
|
+
buildSinglePageAuditInput(
|
|
48
|
+
{ url: "https://evil.example.com/HomePage/some-page" },
|
|
49
|
+
CONTENT_ROOT,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
).rejects.toThrow(/out of scope|use `ailk audit`/i);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("cross-domain error message references ailk audit CLI", async () => {
|
|
56
|
+
const err = await withBaseUrl(TEST_BASE_URL, () =>
|
|
57
|
+
buildSinglePageAuditInput(
|
|
58
|
+
{ url: "https://evil.example.com/HomePage/some-page" },
|
|
59
|
+
CONTENT_ROOT,
|
|
60
|
+
),
|
|
61
|
+
).catch((e: Error) => e);
|
|
62
|
+
expect(err).toBeInstanceOf(Error);
|
|
63
|
+
expect((err as Error).message).toMatch(/ailk audit/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects a URL with user-info (credential-stuffed URL bypass attempt)", async () => {
|
|
67
|
+
// The bypass form: https://trusted-domain@attacker.com/page
|
|
68
|
+
// WHATWG URL: userinfo="trusted-domain", hostname="attacker.com"
|
|
69
|
+
// hostname !== base.hostname → rejected.
|
|
70
|
+
const bypassUrl = `https://${TEST_BASE_URL.replace("https://", "")}@attacker.com/page`;
|
|
71
|
+
await expect(
|
|
72
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
73
|
+
buildSinglePageAuditInput({ url: bypassUrl }, CONTENT_ROOT),
|
|
74
|
+
),
|
|
75
|
+
).rejects.toThrow(/out of scope|use `ailk audit`/i);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("rejects a malformed URL", async () => {
|
|
79
|
+
await expect(
|
|
80
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
81
|
+
buildSinglePageAuditInput({ url: "not-a-url" }, CONTENT_ROOT),
|
|
82
|
+
),
|
|
83
|
+
).rejects.toThrow();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("rejects input with both slug and url provided", async () => {
|
|
87
|
+
await expect(
|
|
88
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
89
|
+
buildSinglePageAuditInput(
|
|
90
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
91
|
+
{
|
|
92
|
+
slug: "home",
|
|
93
|
+
url: `${TEST_BASE_URL}/HomePage/home`,
|
|
94
|
+
} as unknown as Parameters<typeof buildSinglePageAuditInput>[0],
|
|
95
|
+
CONTENT_ROOT,
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
).rejects.toThrow(/both|exclusive/i);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects input with neither slug nor url", async () => {
|
|
102
|
+
await expect(
|
|
103
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
104
|
+
buildSinglePageAuditInput(
|
|
105
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
106
|
+
{} as unknown as Parameters<typeof buildSinglePageAuditInput>[0],
|
|
107
|
+
CONTENT_ROOT,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
).rejects.toThrow(/slug.*url|url.*slug|required/i);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("accepts same-origin URL with matching hostname", async () => {
|
|
114
|
+
await expect(
|
|
115
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
116
|
+
buildSinglePageAuditInput(
|
|
117
|
+
{ url: `${TEST_BASE_URL}/HomePage/home` },
|
|
118
|
+
CONTENT_ROOT,
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
).resolves.toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("strips query string from same-origin URL before slug extraction", async () => {
|
|
125
|
+
const result = await withBaseUrl(TEST_BASE_URL, () =>
|
|
126
|
+
buildSinglePageAuditInput(
|
|
127
|
+
{ url: `${TEST_BASE_URL}/HomePage/home?utm_source=test` },
|
|
128
|
+
CONTENT_ROOT,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
expect(result).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("strips fragment from same-origin URL before slug extraction", async () => {
|
|
135
|
+
const result = await withBaseUrl(TEST_BASE_URL, () =>
|
|
136
|
+
buildSinglePageAuditInput(
|
|
137
|
+
{ url: `${TEST_BASE_URL}/HomePage/home#section` },
|
|
138
|
+
CONTENT_ROOT,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
expect(result).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── Slug happy-path tests ─────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
describe("buildSinglePageAuditInput — slug input", () => {
|
|
148
|
+
it("returns AuditInput with one page for a known HomePage slug", async () => {
|
|
149
|
+
const result = await buildSinglePageAuditInput(
|
|
150
|
+
{ slug: "home", type: "HomePage" },
|
|
151
|
+
CONTENT_ROOT,
|
|
152
|
+
);
|
|
153
|
+
expect(result.pages).toHaveLength(1);
|
|
154
|
+
expect(result.pages[0].slug).toBe("home");
|
|
155
|
+
expect(result.pages[0].type).toBe("HomePage");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("result has locale, identityMode, registry, jsonLdByPage fields", async () => {
|
|
159
|
+
const result = await buildSinglePageAuditInput(
|
|
160
|
+
{ slug: "home", type: "HomePage" },
|
|
161
|
+
CONTENT_ROOT,
|
|
162
|
+
);
|
|
163
|
+
expect(typeof result.locale).toBe("string");
|
|
164
|
+
expect(
|
|
165
|
+
result.identityMode === "person" ||
|
|
166
|
+
result.identityMode === "organization",
|
|
167
|
+
).toBe(true);
|
|
168
|
+
expect(Array.isArray(result.registry)).toBe(true);
|
|
169
|
+
expect(typeof result.jsonLdByPage).toBe("object");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("throws for a slug that does not exist in the content directory", async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
buildSinglePageAuditInput(
|
|
175
|
+
{ slug: "does-not-exist-at-all", type: "HomePage" },
|
|
176
|
+
CONTENT_ROOT,
|
|
177
|
+
),
|
|
178
|
+
).rejects.toThrow(/not found|does not exist/i);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── URL happy-path tests ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("buildSinglePageAuditInput — url input (same-origin)", () => {
|
|
185
|
+
it("returns AuditInput for a same-origin URL pointing at a known page", async () => {
|
|
186
|
+
const result = (await withBaseUrl(TEST_BASE_URL, () =>
|
|
187
|
+
buildSinglePageAuditInput(
|
|
188
|
+
{ url: `${TEST_BASE_URL}/HomePage/home` },
|
|
189
|
+
CONTENT_ROOT,
|
|
190
|
+
),
|
|
191
|
+
)) as Awaited<ReturnType<typeof buildSinglePageAuditInput>>;
|
|
192
|
+
expect(result.pages).toHaveLength(1);
|
|
193
|
+
expect(result.pages[0].slug).toBe("home");
|
|
194
|
+
expect(result.pages[0].type).toBe("HomePage");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── Structural guard ──────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe("structural guard — no fetch()", () => {
|
|
201
|
+
it("apps/mcp/src/lib/audit-single-page.ts does not call fetch()", () => {
|
|
202
|
+
const src = readFileSync(
|
|
203
|
+
resolve(__dirname, "../src/lib/audit-single-page.ts"),
|
|
204
|
+
"utf-8",
|
|
205
|
+
);
|
|
206
|
+
// Strip block comments (/** ... */ and /* ... */) and line comments (// ...)
|
|
207
|
+
// before checking — comments may mention fetch() as a negative assertion.
|
|
208
|
+
const codeOnly = src
|
|
209
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
210
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
211
|
+
expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the audit MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - auditInputSchema validates slug input (with regex enforcement)
|
|
6
|
+
* - auditInputSchema validates url input
|
|
7
|
+
* - auditInputSchema rejects invalid slugs (uppercase, slashes, dots)
|
|
8
|
+
* - ailkAudit returns score + perRule + perPage + meta for a slug input
|
|
9
|
+
* - ailkAudit returns an error result for a cross-domain URL
|
|
10
|
+
* - Output validates against auditOutputSchema
|
|
11
|
+
* - No console.* calls in handler (structural test)
|
|
12
|
+
* - No fetch() call in tool file (structural test)
|
|
13
|
+
*
|
|
14
|
+
* Phase 3 U0 (#294): adds a sanity-check that purchaseInputSchema relocated
|
|
15
|
+
* to @working-theory/validation (per ADR 0013) is reachable and parses the same input
|
|
16
|
+
* shape the inline schema parsed pre-relocation.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { resolve, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
auditInputSchema,
|
|
24
|
+
auditOutputSchema,
|
|
25
|
+
purchaseInputSchema,
|
|
26
|
+
} from "@working-theory/validation";
|
|
27
|
+
|
|
28
|
+
import { ailkAudit } from "../src/tools/audit.js";
|
|
29
|
+
|
|
30
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
31
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
32
|
+
const TEST_BASE_URL = "https://example.ailk.test";
|
|
33
|
+
|
|
34
|
+
function withBaseUrl(
|
|
35
|
+
url: string,
|
|
36
|
+
fn: () => Promise<unknown>,
|
|
37
|
+
): Promise<unknown> {
|
|
38
|
+
const prev = process.env.AILK_BASE_URL;
|
|
39
|
+
process.env.AILK_BASE_URL = url;
|
|
40
|
+
return fn().finally(() => {
|
|
41
|
+
if (prev === undefined) delete process.env.AILK_BASE_URL;
|
|
42
|
+
else process.env.AILK_BASE_URL = prev;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Input schema tests ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("auditInputSchema", () => {
|
|
49
|
+
it("accepts a valid slug", () => {
|
|
50
|
+
const result = auditInputSchema.safeParse({
|
|
51
|
+
slug: "home",
|
|
52
|
+
type: "HomePage",
|
|
53
|
+
});
|
|
54
|
+
expect(result.success).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("accepts slug without explicit type", () => {
|
|
58
|
+
const result = auditInputSchema.safeParse({ slug: "home" });
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("accepts a url string", () => {
|
|
63
|
+
const result = auditInputSchema.safeParse({
|
|
64
|
+
url: "https://example.com/page",
|
|
65
|
+
});
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects slug with uppercase letters", () => {
|
|
70
|
+
const result = auditInputSchema.safeParse({ slug: "LaunchingAilk" });
|
|
71
|
+
expect(result.success).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects slug with forward slash (path traversal guard)", () => {
|
|
75
|
+
const result = auditInputSchema.safeParse({ slug: "some/path" });
|
|
76
|
+
expect(result.success).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects slug with dots (path traversal guard)", () => {
|
|
80
|
+
const result = auditInputSchema.safeParse({ slug: "../etc" });
|
|
81
|
+
expect(result.success).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects empty slug", () => {
|
|
85
|
+
const result = auditInputSchema.safeParse({ slug: "" });
|
|
86
|
+
expect(result.success).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects non-url string in url field", () => {
|
|
90
|
+
const result = auditInputSchema.safeParse({ url: "not-a-url" });
|
|
91
|
+
expect(result.success).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects empty object (neither slug nor url)", () => {
|
|
95
|
+
const result = auditInputSchema.safeParse({});
|
|
96
|
+
expect(result.success).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Handler happy-path tests ──────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe("ailkAudit handler — slug input", () => {
|
|
103
|
+
it("returns score, perRule, perPage, meta for a known HomePage slug", async () => {
|
|
104
|
+
const result = await ailkAudit(
|
|
105
|
+
{ slug: "home", type: "HomePage" },
|
|
106
|
+
CONTENT_ROOT,
|
|
107
|
+
);
|
|
108
|
+
expect(typeof result.score).toBe("number");
|
|
109
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
110
|
+
expect(result.score).toBeLessThanOrEqual(100);
|
|
111
|
+
expect(Array.isArray(result.perRule)).toBe(true);
|
|
112
|
+
expect(Array.isArray(result.perPage)).toBe(true);
|
|
113
|
+
expect(typeof result.meta).toBe("object");
|
|
114
|
+
expect(typeof result.meta.auditedAt).toBe("string");
|
|
115
|
+
expect(typeof result.meta.ruleSet).toBe("string");
|
|
116
|
+
expect(typeof result.meta.locale).toBe("string");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("perRule entries have ruleId, name, weight, status fields", async () => {
|
|
120
|
+
const result = await ailkAudit(
|
|
121
|
+
{ slug: "home", type: "HomePage" },
|
|
122
|
+
CONTENT_ROOT,
|
|
123
|
+
);
|
|
124
|
+
for (const rule of result.perRule) {
|
|
125
|
+
expect(typeof rule.ruleId).toBe("string");
|
|
126
|
+
expect(typeof rule.name).toBe("string");
|
|
127
|
+
expect(typeof rule.weight).toBe("number");
|
|
128
|
+
expect(rule.status === "pass" || rule.status === "fail").toBe(true);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("perPage entries have slug and type fields", async () => {
|
|
133
|
+
const result = await ailkAudit(
|
|
134
|
+
{ slug: "home", type: "HomePage" },
|
|
135
|
+
CONTENT_ROOT,
|
|
136
|
+
);
|
|
137
|
+
expect(result.perPage).toHaveLength(1);
|
|
138
|
+
expect(result.perPage[0].slug).toBe("home");
|
|
139
|
+
expect(result.perPage[0].type).toBe("HomePage");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("output validates against auditOutputSchema", async () => {
|
|
143
|
+
const result = await ailkAudit(
|
|
144
|
+
{ slug: "home", type: "HomePage" },
|
|
145
|
+
CONTENT_ROOT,
|
|
146
|
+
);
|
|
147
|
+
const parsed = auditOutputSchema.safeParse(result);
|
|
148
|
+
expect(parsed.success).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("ailkAudit handler — url input (same-origin)", () => {
|
|
153
|
+
it("returns audit result for a same-origin URL", async () => {
|
|
154
|
+
const result = (await withBaseUrl(TEST_BASE_URL, () =>
|
|
155
|
+
ailkAudit(
|
|
156
|
+
{ url: `${TEST_BASE_URL}/HomePage/home` },
|
|
157
|
+
CONTENT_ROOT,
|
|
158
|
+
),
|
|
159
|
+
)) as Awaited<ReturnType<typeof ailkAudit>>;
|
|
160
|
+
expect(typeof result.score).toBe("number");
|
|
161
|
+
expect(result.perPage[0].slug).toBe("home");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("ailkAudit handler — cross-domain URL", () => {
|
|
166
|
+
it("throws for a cross-domain URL (no fetch attempted)", async () => {
|
|
167
|
+
await expect(
|
|
168
|
+
withBaseUrl(TEST_BASE_URL, () =>
|
|
169
|
+
ailkAudit(
|
|
170
|
+
{ url: "https://evil.example.com/HomePage/page" },
|
|
171
|
+
CONTENT_ROOT,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
).rejects.toThrow(/out of scope|use `ailk audit`/i);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── Structural guards ─────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe("structural guards", () => {
|
|
181
|
+
it("apps/mcp/src/tools/audit.ts does not call console.*", () => {
|
|
182
|
+
const src = readFileSync(
|
|
183
|
+
resolve(__dirname, "../src/tools/audit.ts"),
|
|
184
|
+
"utf-8",
|
|
185
|
+
);
|
|
186
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("apps/mcp/src/tools/audit.ts does not call fetch()", () => {
|
|
190
|
+
const src = readFileSync(
|
|
191
|
+
resolve(__dirname, "../src/tools/audit.ts"),
|
|
192
|
+
"utf-8",
|
|
193
|
+
);
|
|
194
|
+
// Strip comments before checking — JSDoc may mention fetch() as a negative assertion.
|
|
195
|
+
const codeOnly = src
|
|
196
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
197
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
198
|
+
expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ── Schema relocation parity (Phase 3 U0 / ADR 0013) ──────────────────────────
|
|
203
|
+
|
|
204
|
+
describe("purchaseInputSchema relocated to @working-theory/validation", () => {
|
|
205
|
+
it("parses the same shape the inline schema parsed pre-relocation", () => {
|
|
206
|
+
const result = purchaseInputSchema.safeParse({
|
|
207
|
+
priceId: "price_test_123",
|
|
208
|
+
quantity: 2,
|
|
209
|
+
});
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("rejects an empty priceId", () => {
|
|
214
|
+
const result = purchaseInputSchema.safeParse({ priceId: "" });
|
|
215
|
+
expect(result.success).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift-detection test (issue #209): the MCP server's registered tool list
|
|
3
|
+
* MUST equal the canonical catalog from @working-theory/metadata.
|
|
4
|
+
*
|
|
5
|
+
* Without this, two surfaces (apps/web manifest + apps/mcp server) could drift
|
|
6
|
+
* on tool name or description and only an integration test on a real MCP
|
|
7
|
+
* client would catch it. This test forces them in lock-step.
|
|
8
|
+
*
|
|
9
|
+
* What this asserts:
|
|
10
|
+
* - Tool names registered on the McpServer == catalog tool names
|
|
11
|
+
* - Tool descriptions registered on the McpServer == catalog descriptions
|
|
12
|
+
*
|
|
13
|
+
* What this does NOT assert:
|
|
14
|
+
* - inputSchema equality — the server registers Zod schemas; the catalog
|
|
15
|
+
* publishes JSON-Schema descriptors. Same source upstream
|
|
16
|
+
* (packages/validation), but distinct shapes downstream by design.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { getMcpToolCatalog } from "@working-theory/metadata";
|
|
20
|
+
|
|
21
|
+
import { createMcpServer } from "../src/server.js";
|
|
22
|
+
|
|
23
|
+
interface RegisteredTool {
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("MCP catalog ↔ server drift", () => {
|
|
28
|
+
it("registered tool names equal catalog tool names", () => {
|
|
29
|
+
const server = createMcpServer();
|
|
30
|
+
const registered = (
|
|
31
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
32
|
+
server as unknown as { _registeredTools: Record<string, RegisteredTool> }
|
|
33
|
+
)._registeredTools;
|
|
34
|
+
const registeredNames = Object.keys(registered).sort();
|
|
35
|
+
const catalogNames = getMcpToolCatalog()
|
|
36
|
+
.map((t) => t.name)
|
|
37
|
+
.sort();
|
|
38
|
+
expect(registeredNames).toEqual(catalogNames);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("registered tool descriptions equal catalog descriptions", () => {
|
|
42
|
+
const server = createMcpServer();
|
|
43
|
+
const registered = (
|
|
44
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
45
|
+
server as unknown as { _registeredTools: Record<string, RegisteredTool> }
|
|
46
|
+
)._registeredTools;
|
|
47
|
+
for (const entry of getMcpToolCatalog()) {
|
|
48
|
+
expect(registered[entry.name]).toBeDefined();
|
|
49
|
+
expect(registered[entry.name]!.description).toBe(entry.description);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|