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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the create_page MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* D14a Stage 1 Unit C — Layer 1 / Layer 2 acceptance criteria mapped here:
|
|
5
|
+
* - Free tier → canonical payment_required body, no adapter call.
|
|
6
|
+
* - Bad frontmatter → validation_failed, no adapter call.
|
|
7
|
+
* - Slug-already-exists → conflict body with details.existingVersion echoed
|
|
8
|
+
* (so the agent can pivot to update without re-fetch).
|
|
9
|
+
* - Pro tier + valid input + new slug → { page, version } success body.
|
|
10
|
+
*
|
|
11
|
+
* Adapter is injected as the second argument so this unit test does not need
|
|
12
|
+
* Stage 1 Unit A's MDX implementation in place. The default adapter resolution
|
|
13
|
+
* (createMdxAdapter against process.cwd()/content) is exercised by integration
|
|
14
|
+
* tests once Unit A lands.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AdapterWriteError, type ContentAdapter } from "@working-theory/content-adapters";
|
|
18
|
+
|
|
19
|
+
const mockCheckLicense = jest.fn();
|
|
20
|
+
|
|
21
|
+
jest.mock("@working-theory/license", () => ({
|
|
22
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import { ailkCreatePage } from "../src/tools/create_page.js";
|
|
26
|
+
|
|
27
|
+
interface AdapterCalls {
|
|
28
|
+
createPage: jest.Mock;
|
|
29
|
+
getPageBySlug: jest.Mock;
|
|
30
|
+
getPageVersion: jest.Mock;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeAdapter(overrides: Partial<AdapterCalls> = {}): ContentAdapter {
|
|
34
|
+
const calls: AdapterCalls = {
|
|
35
|
+
createPage: jest.fn(),
|
|
36
|
+
getPageBySlug: jest.fn().mockResolvedValue(null),
|
|
37
|
+
getPageVersion: jest.fn(),
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
41
|
+
return {
|
|
42
|
+
getPageBySlug: calls.getPageBySlug,
|
|
43
|
+
listPages: jest.fn().mockResolvedValue([]),
|
|
44
|
+
getDetailBySlug: jest.fn().mockResolvedValue(null),
|
|
45
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
46
|
+
searchPages: jest.fn().mockResolvedValue([]),
|
|
47
|
+
extractStructuredData: jest.fn().mockResolvedValue(null),
|
|
48
|
+
createPage: calls.createPage,
|
|
49
|
+
updatePage: jest.fn(),
|
|
50
|
+
deletePage: jest.fn(),
|
|
51
|
+
getPageVersion: calls.getPageVersion,
|
|
52
|
+
} as unknown as ContentAdapter;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const VALID_INPUT = {
|
|
56
|
+
type: "Product" as const,
|
|
57
|
+
slug: "widget-pro",
|
|
58
|
+
page: {
|
|
59
|
+
title: "Widget Pro",
|
|
60
|
+
description: "The professional widget.",
|
|
61
|
+
},
|
|
62
|
+
body: "# Widget Pro\n\nProfessional-grade widget.",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("ailkCreatePage", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
mockCheckLicense.mockReset();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("license gating", () => {
|
|
71
|
+
it("returns canonical payment_required body for free tier; adapter not called", async () => {
|
|
72
|
+
mockCheckLicense.mockResolvedValue({
|
|
73
|
+
tier: "free",
|
|
74
|
+
plan: null,
|
|
75
|
+
features: [],
|
|
76
|
+
expiresAt: null,
|
|
77
|
+
revoked: false,
|
|
78
|
+
});
|
|
79
|
+
const adapter = makeAdapter();
|
|
80
|
+
|
|
81
|
+
const result = await ailkCreatePage(VALID_INPUT, adapter);
|
|
82
|
+
|
|
83
|
+
expect(result).toMatchObject({ error: "payment_required" });
|
|
84
|
+
expect(adapter.createPage).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("validation", () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
mockCheckLicense.mockResolvedValue({
|
|
91
|
+
tier: "pro",
|
|
92
|
+
plan: "pro",
|
|
93
|
+
features: [],
|
|
94
|
+
expiresAt: null,
|
|
95
|
+
revoked: false,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns validation_failed when frontmatter is missing required fields", async () => {
|
|
100
|
+
const adapter = makeAdapter();
|
|
101
|
+
const result = await ailkCreatePage(
|
|
102
|
+
{
|
|
103
|
+
...VALID_INPUT,
|
|
104
|
+
page: { title: "Only title" }, // missing description
|
|
105
|
+
},
|
|
106
|
+
adapter,
|
|
107
|
+
);
|
|
108
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
109
|
+
expect(adapter.createPage).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns validation_failed when body is empty", async () => {
|
|
113
|
+
const adapter = makeAdapter();
|
|
114
|
+
const result = await ailkCreatePage(
|
|
115
|
+
{ ...VALID_INPUT, body: "" },
|
|
116
|
+
adapter,
|
|
117
|
+
);
|
|
118
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
119
|
+
expect(adapter.createPage).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns validation_failed for unknown SchemaOrgType", async () => {
|
|
123
|
+
const adapter = makeAdapter();
|
|
124
|
+
const result = await ailkCreatePage(
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
+
{ ...VALID_INPUT, type: "NotAType" as any },
|
|
127
|
+
adapter,
|
|
128
|
+
);
|
|
129
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
130
|
+
expect(adapter.createPage).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("slug conflict", () => {
|
|
135
|
+
it("returns conflict with details.existingVersion when slug already exists", async () => {
|
|
136
|
+
mockCheckLicense.mockResolvedValue({
|
|
137
|
+
tier: "pro",
|
|
138
|
+
plan: "pro",
|
|
139
|
+
features: [],
|
|
140
|
+
expiresAt: null,
|
|
141
|
+
revoked: false,
|
|
142
|
+
});
|
|
143
|
+
const adapter = makeAdapter({
|
|
144
|
+
createPage: jest
|
|
145
|
+
.fn()
|
|
146
|
+
.mockRejectedValue(
|
|
147
|
+
new AdapterWriteError(
|
|
148
|
+
"conflict",
|
|
149
|
+
"Page already exists at Product/widget-pro.mdx",
|
|
150
|
+
{ version: "abc123" },
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await ailkCreatePage(VALID_INPUT, adapter);
|
|
156
|
+
|
|
157
|
+
expect(result).toMatchObject({
|
|
158
|
+
error: "conflict",
|
|
159
|
+
details: { version: "abc123" },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("happy path", () => {
|
|
165
|
+
it("returns { page, version } when adapter succeeds", async () => {
|
|
166
|
+
mockCheckLicense.mockResolvedValue({
|
|
167
|
+
tier: "pro",
|
|
168
|
+
plan: "pro",
|
|
169
|
+
features: [],
|
|
170
|
+
expiresAt: null,
|
|
171
|
+
revoked: false,
|
|
172
|
+
});
|
|
173
|
+
const persistedPage = {
|
|
174
|
+
type: "Product" as const,
|
|
175
|
+
slug: "widget-pro",
|
|
176
|
+
title: "Widget Pro",
|
|
177
|
+
description: "The professional widget.",
|
|
178
|
+
};
|
|
179
|
+
const adapter = makeAdapter({
|
|
180
|
+
createPage: jest
|
|
181
|
+
.fn()
|
|
182
|
+
.mockResolvedValue({ page: persistedPage, version: "v1" }),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await ailkCreatePage(VALID_INPUT, adapter);
|
|
186
|
+
|
|
187
|
+
expect(result).toEqual({ page: persistedPage, version: "v1" });
|
|
188
|
+
expect(adapter.createPage).toHaveBeenCalledTimes(1);
|
|
189
|
+
const callArgs = (adapter.createPage as jest.Mock).mock.calls[0];
|
|
190
|
+
expect(callArgs[0]).toBe("Product");
|
|
191
|
+
expect(callArgs[1]).toBe("widget-pro");
|
|
192
|
+
expect(callArgs[2]).toMatchObject({
|
|
193
|
+
type: "Product",
|
|
194
|
+
slug: "widget-pro",
|
|
195
|
+
title: "Widget Pro",
|
|
196
|
+
description: "The professional widget.",
|
|
197
|
+
body: "# Widget Pro\n\nProfessional-grade widget.",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the delete_page MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* D14a Stage 1 Unit C — Layer 1 / Layer 2 acceptance criteria mapped here:
|
|
5
|
+
* - Free tier → payment_required, no adapter call.
|
|
6
|
+
* - Missing ifMatch → validation_failed.
|
|
7
|
+
* - Stale ifMatch → conflict with details.currentVersion echoed.
|
|
8
|
+
* - Slug not found → not_found body.
|
|
9
|
+
* - Pro + matching ifMatch → { ok: true } success body.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AdapterWriteError, type ContentAdapter } from "@working-theory/content-adapters";
|
|
13
|
+
|
|
14
|
+
const mockCheckLicense = jest.fn();
|
|
15
|
+
|
|
16
|
+
jest.mock("@working-theory/license", () => ({
|
|
17
|
+
checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { ailkDeletePage } from "../src/tools/delete_page.js";
|
|
21
|
+
|
|
22
|
+
function makeAdapter(
|
|
23
|
+
overrides: Partial<{ deletePage: jest.Mock }> = {},
|
|
24
|
+
): ContentAdapter {
|
|
25
|
+
// eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
|
|
26
|
+
return {
|
|
27
|
+
getPageBySlug: jest.fn().mockResolvedValue(null),
|
|
28
|
+
listPages: jest.fn().mockResolvedValue([]),
|
|
29
|
+
getDetailBySlug: jest.fn().mockResolvedValue(null),
|
|
30
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
31
|
+
searchPages: jest.fn().mockResolvedValue([]),
|
|
32
|
+
extractStructuredData: jest.fn().mockResolvedValue(null),
|
|
33
|
+
createPage: jest.fn(),
|
|
34
|
+
updatePage: jest.fn(),
|
|
35
|
+
deletePage: overrides.deletePage ?? jest.fn().mockResolvedValue(undefined),
|
|
36
|
+
getPageVersion: jest.fn(),
|
|
37
|
+
} as unknown as ContentAdapter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const VALID_INPUT = {
|
|
41
|
+
type: "Product" as const,
|
|
42
|
+
slug: "widget-pro",
|
|
43
|
+
ifMatch: "v1",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("ailkDeletePage", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
mockCheckLicense.mockReset();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns payment_required for free tier; adapter not called", async () => {
|
|
52
|
+
mockCheckLicense.mockResolvedValue({
|
|
53
|
+
tier: "free",
|
|
54
|
+
plan: null,
|
|
55
|
+
features: [],
|
|
56
|
+
expiresAt: null,
|
|
57
|
+
revoked: false,
|
|
58
|
+
});
|
|
59
|
+
const adapter = makeAdapter();
|
|
60
|
+
|
|
61
|
+
const result = await ailkDeletePage(VALID_INPUT, adapter);
|
|
62
|
+
|
|
63
|
+
expect(result).toMatchObject({ error: "payment_required" });
|
|
64
|
+
expect(adapter.deletePage).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns validation_failed when slug violates SLUG_PATTERN (issue #398 — defence-in-depth parity with create/update)", async () => {
|
|
68
|
+
mockCheckLicense.mockResolvedValue({
|
|
69
|
+
tier: "pro",
|
|
70
|
+
plan: "pro",
|
|
71
|
+
features: [],
|
|
72
|
+
expiresAt: null,
|
|
73
|
+
revoked: false,
|
|
74
|
+
});
|
|
75
|
+
const adapter = makeAdapter();
|
|
76
|
+
|
|
77
|
+
const result = await ailkDeletePage(
|
|
78
|
+
{ ...VALID_INPUT, slug: "../etc/passwd" },
|
|
79
|
+
adapter,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
83
|
+
expect(adapter.deletePage).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns validation_failed when ifMatch is missing", async () => {
|
|
87
|
+
mockCheckLicense.mockResolvedValue({
|
|
88
|
+
tier: "pro",
|
|
89
|
+
plan: "pro",
|
|
90
|
+
features: [],
|
|
91
|
+
expiresAt: null,
|
|
92
|
+
revoked: false,
|
|
93
|
+
});
|
|
94
|
+
const adapter = makeAdapter();
|
|
95
|
+
const { ifMatch: _ignored, ...inputNoIfMatch } = VALID_INPUT;
|
|
96
|
+
|
|
97
|
+
const result = await ailkDeletePage(
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
inputNoIfMatch as any,
|
|
100
|
+
adapter,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result).toMatchObject({ error: "validation_failed" });
|
|
104
|
+
expect(adapter.deletePage).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns conflict on stale ifMatch", async () => {
|
|
108
|
+
mockCheckLicense.mockResolvedValue({
|
|
109
|
+
tier: "pro",
|
|
110
|
+
plan: "pro",
|
|
111
|
+
features: [],
|
|
112
|
+
expiresAt: null,
|
|
113
|
+
revoked: false,
|
|
114
|
+
});
|
|
115
|
+
const adapter = makeAdapter({
|
|
116
|
+
deletePage: jest
|
|
117
|
+
.fn()
|
|
118
|
+
.mockRejectedValue(
|
|
119
|
+
new AdapterWriteError(
|
|
120
|
+
"conflict",
|
|
121
|
+
"Version mismatch on Product/widget-pro.mdx",
|
|
122
|
+
{ currentVersion: "v9" },
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await ailkDeletePage(VALID_INPUT, adapter);
|
|
128
|
+
|
|
129
|
+
expect(result).toMatchObject({
|
|
130
|
+
error: "conflict",
|
|
131
|
+
details: { currentVersion: "v9" },
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns not_found when adapter signals missing slug", async () => {
|
|
136
|
+
mockCheckLicense.mockResolvedValue({
|
|
137
|
+
tier: "pro",
|
|
138
|
+
plan: "pro",
|
|
139
|
+
features: [],
|
|
140
|
+
expiresAt: null,
|
|
141
|
+
revoked: false,
|
|
142
|
+
});
|
|
143
|
+
const adapter = makeAdapter({
|
|
144
|
+
deletePage: jest
|
|
145
|
+
.fn()
|
|
146
|
+
.mockRejectedValue(
|
|
147
|
+
new AdapterWriteError(
|
|
148
|
+
"not_found",
|
|
149
|
+
"No page at Product/widget-pro.mdx",
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await ailkDeletePage(VALID_INPUT, adapter);
|
|
155
|
+
|
|
156
|
+
expect(result).toMatchObject({ error: "not_found" });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("returns { ok: true } on happy path", async () => {
|
|
160
|
+
mockCheckLicense.mockResolvedValue({
|
|
161
|
+
tier: "pro",
|
|
162
|
+
plan: "pro",
|
|
163
|
+
features: [],
|
|
164
|
+
expiresAt: null,
|
|
165
|
+
revoked: false,
|
|
166
|
+
});
|
|
167
|
+
const adapter = makeAdapter();
|
|
168
|
+
|
|
169
|
+
const result = await ailkDeletePage(VALID_INPUT, adapter);
|
|
170
|
+
|
|
171
|
+
expect(result).toEqual({ ok: true });
|
|
172
|
+
expect(adapter.deletePage).toHaveBeenCalledWith("Product", "widget-pro", {
|
|
173
|
+
ifMatch: "v1",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the get_business_hours MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - getBusinessHoursInputSchema validates correct inputs (slug optional)
|
|
6
|
+
* - getBusinessHoursInputSchema rejects invalid slugs
|
|
7
|
+
* - ailkGetBusinessHours returns null for a slug with no content
|
|
8
|
+
* - ailkGetBusinessHours output shape is correct when data exists
|
|
9
|
+
* - Output validates against getBusinessHoursOutputSchema
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ailkGetBusinessHours,
|
|
17
|
+
getBusinessHoursInputSchema,
|
|
18
|
+
getBusinessHoursOutputSchema,
|
|
19
|
+
} from "../src/tools/get_business_hours.js";
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
22
|
+
const CONTENT_ROOT = `${REPO_ROOT}/content`;
|
|
23
|
+
|
|
24
|
+
describe("getBusinessHoursInputSchema", () => {
|
|
25
|
+
it("accepts empty input (slug is optional)", () => {
|
|
26
|
+
const result = getBusinessHoursInputSchema.safeParse({});
|
|
27
|
+
expect(result.success).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("accepts a valid slug", () => {
|
|
31
|
+
const result = getBusinessHoursInputSchema.safeParse({ slug: "home" });
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rejects a slug with path traversal", () => {
|
|
36
|
+
const result = getBusinessHoursInputSchema.safeParse({
|
|
37
|
+
slug: "../etc/passwd",
|
|
38
|
+
});
|
|
39
|
+
expect(result.success).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects a slug with uppercase letters", () => {
|
|
43
|
+
const result = getBusinessHoursInputSchema.safeParse({ slug: "Home" });
|
|
44
|
+
expect(result.success).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects an empty slug string", () => {
|
|
48
|
+
const result = getBusinessHoursInputSchema.safeParse({ slug: "" });
|
|
49
|
+
expect(result.success).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("ailkGetBusinessHours handler", () => {
|
|
54
|
+
it("returns null for a slug with no matching content", async () => {
|
|
55
|
+
const result = await ailkGetBusinessHours(
|
|
56
|
+
{ slug: "nonexistent-slug-xqz" },
|
|
57
|
+
CONTENT_ROOT,
|
|
58
|
+
);
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns an object or null for default slug", async () => {
|
|
63
|
+
const result = await ailkGetBusinessHours({}, CONTENT_ROOT);
|
|
64
|
+
expect(result === null || typeof result === "object").toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("output validates against getBusinessHoursOutputSchema", async () => {
|
|
68
|
+
const result = await ailkGetBusinessHours({}, CONTENT_ROOT);
|
|
69
|
+
const parsed = getBusinessHoursOutputSchema.safeParse(result);
|
|
70
|
+
expect(parsed.success).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("architectural guard — no console calls", () => {
|
|
75
|
+
it("apps/mcp/src/tools/get_business_hours.ts does not call console.*", () => {
|
|
76
|
+
const src = readFileSync(
|
|
77
|
+
resolve(__dirname, "../src/tools/get_business_hours.ts"),
|
|
78
|
+
"utf-8",
|
|
79
|
+
);
|
|
80
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the get_faq MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - getFaqInputSchema validates correct inputs (slug optional)
|
|
6
|
+
* - getFaqInputSchema rejects invalid slugs
|
|
7
|
+
* - ailkGetFaq returns null for a slug with no content
|
|
8
|
+
* - ailkGetFaq output shape is correct when data exists
|
|
9
|
+
* - Output validates against getFaqOutputSchema
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ailkGetFaq,
|
|
17
|
+
getFaqInputSchema,
|
|
18
|
+
getFaqOutputSchema,
|
|
19
|
+
} from "../src/tools/get_faq.js";
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
22
|
+
const CONTENT_ROOT = `${REPO_ROOT}/content`;
|
|
23
|
+
|
|
24
|
+
describe("getFaqInputSchema", () => {
|
|
25
|
+
it("accepts empty input (slug is optional)", () => {
|
|
26
|
+
const result = getFaqInputSchema.safeParse({});
|
|
27
|
+
expect(result.success).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("accepts a valid slug", () => {
|
|
31
|
+
const result = getFaqInputSchema.safeParse({ slug: "faq" });
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rejects a slug with path traversal", () => {
|
|
36
|
+
const result = getFaqInputSchema.safeParse({ slug: "../etc/passwd" });
|
|
37
|
+
expect(result.success).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects a slug with uppercase letters", () => {
|
|
41
|
+
const result = getFaqInputSchema.safeParse({ slug: "FAQ" });
|
|
42
|
+
expect(result.success).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects an empty slug string", () => {
|
|
46
|
+
const result = getFaqInputSchema.safeParse({ slug: "" });
|
|
47
|
+
expect(result.success).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("ailkGetFaq handler", () => {
|
|
52
|
+
it("returns null for a slug with no matching content", async () => {
|
|
53
|
+
const result = await ailkGetFaq(
|
|
54
|
+
{ slug: "nonexistent-slug-xqz" },
|
|
55
|
+
CONTENT_ROOT,
|
|
56
|
+
);
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns an object or null for default slug", async () => {
|
|
61
|
+
const result = await ailkGetFaq({}, CONTENT_ROOT);
|
|
62
|
+
expect(result === null || typeof result === "object").toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("output validates against getFaqOutputSchema when non-null", async () => {
|
|
66
|
+
const result = await ailkGetFaq({}, CONTENT_ROOT);
|
|
67
|
+
const parsed = getFaqOutputSchema.safeParse(result);
|
|
68
|
+
expect(parsed.success).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("architectural guard — no console calls", () => {
|
|
73
|
+
it("apps/mcp/src/tools/get_faq.ts does not call console.*", () => {
|
|
74
|
+
const src = readFileSync(
|
|
75
|
+
resolve(__dirname, "../src/tools/get_faq.ts"),
|
|
76
|
+
"utf-8",
|
|
77
|
+
);
|
|
78
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the get_locations MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - getLocationsInputSchema validates correct inputs (no required fields)
|
|
6
|
+
* - ailkGetLocations returns an array (empty or populated)
|
|
7
|
+
* - Output shape has the correct fields when locations exist
|
|
8
|
+
* - Output validates against getLocationsOutputSchema
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ailkGetLocations,
|
|
16
|
+
getLocationsInputSchema,
|
|
17
|
+
getLocationsOutputSchema,
|
|
18
|
+
} from "../src/tools/get_locations.js";
|
|
19
|
+
|
|
20
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
21
|
+
const CONTENT_ROOT = `${REPO_ROOT}/content`;
|
|
22
|
+
|
|
23
|
+
describe("getLocationsInputSchema", () => {
|
|
24
|
+
it("accepts empty input", () => {
|
|
25
|
+
const result = getLocationsInputSchema.safeParse({});
|
|
26
|
+
expect(result.success).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects extra unknown fields (strict mode not applied — passthrough)", () => {
|
|
30
|
+
// Zod strips unknown fields by default (not strict); this should pass
|
|
31
|
+
const result = getLocationsInputSchema.safeParse({ unexpected: true });
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("ailkGetLocations handler", () => {
|
|
37
|
+
it("returns an array (may be empty if no Place content exists)", async () => {
|
|
38
|
+
const result = await ailkGetLocations({}, CONTENT_ROOT);
|
|
39
|
+
expect(Array.isArray(result.locations)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("location items have required shape fields when array is non-empty", async () => {
|
|
43
|
+
const result = await ailkGetLocations({}, CONTENT_ROOT);
|
|
44
|
+
for (const loc of result.locations) {
|
|
45
|
+
expect(typeof loc.name).toBe("string");
|
|
46
|
+
expect(typeof loc.address).toBe("object");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("output validates against getLocationsOutputSchema", async () => {
|
|
51
|
+
const result = await ailkGetLocations({}, CONTENT_ROOT);
|
|
52
|
+
const parsed = getLocationsOutputSchema.safeParse(result);
|
|
53
|
+
expect(parsed.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("architectural guard — no console calls", () => {
|
|
58
|
+
it("apps/mcp/src/tools/get_locations.ts does not call console.*", () => {
|
|
59
|
+
const src = readFileSync(
|
|
60
|
+
resolve(__dirname, "../src/tools/get_locations.ts"),
|
|
61
|
+
"utf-8",
|
|
62
|
+
);
|
|
63
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the get_offerings MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance criteria verified here:
|
|
5
|
+
* - getOfferingsInputSchema validates correct inputs (type optional: 'service'|'product')
|
|
6
|
+
* - getOfferingsInputSchema rejects invalid type values
|
|
7
|
+
* - ailkGetOfferings returns an array (empty or populated)
|
|
8
|
+
* - Output shape has the correct fields when offerings exist
|
|
9
|
+
* - Output validates against getOfferingsOutputSchema
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ailkGetOfferings,
|
|
17
|
+
getOfferingsInputSchema,
|
|
18
|
+
getOfferingsOutputSchema,
|
|
19
|
+
} from "../src/tools/get_offerings.js";
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
22
|
+
const CONTENT_ROOT = `${REPO_ROOT}/content`;
|
|
23
|
+
|
|
24
|
+
describe("getOfferingsInputSchema", () => {
|
|
25
|
+
it("accepts empty input (type is optional)", () => {
|
|
26
|
+
const result = getOfferingsInputSchema.safeParse({});
|
|
27
|
+
expect(result.success).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("accepts type: 'service'", () => {
|
|
31
|
+
const result = getOfferingsInputSchema.safeParse({ type: "service" });
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("accepts type: 'product'", () => {
|
|
36
|
+
const result = getOfferingsInputSchema.safeParse({ type: "product" });
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects an unknown type string", () => {
|
|
41
|
+
const result = getOfferingsInputSchema.safeParse({ type: "offer" });
|
|
42
|
+
expect(result.success).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects a numeric type", () => {
|
|
46
|
+
const result = getOfferingsInputSchema.safeParse({ type: 42 });
|
|
47
|
+
expect(result.success).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("ailkGetOfferings handler", () => {
|
|
52
|
+
it("returns an array (may be empty if no Service/Product content)", async () => {
|
|
53
|
+
const result = await ailkGetOfferings({}, CONTENT_ROOT);
|
|
54
|
+
expect(Array.isArray(result.offerings)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns only services when type is 'service'", async () => {
|
|
58
|
+
const result = await ailkGetOfferings({ type: "service" }, CONTENT_ROOT);
|
|
59
|
+
expect(Array.isArray(result.offerings)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns only products when type is 'product'", async () => {
|
|
63
|
+
const result = await ailkGetOfferings({ type: "product" }, CONTENT_ROOT);
|
|
64
|
+
expect(Array.isArray(result.offerings)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("offering items have required shape fields when array is non-empty", async () => {
|
|
68
|
+
const result = await ailkGetOfferings({}, CONTENT_ROOT);
|
|
69
|
+
for (const offering of result.offerings) {
|
|
70
|
+
expect(typeof offering.name).toBe("string");
|
|
71
|
+
expect(typeof offering.description).toBe("string");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("output validates against getOfferingsOutputSchema", async () => {
|
|
76
|
+
const result = await ailkGetOfferings({}, CONTENT_ROOT);
|
|
77
|
+
const parsed = getOfferingsOutputSchema.safeParse(result);
|
|
78
|
+
expect(parsed.success).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("architectural guard — no console calls", () => {
|
|
83
|
+
it("apps/mcp/src/tools/get_offerings.ts does not call console.*", () => {
|
|
84
|
+
const src = readFileSync(
|
|
85
|
+
resolve(__dirname, "../src/tools/get_offerings.ts"),
|
|
86
|
+
"utf-8",
|
|
87
|
+
);
|
|
88
|
+
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
89
|
+
});
|
|
90
|
+
});
|