create-ailk 0.18.0 → 0.19.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 +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +60 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +24 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the audit MCP tool handler.
|
|
3
|
-
*
|
|
4
|
-
* Acceptance criteria verified here:
|
|
5
|
-
* - auditInputSchema validates slug input (with regex enforcement)
|
|
6
|
-
* - auditInputSchema validates url input
|
|
7
|
-
* - auditInputSchema rejects invalid slugs (uppercase, slashes, dots)
|
|
8
|
-
* - ailkAudit returns score + perRule + perPage + meta for a slug input
|
|
9
|
-
* - ailkAudit returns an error result for a cross-domain URL
|
|
10
|
-
* - Output validates against auditOutputSchema
|
|
11
|
-
* - No console.* calls in handler (structural test)
|
|
12
|
-
* - No fetch() call in tool file (structural test)
|
|
13
|
-
*
|
|
14
|
-
* Phase 3 U0 (#294): adds a sanity-check that purchaseInputSchema relocated
|
|
15
|
-
* to @working-theory/validation (per ADR 0013) is reachable and parses the same input
|
|
16
|
-
* shape the inline schema parsed pre-relocation.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
mkdirSync,
|
|
21
|
-
mkdtempSync,
|
|
22
|
-
readFileSync,
|
|
23
|
-
rmSync,
|
|
24
|
-
writeFileSync,
|
|
25
|
-
} from "node:fs";
|
|
26
|
-
import { tmpdir } from "node:os";
|
|
27
|
-
import { resolve, join } from "node:path";
|
|
28
|
-
|
|
29
|
-
import { READINESS_SCAN_TYPES } from "@working-theory/aeo";
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
auditInputSchema,
|
|
33
|
-
auditOutputSchema,
|
|
34
|
-
purchaseInputSchema,
|
|
35
|
-
} from "@working-theory/validation";
|
|
36
|
-
|
|
37
|
-
import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
|
|
38
|
-
import { readStripeTaxSignal } from "../src/lib/stripe-tax-signal.js";
|
|
39
|
-
import { ailkAudit } from "../src/tools/audit.js";
|
|
40
|
-
|
|
41
|
-
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
42
|
-
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
43
|
-
const TEST_BASE_URL = "https://example.ailk.test";
|
|
44
|
-
|
|
45
|
-
function withBaseUrl(
|
|
46
|
-
url: string,
|
|
47
|
-
fn: () => Promise<unknown>,
|
|
48
|
-
): Promise<unknown> {
|
|
49
|
-
const prev = process.env.AILK_BASE_URL;
|
|
50
|
-
process.env.AILK_BASE_URL = url;
|
|
51
|
-
return fn().finally(() => {
|
|
52
|
-
if (prev === undefined) delete process.env.AILK_BASE_URL;
|
|
53
|
-
else process.env.AILK_BASE_URL = prev;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Input schema tests ────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
describe("auditInputSchema", () => {
|
|
60
|
-
it("accepts a valid slug", () => {
|
|
61
|
-
const result = auditInputSchema.safeParse({
|
|
62
|
-
slug: "home",
|
|
63
|
-
type: "HomePage",
|
|
64
|
-
});
|
|
65
|
-
expect(result.success).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("accepts slug without explicit type", () => {
|
|
69
|
-
const result = auditInputSchema.safeParse({ slug: "home" });
|
|
70
|
-
expect(result.success).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("accepts a url string", () => {
|
|
74
|
-
const result = auditInputSchema.safeParse({
|
|
75
|
-
url: "https://example.com/page",
|
|
76
|
-
});
|
|
77
|
-
expect(result.success).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("rejects slug with uppercase letters", () => {
|
|
81
|
-
const result = auditInputSchema.safeParse({ slug: "LaunchingAilk" });
|
|
82
|
-
expect(result.success).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("rejects slug with forward slash (path traversal guard)", () => {
|
|
86
|
-
const result = auditInputSchema.safeParse({ slug: "some/path" });
|
|
87
|
-
expect(result.success).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("rejects slug with dots (path traversal guard)", () => {
|
|
91
|
-
const result = auditInputSchema.safeParse({ slug: "../etc" });
|
|
92
|
-
expect(result.success).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("rejects empty slug", () => {
|
|
96
|
-
const result = auditInputSchema.safeParse({ slug: "" });
|
|
97
|
-
expect(result.success).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("rejects non-url string in url field", () => {
|
|
101
|
-
const result = auditInputSchema.safeParse({ url: "not-a-url" });
|
|
102
|
-
expect(result.success).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("rejects empty object (neither slug nor url)", () => {
|
|
106
|
-
const result = auditInputSchema.safeParse({});
|
|
107
|
-
expect(result.success).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ── Handler happy-path tests ──────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
describe("ailkAudit handler — slug input", () => {
|
|
114
|
-
it("returns score, perRule, perPage, meta for a known HomePage slug", async () => {
|
|
115
|
-
const result = await ailkAudit(
|
|
116
|
-
{ slug: "home", type: "HomePage" },
|
|
117
|
-
CONTENT_ROOT,
|
|
118
|
-
);
|
|
119
|
-
expect(typeof result.score).toBe("number");
|
|
120
|
-
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
121
|
-
expect(result.score).toBeLessThanOrEqual(100);
|
|
122
|
-
expect(Array.isArray(result.perRule)).toBe(true);
|
|
123
|
-
expect(Array.isArray(result.perPage)).toBe(true);
|
|
124
|
-
expect(typeof result.meta).toBe("object");
|
|
125
|
-
expect(typeof result.meta.auditedAt).toBe("string");
|
|
126
|
-
expect(typeof result.meta.ruleSet).toBe("string");
|
|
127
|
-
expect(typeof result.meta.locale).toBe("string");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("perRule entries have ruleId, name, weight, status fields", async () => {
|
|
131
|
-
const result = await ailkAudit(
|
|
132
|
-
{ slug: "home", type: "HomePage" },
|
|
133
|
-
CONTENT_ROOT,
|
|
134
|
-
);
|
|
135
|
-
for (const rule of result.perRule) {
|
|
136
|
-
expect(typeof rule.ruleId).toBe("string");
|
|
137
|
-
expect(typeof rule.name).toBe("string");
|
|
138
|
-
expect(typeof rule.weight).toBe("number");
|
|
139
|
-
expect(rule.status === "pass" || rule.status === "fail").toBe(true);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("perPage entries have slug and type fields", async () => {
|
|
144
|
-
const result = await ailkAudit(
|
|
145
|
-
{ slug: "home", type: "HomePage" },
|
|
146
|
-
CONTENT_ROOT,
|
|
147
|
-
);
|
|
148
|
-
expect(result.perPage).toHaveLength(1);
|
|
149
|
-
expect(result.perPage[0].slug).toBe("home");
|
|
150
|
-
expect(result.perPage[0].type).toBe("HomePage");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("output validates against auditOutputSchema", async () => {
|
|
154
|
-
const result = await ailkAudit(
|
|
155
|
-
{ slug: "home", type: "HomePage" },
|
|
156
|
-
CONTENT_ROOT,
|
|
157
|
-
);
|
|
158
|
-
const parsed = auditOutputSchema.safeParse(result);
|
|
159
|
-
expect(parsed.success).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("ailkAudit handler — url input (same-origin)", () => {
|
|
164
|
-
it("returns audit result for a same-origin URL", async () => {
|
|
165
|
-
const result = (await withBaseUrl(TEST_BASE_URL, () =>
|
|
166
|
-
ailkAudit({ url: `${TEST_BASE_URL}/HomePage/home` }, CONTENT_ROOT),
|
|
167
|
-
)) as Awaited<ReturnType<typeof ailkAudit>>;
|
|
168
|
-
expect(typeof result.score).toBe("number");
|
|
169
|
-
expect(result.perPage[0].slug).toBe("home");
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("ailkAudit handler — cross-domain URL", () => {
|
|
174
|
-
it("throws for a cross-domain URL (no fetch attempted)", async () => {
|
|
175
|
-
await expect(
|
|
176
|
-
withBaseUrl(TEST_BASE_URL, () =>
|
|
177
|
-
ailkAudit(
|
|
178
|
-
{ url: "https://evil.example.com/HomePage/page" },
|
|
179
|
-
CONTENT_ROOT,
|
|
180
|
-
),
|
|
181
|
-
),
|
|
182
|
-
).rejects.toThrow(/out of scope|use `ailk audit`/i);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ── Structural guards ─────────────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
describe("structural guards", () => {
|
|
189
|
-
it("apps/mcp/src/tools/audit.ts does not call console.*", () => {
|
|
190
|
-
const src = readFileSync(
|
|
191
|
-
resolve(__dirname, "../src/tools/audit.ts"),
|
|
192
|
-
"utf-8",
|
|
193
|
-
);
|
|
194
|
-
expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("apps/mcp/src/tools/audit.ts does not call fetch()", () => {
|
|
198
|
-
const src = readFileSync(
|
|
199
|
-
resolve(__dirname, "../src/tools/audit.ts"),
|
|
200
|
-
"utf-8",
|
|
201
|
-
);
|
|
202
|
-
// Strip comments before checking — JSDoc may mention fetch() as a negative assertion.
|
|
203
|
-
const codeOnly = src
|
|
204
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
205
|
-
.replace(/\/\/[^\n]*/g, "");
|
|
206
|
-
expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// ── Schema relocation parity (Phase 3 U0 / ADR 0013) ──────────────────────────
|
|
211
|
-
|
|
212
|
-
describe("purchaseInputSchema relocated to @working-theory/validation", () => {
|
|
213
|
-
it("parses the same shape the inline schema parsed pre-relocation", () => {
|
|
214
|
-
const result = purchaseInputSchema.safeParse({
|
|
215
|
-
productCode: "price_test_123",
|
|
216
|
-
quantity: 2,
|
|
217
|
-
});
|
|
218
|
-
expect(result.success).toBe(true);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("rejects an empty productCode", () => {
|
|
222
|
-
const result = purchaseInputSchema.safeParse({ productCode: "" });
|
|
223
|
-
expect(result.success).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// ── #5949 — the agent-readiness rules on the MCP audit path ───────────────────
|
|
228
|
-
|
|
229
|
-
const T1_ROOT = resolve(
|
|
230
|
-
__dirname,
|
|
231
|
-
"../../../packages/aeo/__tests__/fixtures/t1-site",
|
|
232
|
-
);
|
|
233
|
-
const T1_CONTENT_ROOT = join(T1_ROOT, "content");
|
|
234
|
-
|
|
235
|
-
const READINESS_RULE_IDS = [
|
|
236
|
-
"policies-present",
|
|
237
|
-
"offer-on-priced-page",
|
|
238
|
-
"tax-code-when-stripe-tax",
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
describe("audit — readiness rules (#5949)", () => {
|
|
242
|
-
it("AC-1: the T1 fixture site passes all three through the MCP tool", async () => {
|
|
243
|
-
const result = await ailkAudit(
|
|
244
|
-
{ slug: "widget", type: "Product" },
|
|
245
|
-
T1_CONTENT_ROOT,
|
|
246
|
-
T1_ROOT,
|
|
247
|
-
);
|
|
248
|
-
for (const id of READINESS_RULE_IDS) {
|
|
249
|
-
const row = result.perRule.find((r) => r.ruleId === id);
|
|
250
|
-
expect(row).toBeDefined();
|
|
251
|
-
expect(row!.status).toBe("pass");
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("the tool's rule set is 9 rows and still validates against auditOutputSchema", async () => {
|
|
256
|
-
const result = await ailkAudit(
|
|
257
|
-
{ slug: "widget", type: "Product" },
|
|
258
|
-
T1_CONTENT_ROOT,
|
|
259
|
-
T1_ROOT,
|
|
260
|
-
);
|
|
261
|
-
expect(result.perRule).toHaveLength(9);
|
|
262
|
-
expect(auditOutputSchema.safeParse(result).success).toBe(true);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("D5: the site scan is the five READINESS_SCAN_TYPES and no more", async () => {
|
|
266
|
-
const input = await buildSinglePageAuditInput(
|
|
267
|
-
{ slug: "widget", type: "Product" },
|
|
268
|
-
T1_CONTENT_ROOT,
|
|
269
|
-
T1_ROOT,
|
|
270
|
-
);
|
|
271
|
-
const scanned = new Set(input.readiness!.sitePages.map((p) => p.type));
|
|
272
|
-
expect([...scanned].sort()).toEqual([...READINESS_SCAN_TYPES].sort());
|
|
273
|
-
// The fixture publishes a HomePage; it is deliberately outside the universe.
|
|
274
|
-
expect(scanned.has("HomePage")).toBe(false);
|
|
275
|
-
expect(input.readiness!.sitePages.find((p) => p.slug === "widget")).toEqual(
|
|
276
|
-
{
|
|
277
|
-
slug: "widget",
|
|
278
|
-
type: "Product",
|
|
279
|
-
price: 49,
|
|
280
|
-
taxCode: "txcd_99999999",
|
|
281
|
-
},
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("D5 fail-open-to-pass: a site the scan finds nothing in yields sitePages: [] and three passes", async () => {
|
|
286
|
-
const dir = mkdtempSync(join(tmpdir(), "ailk-mcp-readiness-"));
|
|
287
|
-
try {
|
|
288
|
-
mkdirSync(join(dir, "content", "en", "HomePage"), { recursive: true });
|
|
289
|
-
writeFileSync(
|
|
290
|
-
join(dir, "content", "en", "HomePage", "index.mdx"),
|
|
291
|
-
[
|
|
292
|
-
"---",
|
|
293
|
-
'type: "HomePage"',
|
|
294
|
-
'slug: "index"',
|
|
295
|
-
'title: "Nothing for sale"',
|
|
296
|
-
'description: "A site the readiness scan finds no priced page in."',
|
|
297
|
-
"---",
|
|
298
|
-
"",
|
|
299
|
-
"Body.",
|
|
300
|
-
].join("\n"),
|
|
301
|
-
"utf8",
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
const input = await buildSinglePageAuditInput(
|
|
305
|
-
{ slug: "index", type: "HomePage" },
|
|
306
|
-
join(dir, "content"),
|
|
307
|
-
dir,
|
|
308
|
-
);
|
|
309
|
-
expect(input.readiness!.sitePages).toEqual([]);
|
|
310
|
-
|
|
311
|
-
const result = await ailkAudit(
|
|
312
|
-
{ slug: "index", type: "HomePage" },
|
|
313
|
-
join(dir, "content"),
|
|
314
|
-
dir,
|
|
315
|
-
);
|
|
316
|
-
for (const id of READINESS_RULE_IDS) {
|
|
317
|
-
expect(result.perRule.find((r) => r.ruleId === id)!.status).toBe(
|
|
318
|
-
"pass",
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
} finally {
|
|
322
|
-
rmSync(dir, { recursive: true, force: true });
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("the projectRoot seam defaults to cwd when omitted (existing call sites unchanged)", async () => {
|
|
327
|
-
const result = await ailkAudit(
|
|
328
|
-
{ slug: "home", type: "HomePage" },
|
|
329
|
-
CONTENT_ROOT,
|
|
330
|
-
);
|
|
331
|
-
expect(result.perRule).toHaveLength(9);
|
|
332
|
-
expect(auditOutputSchema.safeParse(result).success).toBe(true);
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
describe("readStripeTaxSignal — wiring smoke test (#6012)", () => {
|
|
337
|
-
// Not a duplicate of packages/aeo's own loadCommerceConfig suite (which
|
|
338
|
-
// covers the reader's parse/extract arms in isolation, against synthetic
|
|
339
|
-
// tmpdirs): this proves the EXPORTED apps/mcp wrapper itself is wired
|
|
340
|
-
// correctly — the directory probe delegating to loadCommerceConfig — against
|
|
341
|
-
// a real fixture file, since #6012 deleted the rest of this describe block
|
|
342
|
-
// as duplicate coverage but nothing else here exercises readStripeTaxSignal.
|
|
343
|
-
it("reads the T1 fixture's own project.yaml — Stripe Tax on", () => {
|
|
344
|
-
expect(readStripeTaxSignal(T1_ROOT)).toBe(true);
|
|
345
|
-
});
|
|
346
|
-
});
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP ↔ Server-Action parity test.
|
|
3
|
-
*
|
|
4
|
-
* Today: trivially passes (no Server Actions in apps/web, no MCP tools yet).
|
|
5
|
-
* Phase 3: every Server Action in apps/web has a matching MCP tool.
|
|
6
|
-
*
|
|
7
|
-
* Convention (locked in spec §3):
|
|
8
|
-
* - Server Actions live at `apps/web/**\/*.actions.ts` and export a symbol
|
|
9
|
-
* named `^[a-z][a-zA-Z0-9]*Action$` (e.g. `captureLeadAction`).
|
|
10
|
-
* - MCP tools live at `apps/mcp/src/tools/<tool>.ts` and export a symbol
|
|
11
|
-
* named `^ailk[A-Z][a-zA-Z0-9]*$` (e.g. `ailkCaptureLead`).
|
|
12
|
-
* - Mapping: `<name>Action` ↔ `ailk<Name>` (PascalCase of the action prefix).
|
|
13
|
-
*
|
|
14
|
-
* The harness exposes `computeParityFailures(actions, tools)` as a pure helper
|
|
15
|
-
* so the populated/empty/divergent cases can be exercised without filesystem.
|
|
16
|
-
* The integration case enumerates the real filesystem and runs the same helper.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
|
|
20
|
-
import { resolve, join } from "node:path";
|
|
21
|
-
|
|
22
|
-
import { getMcpToolRegistrations } from "@working-theory/metadata";
|
|
23
|
-
|
|
24
|
-
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
25
|
-
const WEB_ROOT = join(REPO_ROOT, "apps/web");
|
|
26
|
-
const MCP_TOOLS_ROOT = join(REPO_ROOT, "apps/mcp/src/tools");
|
|
27
|
-
|
|
28
|
-
const ACTION_FILE_RE = /\.actions\.ts$/;
|
|
29
|
-
const ACTION_EXPORT_RE =
|
|
30
|
-
/export\s+(?:async\s+)?(?:function|const)\s+([a-z][a-zA-Z0-9]*Action)\b/g;
|
|
31
|
-
const TOOL_EXPORT_RE =
|
|
32
|
-
/export\s+(?:async\s+)?(?:function|const)\s+(ailk[A-Z][a-zA-Z0-9]*)\b/g;
|
|
33
|
-
|
|
34
|
-
function walk(dir: string, out: string[] = []): string[] {
|
|
35
|
-
if (!existsSync(dir)) return out;
|
|
36
|
-
for (const entry of readdirSync(dir)) {
|
|
37
|
-
if (entry === "node_modules" || entry === "dist" || entry === ".next")
|
|
38
|
-
continue;
|
|
39
|
-
const p = join(dir, entry);
|
|
40
|
-
const s = statSync(p);
|
|
41
|
-
if (s.isDirectory()) walk(p, out);
|
|
42
|
-
else out.push(p);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function discoverServerActions(webRoot: string = WEB_ROOT): Set<string> {
|
|
48
|
-
const found = new Set<string>();
|
|
49
|
-
for (const file of walk(webRoot)) {
|
|
50
|
-
if (!ACTION_FILE_RE.test(file)) continue;
|
|
51
|
-
const content = readFileSync(file, "utf-8");
|
|
52
|
-
for (const m of content.matchAll(ACTION_EXPORT_RE)) {
|
|
53
|
-
found.add(m[1]);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return found;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function discoverMcpTools(
|
|
60
|
-
toolsRoot: string = MCP_TOOLS_ROOT,
|
|
61
|
-
): Set<string> {
|
|
62
|
-
const found = new Set<string>();
|
|
63
|
-
if (!existsSync(toolsRoot)) return found;
|
|
64
|
-
for (const file of walk(toolsRoot)) {
|
|
65
|
-
if (!file.endsWith(".ts")) continue;
|
|
66
|
-
const content = readFileSync(file, "utf-8");
|
|
67
|
-
for (const m of content.matchAll(TOOL_EXPORT_RE)) {
|
|
68
|
-
found.add(m[1]);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return found;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Map an action symbol (e.g. `captureLeadAction`) to its expected MCP tool
|
|
76
|
-
* symbol (`ailkCaptureLead`).
|
|
77
|
-
*/
|
|
78
|
-
export function actionToToolName(action: string): string {
|
|
79
|
-
// Strip trailing "Action" and capitalize first char.
|
|
80
|
-
const base = action.replace(/Action$/, "");
|
|
81
|
-
if (base.length === 0) return "ailk";
|
|
82
|
-
return `ailk${base[0].toUpperCase()}${base.slice(1)}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Map a catalog tool name (e.g. `get_agent_context`) to its handler symbol
|
|
87
|
-
* (`ailkGetAgentContext`) — the ADR 0013 naming the harness discovers.
|
|
88
|
-
*/
|
|
89
|
-
export function toolNameToHandler(toolName: string): string {
|
|
90
|
-
const pascal = toolName
|
|
91
|
-
.split("_")
|
|
92
|
-
.map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
|
|
93
|
-
.join("");
|
|
94
|
-
return `ailk${pascal}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The signed-in `/mcp/workspace` handlers (#3591 D13–D16), derived from the
|
|
99
|
-
* catalog's `surface: "workspace"` entries — never a hand-kept list or count.
|
|
100
|
-
* They sit outside this Server-Action harness, not inside MCP_ONLY_TOOLS: each
|
|
101
|
-
* has an apps/api twin route, proven by the API ↔ MCP parity guard
|
|
102
|
-
* (`tests/parity/api-parity/kinds/site.ts`), and no web Server Action.
|
|
103
|
-
*/
|
|
104
|
-
export function workspaceSurfaceHandlers(): Set<string> {
|
|
105
|
-
return new Set(
|
|
106
|
-
getMcpToolRegistrations({ surface: "workspace" }).map((reg) =>
|
|
107
|
-
toolNameToHandler(reg.name),
|
|
108
|
-
),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* MCP-only tools — tools that have no Web Server Action equivalent because
|
|
114
|
-
* they are pure-read content tools. Adding a Server Action wrapper would be
|
|
115
|
-
* meaningless: these tools exist to serve agent consumers, not web UI flows.
|
|
116
|
-
*
|
|
117
|
-
* Each entry is the camelCase MCP handler name (e.g. ailkListPages).
|
|
118
|
-
* Extend this list in subsequent issues (I3–I5) as more read-only tools land.
|
|
119
|
-
*
|
|
120
|
-
* Note: this allow-list uses the symbol name convention (`ailk<PascalVerb>`),
|
|
121
|
-
* not the MCP tool name convention (`list_pages`). The parity harness operates
|
|
122
|
-
* on symbol names discovered via regex from tool source files.
|
|
123
|
-
*/
|
|
124
|
-
const MCP_ONLY_TOOLS = new Set([
|
|
125
|
-
"ailkListPages", // list_pages — pure read, no action equivalent (I2)
|
|
126
|
-
"ailkReadContent", // read_content — pure read, no action equivalent (I2)
|
|
127
|
-
"ailkSearchContent", // search_content — pure read, no action equivalent (I4)
|
|
128
|
-
"ailkGetSiteInfo", // get_site_info — pure read, no action equivalent (I4)
|
|
129
|
-
"ailkGetBusinessHours", // get_business_hours — pure read, no action equivalent (I4)
|
|
130
|
-
"ailkGetLocations", // get_locations — pure read, no action equivalent (I4)
|
|
131
|
-
"ailkGetOfferings", // get_offerings — pure read, no action equivalent (I4)
|
|
132
|
-
"ailkGetPricing", // get_pricing — pure read, no action equivalent (I4)
|
|
133
|
-
"ailkGetFaq", // get_faq — pure read, no action equivalent (I4)
|
|
134
|
-
"ailkAudit", // audit — AEO scoring tool, no web action equivalent (I5; ADR 0013)
|
|
135
|
-
"ailkRequestCallback", // request_callback — anonymous write via MCP; no Server Action (B7)
|
|
136
|
-
"ailkCreateProject", // create_project — agent-only project creator; no Server Action equivalent (#299)
|
|
137
|
-
"ailkGetFlow", // get_flow — agent-callable flow-engine read; no web Server Action (#3956 D7)
|
|
138
|
-
"ailkListAvailability", // list_availability — anonymous scheduling read via MCP; the booker section is a client island calling apps/api directly, not a Server Action (#4416)
|
|
139
|
-
"ailkGetEventMeta", // get_event_meta — meta-only scheduling read via MCP; a tile-rendering page is an RSC calling apps/api via api-client, not a Server Action (#4633)
|
|
140
|
-
"ailkCreateBooking", // create_booking — anonymous scheduling write via MCP; same posture as subscribe_newsletter/request_callback (#4416)
|
|
141
|
-
// ailkSubmitFlowStep REMOVED (#3957 D8) — submitFlowStepAction is its web
|
|
142
|
-
// Server Action twin (apps/web/app/[locale]/flows/[slug]/flow.actions.ts).
|
|
143
|
-
]);
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Server Actions that are intentionally WEB-ONLY — a RECORDED exception to the
|
|
147
|
-
* API ↔ MCP parity contract, not a gap. The mirror of MCP_ONLY_TOOLS: an
|
|
148
|
-
* action listed here is excluded from missing-tool reporting.
|
|
149
|
-
*
|
|
150
|
-
* Note: this allow-list uses the action's exported symbol name
|
|
151
|
-
* (`<verb>Action`), exactly as discovered from `apps/web/**\/*.actions.ts`.
|
|
152
|
-
*/
|
|
153
|
-
const WEB_ONLY_ACTIONS = new Set([
|
|
154
|
-
// The recommender's email moment (#4841 → POST /v1/flows/:slug/save and
|
|
155
|
-
// /checkout; consumed by the web Server Actions in #4842's
|
|
156
|
-
// apps/web/app/[locale]/flows/[slug]/checkout.actions.ts). No MCP twin —
|
|
157
|
-
// the recorded exception in apps/api/CLAUDE.md (the `/v1/flows/:slug/save`
|
|
158
|
-
// + `/checkout` row, #4841): "an agent walking a flow has no email to give
|
|
159
|
-
// and no card to pay with; the anonymous MCP surface is identity-blind."
|
|
160
|
-
"startCheckoutAction", // checkout.actions.ts — Pay → Stripe TEST-mode hosted Checkout (#4842 D7)
|
|
161
|
-
"saveRecommendationAction", // checkout.actions.ts — "Email me this plan" → flows.save (#4842 D7)
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Pure parity computation. Given the set of action symbols and the set of
|
|
166
|
-
* MCP tool symbols, return the missing-tool and orphan-tool sets.
|
|
167
|
-
*
|
|
168
|
-
* Tools in MCP_ONLY_TOOLS are excluded from orphan reporting — they are
|
|
169
|
-
* intentionally MCP-only and have no Server Action counterpart. Actions in
|
|
170
|
-
* WEB_ONLY_ACTIONS are excluded from missing-tool reporting — they are
|
|
171
|
-
* intentionally web-only and have no MCP tool counterpart.
|
|
172
|
-
*/
|
|
173
|
-
export function computeParityFailures(
|
|
174
|
-
actions: Set<string>,
|
|
175
|
-
tools: Set<string>,
|
|
176
|
-
): { missingTools: string[]; orphanTools: string[] } {
|
|
177
|
-
const expected = new Set<string>();
|
|
178
|
-
for (const a of actions) {
|
|
179
|
-
if (WEB_ONLY_ACTIONS.has(a)) continue;
|
|
180
|
-
expected.add(actionToToolName(a));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const missingTools: string[] = [];
|
|
184
|
-
for (const e of expected) if (!tools.has(e)) missingTools.push(e);
|
|
185
|
-
|
|
186
|
-
const orphanTools: string[] = [];
|
|
187
|
-
for (const t of tools) {
|
|
188
|
-
if (!expected.has(t) && !MCP_ONLY_TOOLS.has(t)) orphanTools.push(t);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
missingTools.sort();
|
|
192
|
-
orphanTools.sort();
|
|
193
|
-
return { missingTools, orphanTools };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
describe("MCP ↔ Server-Action parity (helper)", () => {
|
|
199
|
-
it("trivially passes when both sides are empty", () => {
|
|
200
|
-
const r = computeParityFailures(new Set(), new Set());
|
|
201
|
-
expect(r.missingTools).toEqual([]);
|
|
202
|
-
expect(r.orphanTools).toEqual([]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("reports missing MCP tools when an action has no matching tool", () => {
|
|
206
|
-
const actions = new Set(["captureLeadAction"]);
|
|
207
|
-
const tools = new Set<string>();
|
|
208
|
-
const r = computeParityFailures(actions, tools);
|
|
209
|
-
expect(r.missingTools).toEqual(["ailkCaptureLead"]);
|
|
210
|
-
expect(r.orphanTools).toEqual([]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("reports orphan MCP tools when a tool has no matching action", () => {
|
|
214
|
-
const actions = new Set<string>();
|
|
215
|
-
const tools = new Set(["ailkPurchase"]);
|
|
216
|
-
const r = computeParityFailures(actions, tools);
|
|
217
|
-
expect(r.missingTools).toEqual([]);
|
|
218
|
-
expect(r.orphanTools).toEqual(["ailkPurchase"]);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("passes when actions and tools are paired", () => {
|
|
222
|
-
const actions = new Set(["captureLeadAction", "purchaseAction"]);
|
|
223
|
-
const tools = new Set(["ailkCaptureLead", "ailkPurchase"]);
|
|
224
|
-
const r = computeParityFailures(actions, tools);
|
|
225
|
-
expect(r.missingTools).toEqual([]);
|
|
226
|
-
expect(r.orphanTools).toEqual([]);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("skips WEB_ONLY_ACTIONS when computing missing tools (the recorded #4841 exception)", () => {
|
|
230
|
-
const actions = new Set([
|
|
231
|
-
"captureLeadAction",
|
|
232
|
-
"startCheckoutAction",
|
|
233
|
-
"saveRecommendationAction",
|
|
234
|
-
]);
|
|
235
|
-
const tools = new Set(["ailkCaptureLead"]);
|
|
236
|
-
const r = computeParityFailures(actions, tools);
|
|
237
|
-
expect(r.missingTools).toEqual([]);
|
|
238
|
-
expect(r.orphanTools).toEqual([]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("maps action names to tool names by stripping Action and prefixing ailk", () => {
|
|
242
|
-
expect(actionToToolName("captureLeadAction")).toBe("ailkCaptureLead");
|
|
243
|
-
expect(actionToToolName("purchaseAction")).toBe("ailkPurchase");
|
|
244
|
-
expect(actionToToolName("auditAction")).toBe("ailkAudit");
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("maps catalog tool names to handler symbols by PascalCasing and prefixing ailk", () => {
|
|
248
|
-
expect(toolNameToHandler("get_agent_context")).toBe("ailkGetAgentContext");
|
|
249
|
-
expect(toolNameToHandler("mark_inbox_message_done")).toBe(
|
|
250
|
-
"ailkMarkInboxMessageDone",
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe("MCP ↔ Server-Action parity (filesystem)", () => {
|
|
256
|
-
it("post-#4842 state: five Server Actions (purchaseAction + captureLeadAction + submitFlowStepAction + the two web-only startCheckoutAction + saveRecommendationAction), twenty-three MCP tools, zero orphans", () => {
|
|
257
|
-
const actions = discoverServerActions();
|
|
258
|
-
const discovered = discoverMcpTools();
|
|
259
|
-
const workspaceHandlers = workspaceSurfaceHandlers();
|
|
260
|
-
// The anonymous-surface handlers — what this harness has always counted.
|
|
261
|
-
// The signed-in handlers are set aside and checked against the catalog
|
|
262
|
-
// below, so a handler the catalog does not name lands here and fails.
|
|
263
|
-
const tools = new Set(
|
|
264
|
-
[...discovered].filter((t) => !workspaceHandlers.has(t)),
|
|
265
|
-
);
|
|
266
|
-
const r = computeParityFailures(actions, tools);
|
|
267
|
-
|
|
268
|
-
// After D14a Stage 1 Unit C (#55) + #3956 (get_flow/submit_flow_step) +
|
|
269
|
-
// #3957 (submitFlowStepAction — the web Server Action twin of
|
|
270
|
-
// ailkSubmitFlowStep): twenty tools + three paired Server Actions.
|
|
271
|
-
// - ailkPurchase: paired with purchaseAction (apps/web/app/dev/purchase/).
|
|
272
|
-
// - ailkCaptureLead: paired with captureLeadAction (apps/web/app/contact/).
|
|
273
|
-
// - ailkSubscribeNewsletter: paired with subscribeNewsletterAction
|
|
274
|
-
// (apps/web/app/[locale]/newsletter/newsletter.actions.ts, #6730 D9).
|
|
275
|
-
// - ailkSubmitFlowStep: paired with submitFlowStepAction
|
|
276
|
-
// (apps/web/app/[locale]/flows/[slug]/flow.actions.ts, #3957).
|
|
277
|
-
// - startCheckoutAction, saveRecommendationAction: WEB-ONLY (#4842 D7,
|
|
278
|
-
// apps/web/app/[locale]/flows/[slug]/checkout.actions.ts) — the HTTP
|
|
279
|
-
// twins are POST /v1/flows/:slug/checkout and /save (#4841); no MCP
|
|
280
|
-
// twin by the recorded apps/api/CLAUDE.md exception (an agent walking
|
|
281
|
-
// a flow has no email to give and no card to pay with; the anonymous
|
|
282
|
-
// MCP surface is identity-blind). In WEB_ONLY_ACTIONS.
|
|
283
|
-
// - ailkListPages, ailkReadContent: MCP-only (pure read; I2)
|
|
284
|
-
// - ailkSearchContent, ailkGetSiteInfo, ailkGetBusinessHours,
|
|
285
|
-
// ailkGetLocations, ailkGetOfferings, ailkGetPricing, ailkGetFaq:
|
|
286
|
-
// MCP-only (pure read; I4; in MCP_ONLY_TOOLS allow-list).
|
|
287
|
-
// - ailkAudit: MCP-only (AEO scoring; I5; in MCP_ONLY_TOOLS allow-list).
|
|
288
|
-
// - ailkRequestCallback: MCP-only anonymous-write tool (B7; in
|
|
289
|
-
// MCP_ONLY_TOOLS allow-list).
|
|
290
|
-
// - ailkCreateProject: MCP-only agent project creator (#299; in MCP_ONLY_TOOLS).
|
|
291
|
-
// - ailkCreatePage, ailkUpdatePage, ailkDeletePage: RETIRED by #5853
|
|
292
|
-
// (D7). Their HTTP twins — the untenanted /v1/content writes — were
|
|
293
|
-
// deleted in the same PR, so keeping the tools would have left three
|
|
294
|
-
// MCP tools with no HTTP equivalent, a direct breach of the parity rule
|
|
295
|
-
// this file enforces. Their replacement is identity-BOUND
|
|
296
|
-
// (/v1/workspaces/:workspaceId/content), which the identity-blind
|
|
297
|
-
// anonymous MCP surface can never carry (D15); the authed MCP surface
|
|
298
|
-
// is #3591.
|
|
299
|
-
// - ailkGetFlow: MCP-only agent-callable flow-engine read; HTTP twin is
|
|
300
|
-
// apps/api, not a Server Action (#3956 D7) — the page fetch in
|
|
301
|
-
// apps/web/app/[locale]/flows/[slug]/page.tsx is a direct api-client
|
|
302
|
-
// call in an RSC, not a Server Action.
|
|
303
|
-
// - ailkListAvailability, ailkCreateBooking: MCP-only scheduling twins of
|
|
304
|
-
// GET /v1/schedule/availability and POST /v1/schedule/book (#4416). The
|
|
305
|
-
// `booker` section is a "use client" island that fetches those routes
|
|
306
|
-
// directly (the ContactForm/StepFlow pattern) — no Server Action, so
|
|
307
|
-
// the HTTP twin is apps/api.
|
|
308
|
-
// - ailkGetEventMeta: MCP-only meta-only scheduling read, twin of
|
|
309
|
-
// GET /v1/schedule/event-meta (#4633) — same posture as the other
|
|
310
|
-
// scheduling tools.
|
|
311
|
-
expect(actions.size).toBe(6);
|
|
312
|
-
expect(tools.size).toBe(20);
|
|
313
|
-
expect(r.missingTools).toEqual([]);
|
|
314
|
-
expect(r.orphanTools).toEqual([]);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("every catalog workspace-surface tool has its handler under src/tools/, and no other signed-in handler exists (#3591 D16)", () => {
|
|
318
|
-
const discovered = discoverMcpTools();
|
|
319
|
-
const workspaceHandlers = workspaceSurfaceHandlers();
|
|
320
|
-
expect(workspaceHandlers.size).toBeGreaterThan(0);
|
|
321
|
-
expect(
|
|
322
|
-
[...discovered].filter((t) => workspaceHandlers.has(t)).sort(),
|
|
323
|
-
).toEqual([...workspaceHandlers].sort());
|
|
324
|
-
});
|
|
325
|
-
});
|