create-ailk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -10
- package/component-catalog.json +520 -5
- package/component-catalog.md +9 -6
- package/dist/cli.js +22 -24
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/parse-args.d.ts +78 -10
- package/dist/parse-args.js +204 -14
- package/dist/programmatic.d.ts +39 -9
- package/dist/programmatic.js +163 -25
- package/dist/site-roster.js +11 -6
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.js +102 -43
- package/dist/template-manifest.d.ts +36 -17
- package/dist/template-manifest.js +121 -367
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +3 -3
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/standard/DESIGN.md +5 -0
- package/site-templates/marketing-site/DESIGN.md +5 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
- package/site-templates/marketing-site-free/DESIGN.md +5 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/site-templates/saas-product/DESIGN.md +5 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-page/references/page-types.md +4 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/DESIGN.md +5 -0
- package/templates/README.md +46 -38
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +7 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/layout.tsx +37 -5
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
- package/templates/apps/web/lib/nav-config.ts +48 -36
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/messages/en.json +28 -2
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/ContactPage/contact.mdx +14 -44
- package/templates/content/en/HomePage/home.mdx +8 -16
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
|
@@ -103,7 +103,6 @@ const MCP_ONLY_TOOLS = new Set([
|
|
|
103
103
|
"ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
|
|
104
104
|
"ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
|
|
105
105
|
"ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
|
|
106
|
-
"ailkSubscribeNewsletter", // subscribe_newsletter — anonymous write via MCP; no Server Action (B7)
|
|
107
106
|
"ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
|
|
108
107
|
"ailkCreateProject", // create_project — agent-only project creator; no Server Action equivalent (#299)
|
|
109
108
|
"ailkGetFlow", // get_flow — agent-callable flow-engine read; no web Server Action (#3956 D7)
|
|
@@ -228,6 +227,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
|
|
|
228
227
|
// ailkSubmitFlowStep): twenty tools + three paired Server Actions.
|
|
229
228
|
// - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
|
|
230
229
|
// - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
|
|
230
|
+
// - ailkSubscribeNewsletter: paired with subscribeNewsletterAction
|
|
231
|
+
// (apps/web/app/[locale]/newsletter/newsletter.actions.ts, #6730 D9).
|
|
231
232
|
// - ailkSubmitFlowStep: paired with submitFlowStepAction
|
|
232
233
|
// (apps/web/app/[locale]/flows/[slug]/flow.actions.ts, #3957).
|
|
233
234
|
// - startCheckoutAction, saveRecommendationAction: WEB-ONLY (#4842 D7,
|
|
@@ -241,8 +242,8 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
|
|
|
241
242
|
// ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
|
|
242
243
|
// MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
|
|
243
244
|
// - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
|
|
244
|
-
// -
|
|
245
|
-
//
|
|
245
|
+
// - ailkRequestCallback: MCP-only anonymous-write tool (B7; in
|
|
246
|
+
// MCP_ONLY_TOOLS allow-list).
|
|
246
247
|
// - ailkCreateProject: MCP-only agent project creator (#299; in MCP_ONLY_TOOLS).
|
|
247
248
|
// - ailkCreatePage, ailkUpdatePage, ailkDeletePage: RETIRED by #5853
|
|
248
249
|
// (D7). Their HTTP twins — the untenanted /v1/content writes — were
|
|
@@ -264,7 +265,7 @@ describe("MCP ↔ Server-Action parity (filesystem)", () => {
|
|
|
264
265
|
// - ailkGetEventMeta: MCP-only meta-only scheduling read, twin of
|
|
265
266
|
// GET /v1/schedule/event-meta (#4633) — same posture as the other
|
|
266
267
|
// scheduling tools.
|
|
267
|
-
expect(actions.size).toBe(
|
|
268
|
+
expect(actions.size).toBe(6);
|
|
268
269
|
expect(tools.size).toBe(20);
|
|
269
270
|
expect(r.missingTools).toEqual([]);
|
|
270
271
|
expect(r.orphanTools).toEqual([]);
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* correctly in production and in integration tests.
|
|
17
17
|
*
|
|
18
18
|
* Input: { targetDir, projectName, preset? }
|
|
19
|
+
* The tool builds the Free organization-product starter (`--shape
|
|
20
|
+
* organization-product`, #6754 D9); `preset` stays reserved. Always builds
|
|
21
|
+
* the Free module set (`--modules marketing`, #6759 D6, SEC-7): a licence
|
|
22
|
+
* key sitting in the MCP server's own environment never widens what an
|
|
23
|
+
* anonymous caller receives — the explicit keep list takes precedence over
|
|
24
|
+
* any key-derived level probe before the child process reads its env.
|
|
19
25
|
* targetDir — absolute or relative path to scaffold into; must not exist
|
|
20
26
|
* or must be empty (enforced by create-ailk ValidationError)
|
|
21
27
|
* projectName — project name used in README and package.json
|
|
@@ -94,12 +100,21 @@ export async function ailkCreateProject(
|
|
|
94
100
|
const targetAbs = resolve(input.targetDir);
|
|
95
101
|
|
|
96
102
|
// Spawn the CLI synchronously — no network, no async I/O needed.
|
|
97
|
-
// Hard-coded node invocation; only targetDir flows through user input.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
// Hard-coded node invocation; only targetDir flows through user input. The
|
|
104
|
+
// CLI refuses a run naming no shape and no template, so the tool names one.
|
|
105
|
+
// `--modules marketing` is pinned explicitly (#6759 D6, SEC-7): an explicit
|
|
106
|
+
// keep list takes the keep branch and resolves before the child ever reads
|
|
107
|
+
// AILK_LICENSE_KEY, so a licence key in the server's own environment can
|
|
108
|
+
// never turn this anonymous call into the keyed level probe.
|
|
109
|
+
const result = spawnSync(
|
|
110
|
+
"node",
|
|
111
|
+
[bin, targetAbs, "--shape", "organization-product", "--modules", "marketing"],
|
|
112
|
+
{
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
shell: false,
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
},
|
|
117
|
+
);
|
|
103
118
|
|
|
104
119
|
if (result.status !== 0) {
|
|
105
120
|
// Extract the error message from stderr (CLI writes "[create-ailk] <msg>").
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* conversation logic, only:
|
|
9
9
|
*
|
|
10
10
|
* 1. Submission — `onComplete` hands the finished answers to the SAME
|
|
11
|
-
* `captureLeadAction` (`./contact.actions.ts`) the
|
|
12
|
-
* already uses, built as a `FormData` (imperatively, not a real
|
|
11
|
+
* `captureLeadAction` (`./contact.actions.ts`) the shared
|
|
12
|
+
* `LeadCaptureForm` on `/contact` already uses, built as a `FormData` (imperatively, not a real
|
|
13
13
|
* `<form>` submit — React 19 Server Actions are plain async functions).
|
|
14
14
|
* That action posts through the EXISTING `POST /v1/leads/submit` route
|
|
15
15
|
* (`apps/api/src/routes/leads`, consumed via `@working-theory/api-client`,
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* second web surface, not a new one. `contact.actions.ts`'s own
|
|
22
22
|
* doc-comment covers the additive `source` FormData field this file
|
|
23
23
|
* sets to distinguish intake-originated leads.
|
|
24
|
-
* 2. Skip fallback — `onSkip` swaps to the
|
|
24
|
+
* 2. Skip fallback — `onSkip` swaps to the shared `LeadCaptureForm` block
|
|
25
|
+
* (#6730 — the one contact form), bound to the same action,
|
|
25
26
|
* carrying over whatever answers were already captured as its
|
|
26
27
|
* `initialValues` prefill. A failed auto-submit on `onComplete` falls
|
|
27
28
|
* back the same way, so a lead is never silently lost.
|
|
@@ -43,14 +44,16 @@ import {
|
|
|
43
44
|
ConversationalIntake,
|
|
44
45
|
type ConversationalIntakeProps,
|
|
45
46
|
InlineMessage,
|
|
47
|
+
LeadCaptureForm,
|
|
48
|
+
localeHref,
|
|
46
49
|
THINKING_DELAY_MAX_MS,
|
|
47
50
|
THINKING_DELAY_MIN_MS,
|
|
48
51
|
} from "@working-theory/ui";
|
|
49
52
|
import { useLocale, useTranslations } from "next-intl";
|
|
50
53
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
51
54
|
|
|
55
|
+
import { contactFormText } from "./contact-form-text";
|
|
52
56
|
import { captureLeadAction } from "./contact.actions";
|
|
53
|
-
import { ContactForm, type ContactFormInitialValues } from "./ContactForm";
|
|
54
57
|
import { recordIntakeTurn } from "./intake-analytics";
|
|
55
58
|
import { buildContactIntakeScript } from "./intake-script";
|
|
56
59
|
|
|
@@ -120,7 +123,7 @@ export function ConversationalIntakeWrapper({
|
|
|
120
123
|
);
|
|
121
124
|
|
|
122
125
|
const [surface, setSurface] = useState<Surface>("intake");
|
|
123
|
-
const [initialValues, setInitialValues] = useState<
|
|
126
|
+
const [initialValues, setInitialValues] = useState<Record<string, string>>(
|
|
124
127
|
{},
|
|
125
128
|
);
|
|
126
129
|
const [submitError, setSubmitError] = useState<string | undefined>(
|
|
@@ -193,7 +196,12 @@ export function ConversationalIntakeWrapper({
|
|
|
193
196
|
{submitError}
|
|
194
197
|
</InlineMessage>
|
|
195
198
|
) : null}
|
|
196
|
-
<
|
|
199
|
+
<LeadCaptureForm
|
|
200
|
+
action={captureLeadAction}
|
|
201
|
+
text={contactFormText(t)}
|
|
202
|
+
termsHref={localeHref(locale, "/terms")}
|
|
203
|
+
initialValues={initialValues}
|
|
204
|
+
/>
|
|
197
205
|
</Container>
|
|
198
206
|
);
|
|
199
207
|
}
|
package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* the EXISTING lead-capture endpoint. Only the outermost network
|
|
7
7
|
* boundary is mocked (`@working-theory/api-client`'s `leads.submit`) —
|
|
8
8
|
* the REAL `ConversationalIntake` block (#4543) and its engine (#4532),
|
|
9
|
-
* the REAL `captureLeadAction` (the SAME action
|
|
10
|
-
* uses), and the REAL `submitLeadInputSchema` validation all run, proving
|
|
9
|
+
* the REAL `captureLeadAction` (the SAME action the shared
|
|
10
|
+
* `LeadCaptureForm` on `/contact` uses), and the REAL `submitLeadInputSchema` validation all run, proving
|
|
11
11
|
* the whole chain end to end. No file under apps/api is imported, mocked,
|
|
12
12
|
* or touched by this test or by the wiring it exercises.
|
|
13
13
|
* AC2 — Skip renders the existing form, carrying over answers already
|
|
@@ -36,13 +36,14 @@ jest.mock("@working-theory/analytics/server", () => ({
|
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
38
|
// ConversationalIntakeWrapper's own responsibility under test here is the
|
|
39
|
-
// submission/skip/analytics wiring around the block — not
|
|
40
|
-
// rendering, which
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
jest.mock("
|
|
45
|
-
|
|
39
|
+
// submission/skip/analytics wiring around the block — not the shared
|
|
40
|
+
// LeadCaptureForm's own rendering, which its block suite covers
|
|
41
|
+
// (packages/ui/src/blocks/__tests__/LeadCaptureForm.test.tsx). The real
|
|
42
|
+
// `@working-theory/ui` runs (the ConversationalIntake block included); only
|
|
43
|
+
// the form block is stubbed, so the carried-over `initialValues` can be read.
|
|
44
|
+
jest.mock("@working-theory/ui", () => ({
|
|
45
|
+
...jest.requireActual("@working-theory/ui"),
|
|
46
|
+
LeadCaptureForm: (props: { initialValues?: Record<string, string> }) =>
|
|
46
47
|
require("react").createElement("div", {
|
|
47
48
|
"data-testid": "contact-form-stub",
|
|
48
49
|
"data-initial-values": JSON.stringify(props.initialValues ?? {}),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contactFormText (#6730 D8) — the `contact` namespace mapped onto the shared
|
|
3
|
+
* form block's copy, with `unavailableBody`'s `{email}` placeholder kept for
|
|
4
|
+
* the block to fill.
|
|
5
|
+
*/
|
|
6
|
+
import messages from "../../../../messages/en.json";
|
|
7
|
+
import { contactFormText } from "../contact-form-text";
|
|
8
|
+
|
|
9
|
+
/** A translator over the English catalog that interpolates `{name}` values, as ICU does. */
|
|
10
|
+
function translator(key: string, values?: Record<string, string>): string {
|
|
11
|
+
const template = (messages.contact as Record<string, unknown>)[key];
|
|
12
|
+
if (typeof template !== "string") throw new Error(`missing contact.${key}`);
|
|
13
|
+
return template.replace(/\{(\w+)\}/g, (m, name: string) => values?.[name] ?? m);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("contactFormText", () => {
|
|
17
|
+
it("maps every block string from the English catalog, keeping {email} for the block", () => {
|
|
18
|
+
expect(contactFormText(translator)).toEqual({
|
|
19
|
+
nameLabel: "Name",
|
|
20
|
+
emailLabel: "Email",
|
|
21
|
+
messageLabel: "Message",
|
|
22
|
+
submit: "Send message",
|
|
23
|
+
sending: "Sending…",
|
|
24
|
+
success: messages.contact.successMessage,
|
|
25
|
+
genericError: messages.contact.genericError,
|
|
26
|
+
consentStatement: messages.contact.consentStatement,
|
|
27
|
+
unavailableHeading: messages.contact.unavailableHeading,
|
|
28
|
+
unavailableBody: "Email us at {email} and we'll reply.",
|
|
29
|
+
unavailableNoEmail: messages.contact.unavailableNoEmail,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -179,3 +179,132 @@ describe("captureLeadAction — API error", () => {
|
|
|
179
179
|
consoleErrorSpy.mockRestore();
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
|
+
|
|
183
|
+
// ─── #6730 D8 — honest outcomes and declared extra fields ────────────────────
|
|
184
|
+
|
|
185
|
+
describe("captureLeadAction — outcome mapping (#6730)", () => {
|
|
186
|
+
it("503 lead_capture_not_configured → unavailable", async () => {
|
|
187
|
+
mockSubmit.mockResolvedValueOnce({
|
|
188
|
+
ok: false,
|
|
189
|
+
status: 503,
|
|
190
|
+
body: { error: "lead_capture_not_configured", message: "Lead capture is not configured." },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual({ kind: "unavailable" });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("503 delivery_failed → the generic error, never success", async () => {
|
|
199
|
+
mockSubmit.mockResolvedValueOnce({
|
|
200
|
+
ok: false,
|
|
201
|
+
status: 503,
|
|
202
|
+
body: { error: "delivery_failed", message: "The message could not be delivered." },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
206
|
+
|
|
207
|
+
expect(result).toMatchObject({ kind: "error" });
|
|
208
|
+
expect(result).not.toMatchObject({ kind: "success" });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it.each([400, 429, 500, 503])("a non-2xx (%s) is never success", async (status) => {
|
|
212
|
+
mockSubmit.mockResolvedValueOnce({ ok: false, status, body: { error: "x" } });
|
|
213
|
+
|
|
214
|
+
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
215
|
+
|
|
216
|
+
expect(result?.kind).toBe("error");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("201 → success", async () => {
|
|
220
|
+
mockSubmit.mockResolvedValueOnce({
|
|
221
|
+
ok: true,
|
|
222
|
+
body: { id: "email-1-abcdef12", persisted: "email" },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
|
|
226
|
+
|
|
227
|
+
expect(result).toEqual({ kind: "success" });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("captureLeadAction — declared extra fields (#6730 D8a)", () => {
|
|
232
|
+
it("carries an extra string field (e.g. a select) into payload", async () => {
|
|
233
|
+
mockSubmit.mockResolvedValueOnce({ ok: true, body: { id: "lead_1", persisted: "db" } });
|
|
234
|
+
|
|
235
|
+
await captureLeadAction(INITIAL_STATE, makeFormData({ ...VALID_FIELDS, plan: "pro" }));
|
|
236
|
+
|
|
237
|
+
const body = mockSubmit.mock.calls[0][0] as { payload: Record<string, unknown> };
|
|
238
|
+
expect(body.payload.plan).toBe("pro");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("never copies reserved or Next-internal fields into payload", async () => {
|
|
242
|
+
mockSubmit.mockResolvedValueOnce({ ok: true, body: { id: "lead_1", persisted: "db" } });
|
|
243
|
+
|
|
244
|
+
await captureLeadAction(
|
|
245
|
+
INITIAL_STATE,
|
|
246
|
+
makeFormData({
|
|
247
|
+
...VALID_FIELDS,
|
|
248
|
+
consent: "true",
|
|
249
|
+
ailk_hp: "",
|
|
250
|
+
source: "conversational-intake",
|
|
251
|
+
$ACTION_ID_abc: "",
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const body = mockSubmit.mock.calls[0][0] as {
|
|
256
|
+
source: string;
|
|
257
|
+
payload: Record<string, unknown>;
|
|
258
|
+
};
|
|
259
|
+
expect(Object.keys(body.payload).sort()).toEqual(["message", "name"]);
|
|
260
|
+
expect(body.source).toBe("conversational-intake");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("more than 20 extra keys → a form error, no API call", async () => {
|
|
264
|
+
const extras = Object.fromEntries(
|
|
265
|
+
Array.from({ length: 21 }, (_, i) => [`extra${i}`, "x"]),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const result = await captureLeadAction(
|
|
269
|
+
INITIAL_STATE,
|
|
270
|
+
makeFormData({ ...VALID_FIELDS, ...extras }),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(result).toMatchObject({ kind: "error" });
|
|
274
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("an extra value over 2000 characters → a form error, no API call", async () => {
|
|
278
|
+
const result = await captureLeadAction(
|
|
279
|
+
INITIAL_STATE,
|
|
280
|
+
makeFormData({ ...VALID_FIELDS, notes: "x".repeat(2001) }),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(result).toMatchObject({ kind: "error" });
|
|
284
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("captureLeadAction — honeypot (#6730)", () => {
|
|
289
|
+
it("a filled honeypot → success, and nothing is sent", async () => {
|
|
290
|
+
const result = await captureLeadAction(
|
|
291
|
+
INITIAL_STATE,
|
|
292
|
+
makeFormData({ ...VALID_FIELDS, ailk_hp: "https://spam.example" }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(result).toEqual({ kind: "success" });
|
|
296
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("a declared `website` field is an ordinary field, carried into payload", async () => {
|
|
300
|
+
mockSubmit.mockResolvedValueOnce({ ok: true, body: { id: "lead_1", persisted: "db" } });
|
|
301
|
+
|
|
302
|
+
await captureLeadAction(
|
|
303
|
+
INITIAL_STATE,
|
|
304
|
+
makeFormData({ ...VALID_FIELDS, website: "https://acme.example" }),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const body = mockSubmit.mock.calls[0][0] as { payload: Record<string, unknown> };
|
|
308
|
+
expect(body.payload.website).toBe("https://acme.example");
|
|
309
|
+
});
|
|
310
|
+
});
|