create-ailk 0.10.0 → 0.11.1
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 +7 -2
- package/dist/cli.js +5 -1
- package/dist/content-payloads.d.ts +37 -0
- package/dist/content-payloads.js +64 -0
- package/dist/lib/fetch-module.d.ts +3 -3
- package/dist/lib/fetch-module.js +11 -5
- package/dist/lib/fetch-template.d.ts +1 -1
- package/dist/lib/fetch-template.js +7 -4
- package/dist/lib/scaffold-render-sweep.d.ts +16 -0
- package/dist/lib/scaffold-render-sweep.js +52 -3
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +43 -12
- package/dist/parse-args.d.ts +2 -0
- package/dist/parse-args.js +7 -0
- package/dist/surfaces.js +2 -3
- package/package.json +3 -3
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +4 -3
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +8 -8
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +8 -8
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +8 -8
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +8 -8
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +8 -8
- package/templates/.claude/settings.json +12 -0
- package/templates/.env.example +1 -5
- package/templates/CLAUDE.md +38 -22
- package/templates/README.md +12 -12
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/mcp/CLAUDE.md +23 -10
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +160 -17
- package/templates/apps/mcp/__tests__/index.test.ts +118 -1
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +425 -5
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +67 -3
- package/templates/apps/mcp/__tests__/server.test.ts +162 -1
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/index.ts +35 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +193 -24
- package/templates/apps/mcp/src/server.ts +77 -5
- package/templates/apps/mcp/src/tools/purchase.ts +1 -1
- package/templates/apps/web/CLAUDE.md +1 -1
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +15 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +10 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/for/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +1 -15
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +1 -14
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +4 -16
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +3 -15
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +0 -10
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +1 -14
- package/templates/apps/web/src/lib/commerce-claim.ts +15 -6
- package/templates/content/_site.mdx +23 -4
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +34 -34
- package/templates/.claude/skills/README.md +0 -37
- package/templates/.claude/skills/add-auth/SKILL.md +0 -313
- package/templates/.claude/skills/add-auth/references/env-vars.md +0 -106
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +0 -180
- package/templates/.claude/skills/add-page/SKILL.md +0 -260
- package/templates/.claude/skills/add-page/references/page-types.md +0 -123
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +0 -87
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +0 -99
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +0 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +0 -43
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +0 -52
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +0 -38
- package/templates/.claude/skills/check-site/SKILL.md +0 -245
- package/templates/.claude/skills/check-site/references/common-errors.md +0 -116
- package/templates/.claude/skills/check-site/references/findings-glossary.md +0 -140
- package/templates/.claude/skills/check-site/references/severity-levels.md +0 -114
- package/templates/.claude/skills/fill-page/SKILL.md +0 -199
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +0 -80
- package/templates/.claude/skills/fill-page/references/section-sources.md +0 -41
- package/templates/.claude/skills/setup-database/SKILL.md +0 -156
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +0 -154
- package/templates/.claude/skills/setup-deploy/SKILL.md +0 -312
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +0 -204
- package/templates/.claude/skills/setup-deploy/references/vercel-json-template.md +0 -98
- package/templates/.claude/skills/update-page/SKILL.md +0 -117
- package/templates/.claude/skills/update-page/scripts/update-page.ts +0 -640
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +0 -31
- package/templates/.claude/skills/upgrade-plan/SKILL.md +0 -121
- package/templates/.claude/skills/upgrade-plan/references/options.md +0 -24
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +0 -527
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +0 -75
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +0 -44
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* `start_purchase` visible, so comparing a claimed server against an
|
|
14
14
|
* unclaimed catalog (or vice versa) would compare unlike surfaces and
|
|
15
15
|
* report drift that isn't there — or, worse, miss drift that is.
|
|
16
|
+
* - …and under every licence-feature state × gate mode (#7136): 2 commerce
|
|
17
|
+
* states × {warn, refuse} × 4 held-feature sets = 16. In refuse mode a
|
|
18
|
+
* feature's tools are in both surfaces iff the feature is held; in warn
|
|
19
|
+
* mode all five are in both.
|
|
16
20
|
*
|
|
17
21
|
* What this does NOT assert:
|
|
18
22
|
* - inputSchema equality — the server registers Zod schemas; the catalog
|
|
@@ -21,14 +25,35 @@
|
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
getMcpToolCatalog,
|
|
30
|
+
type McpCatalogOptions,
|
|
31
|
+
type McpToolCatalogEntry,
|
|
32
|
+
} from "@working-theory/metadata";
|
|
25
33
|
|
|
26
34
|
import {
|
|
35
|
+
MCP_CLAIM_REFRESH_MS,
|
|
27
36
|
__setMcpCommerceClaimForTest,
|
|
37
|
+
__setMcpFeatureGateForTest,
|
|
38
|
+
__setMcpHeldFeaturesForTest,
|
|
39
|
+
initMcpCommerceClaim,
|
|
28
40
|
resetMcpCommerceClaim,
|
|
41
|
+
startMcpClaimRefresh,
|
|
29
42
|
type McpCommerceClaim,
|
|
30
43
|
} from "../src/lib/commerce-claim.js";
|
|
31
|
-
import { createMcpServer } from "../src/server.js";
|
|
44
|
+
import { createMcpServer, listRegisteredTools } from "../src/server.js";
|
|
45
|
+
|
|
46
|
+
/** The injected licence reader — same seam `commerce-claim.test.ts` types. */
|
|
47
|
+
type CheckLicenseSeam = NonNullable<
|
|
48
|
+
NonNullable<Parameters<typeof initMcpCommerceClaim>[0]>["checkLicense"]
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
type LicenseShape = Awaited<ReturnType<CheckLicenseSeam>>;
|
|
52
|
+
|
|
53
|
+
/** A licence reader that resolves to the given (partial) result. */
|
|
54
|
+
function resolving(result: Partial<LicenseShape>): CheckLicenseSeam {
|
|
55
|
+
return () => Promise.resolve(result as LicenseShape);
|
|
56
|
+
}
|
|
32
57
|
|
|
33
58
|
interface RegisteredTool {
|
|
34
59
|
description?: string;
|
|
@@ -47,29 +72,70 @@ const PAID_MODULES = "scheduling,content-write,signal-collectors";
|
|
|
47
72
|
* @modelcontextprotocol/sdk ^1.29.0 — if the SDK renames the field, change it
|
|
48
73
|
* here once rather than at each call site.
|
|
49
74
|
*/
|
|
50
|
-
function registeredToolsOf(
|
|
51
|
-
server: McpServer,
|
|
52
|
-
): Record<string, RegisteredTool> {
|
|
75
|
+
function registeredToolsOf(server: McpServer): Record<string, RegisteredTool> {
|
|
53
76
|
return (
|
|
54
77
|
// eslint-disable-next-line no-restricted-syntax -- #506/#507: the SDK exposes no public accessor; the double assertion is confined to this one helper
|
|
55
|
-
server as unknown as { _registeredTools: Record<string, RegisteredTool> }
|
|
56
|
-
|
|
78
|
+
(server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
79
|
+
._registeredTools
|
|
80
|
+
);
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
type GateMode = NonNullable<McpCatalogOptions["featureGate"]>;
|
|
84
|
+
|
|
85
|
+
type McpFeature = NonNullable<McpToolCatalogEntry["feature"]>;
|
|
86
|
+
|
|
87
|
+
/** The five licence-feature tools, by the feature that entitles them. */
|
|
88
|
+
const FEATURE_TOOLS: Record<McpFeature, readonly string[]> = {
|
|
89
|
+
scheduling: ["list_availability", "get_event_meta", "create_booking"],
|
|
90
|
+
"flows-deliverables": ["get_flow", "submit_flow_step"],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const HELD_SETS: readonly (readonly McpFeature[])[] = [
|
|
94
|
+
[],
|
|
95
|
+
["scheduling"],
|
|
96
|
+
["flows-deliverables"],
|
|
97
|
+
["scheduling", "flows-deliverables"],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/** Every claim state: commerce × gate mode × held features (16). */
|
|
101
|
+
const CLAIM_STATES = (["unclaimed", "claimed"] as const).flatMap((claim) =>
|
|
102
|
+
(["warn", "refuse"] as const).flatMap((mode) =>
|
|
103
|
+
HELD_SETS.map(
|
|
104
|
+
(held) =>
|
|
105
|
+
[claim, mode, held] as [
|
|
106
|
+
McpCommerceClaim,
|
|
107
|
+
GateMode,
|
|
108
|
+
readonly McpFeature[],
|
|
109
|
+
],
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
59
114
|
/** The catalog options a given claim state implies — the same mapping
|
|
60
115
|
* `server.ts` applies internally, so both surfaces are read like for like. */
|
|
61
|
-
function catalogOptionsFor(
|
|
62
|
-
|
|
116
|
+
function catalogOptionsFor(
|
|
117
|
+
claim: McpCommerceClaim,
|
|
118
|
+
mode: GateMode,
|
|
119
|
+
held: readonly McpFeature[],
|
|
120
|
+
): McpCatalogOptions {
|
|
121
|
+
return {
|
|
122
|
+
addons: { commerce: claim === "claimed" },
|
|
123
|
+
features: held,
|
|
124
|
+
featureGate: mode,
|
|
125
|
+
};
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
describe.each
|
|
66
|
-
"MCP catalog ↔ server drift — commerce
|
|
67
|
-
(claim) => {
|
|
128
|
+
describe.each(CLAIM_STATES)(
|
|
129
|
+
"MCP catalog ↔ server drift — commerce %s, mode %s, held %j",
|
|
130
|
+
(claim, mode, held) => {
|
|
68
131
|
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
132
|
+
const options = catalogOptionsFor(claim, mode, held);
|
|
69
133
|
|
|
70
134
|
beforeEach(() => {
|
|
71
135
|
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
72
136
|
__setMcpCommerceClaimForTest(claim);
|
|
137
|
+
__setMcpFeatureGateForTest(mode);
|
|
138
|
+
__setMcpHeldFeaturesForTest(held);
|
|
73
139
|
});
|
|
74
140
|
|
|
75
141
|
afterEach(() => {
|
|
@@ -87,7 +153,7 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
87
153
|
it("registered tool names equal catalog tool names", () => {
|
|
88
154
|
const registered = registeredToolsOf(createMcpServer());
|
|
89
155
|
const registeredNames = Object.keys(registered).sort();
|
|
90
|
-
const catalogNames = getMcpToolCatalog(
|
|
156
|
+
const catalogNames = getMcpToolCatalog(options)
|
|
91
157
|
.map((t) => t.name)
|
|
92
158
|
.sort();
|
|
93
159
|
expect(registeredNames).toEqual(catalogNames);
|
|
@@ -95,7 +161,7 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
95
161
|
|
|
96
162
|
it("registered tool descriptions equal catalog descriptions", () => {
|
|
97
163
|
const registered = registeredToolsOf(createMcpServer());
|
|
98
|
-
for (const entry of getMcpToolCatalog(
|
|
164
|
+
for (const entry of getMcpToolCatalog(options)) {
|
|
99
165
|
expect(registered[entry.name]).toBeDefined();
|
|
100
166
|
expect(registered[entry.name]!.description).toBe(entry.description);
|
|
101
167
|
}
|
|
@@ -107,10 +173,87 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
107
173
|
|
|
108
174
|
expect("start_purchase" in registered).toBe(expected);
|
|
109
175
|
expect(
|
|
110
|
-
getMcpToolCatalog(
|
|
111
|
-
(t) => t.name === "start_purchase",
|
|
112
|
-
),
|
|
176
|
+
getMcpToolCatalog(options).some((t) => t.name === "start_purchase"),
|
|
113
177
|
).toBe(expected);
|
|
114
178
|
});
|
|
179
|
+
|
|
180
|
+
it("each feature's tools appear in both surfaces exactly when the gate lets them", () => {
|
|
181
|
+
const registered = registeredToolsOf(createMcpServer());
|
|
182
|
+
const catalogNames = getMcpToolCatalog(options).map((t) => t.name);
|
|
183
|
+
|
|
184
|
+
for (const [feature, tools] of Object.entries(FEATURE_TOOLS)) {
|
|
185
|
+
const expected =
|
|
186
|
+
mode === "warn" || held.includes(feature as McpFeature);
|
|
187
|
+
for (const name of tools) {
|
|
188
|
+
expect(name in registered).toBe(expected);
|
|
189
|
+
expect(catalogNames.includes(name)).toBe(expected);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
115
193
|
},
|
|
116
194
|
);
|
|
195
|
+
|
|
196
|
+
describe("MCP catalog ↔ server agreement after a bounded re-read (#7156 AC-1)", () => {
|
|
197
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
201
|
+
jest.useFakeTimers();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
resetMcpCommerceClaim();
|
|
206
|
+
jest.useRealTimers();
|
|
207
|
+
if (priorEnabledModules === undefined) {
|
|
208
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
209
|
+
} else {
|
|
210
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("a new server and listRegisteredTools() match the catalog once the re-read resolves", async () => {
|
|
215
|
+
__setMcpFeatureGateForTest("refuse");
|
|
216
|
+
await initMcpCommerceClaim({
|
|
217
|
+
checkLicense: resolving({
|
|
218
|
+
tier: "pro",
|
|
219
|
+
features: ["scheduling", "flows-deliverables", "commerce"],
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const bootOptions: McpCatalogOptions = {
|
|
224
|
+
addons: { commerce: true },
|
|
225
|
+
features: ["scheduling", "flows-deliverables"],
|
|
226
|
+
featureGate: "refuse",
|
|
227
|
+
};
|
|
228
|
+
const bootNames = Object.keys(registeredToolsOf(createMcpServer())).sort();
|
|
229
|
+
expect(bootNames).toEqual(
|
|
230
|
+
getMcpToolCatalog(bootOptions)
|
|
231
|
+
.map((t) => t.name)
|
|
232
|
+
.sort(),
|
|
233
|
+
);
|
|
234
|
+
expect(bootNames).toHaveLength(20);
|
|
235
|
+
|
|
236
|
+
startMcpClaimRefresh({ checkLicense: resolving({ features: [] }) });
|
|
237
|
+
await jest.advanceTimersByTimeAsync(MCP_CLAIM_REFRESH_MS);
|
|
238
|
+
|
|
239
|
+
const refreshedOptions: McpCatalogOptions = {
|
|
240
|
+
addons: { commerce: false },
|
|
241
|
+
features: [],
|
|
242
|
+
featureGate: "refuse",
|
|
243
|
+
};
|
|
244
|
+
const catalogNames = getMcpToolCatalog(refreshedOptions)
|
|
245
|
+
.map((t) => t.name)
|
|
246
|
+
.sort();
|
|
247
|
+
expect(catalogNames).toHaveLength(14);
|
|
248
|
+
|
|
249
|
+
const serverNames = Object.keys(
|
|
250
|
+
registeredToolsOf(createMcpServer()),
|
|
251
|
+
).sort();
|
|
252
|
+
expect(serverNames).toEqual(catalogNames);
|
|
253
|
+
|
|
254
|
+
const listedNames = listRegisteredTools()
|
|
255
|
+
.map((t) => t.name)
|
|
256
|
+
.sort();
|
|
257
|
+
expect(listedNames).toEqual(catalogNames);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural ordering guard for apps/mcp's bootstrap (#5876 spec D4, AC-3
|
|
2
|
+
* Structural ordering guard for apps/mcp's bootstrap (#5876 spec D4, AC-3;
|
|
3
|
+
* #7156 AC-1, AC-2).
|
|
3
4
|
*
|
|
4
5
|
* `apps/mcp/src/index.ts` has real side effects at import time (starts a
|
|
5
6
|
* transport, opens a stdio/HTTP listener) — it cannot safely be imported by
|
|
@@ -8,6 +9,9 @@
|
|
|
8
9
|
*/
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
10
11
|
import { resolve } from "node:path";
|
|
12
|
+
import vm from "node:vm";
|
|
13
|
+
|
|
14
|
+
import * as ts from "typescript";
|
|
11
15
|
|
|
12
16
|
describe("apps/mcp/src/index.ts — bootstrap ordering", () => {
|
|
13
17
|
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
@@ -37,3 +41,116 @@ describe("apps/mcp/src/index.ts — bootstrap ordering", () => {
|
|
|
37
41
|
expect(initIdx).toBeLessThan(catchIdx);
|
|
38
42
|
});
|
|
39
43
|
});
|
|
44
|
+
|
|
45
|
+
describe("apps/mcp/src/index.ts — the claim refresh loop starts in production (#7156 AC-1)", () => {
|
|
46
|
+
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
47
|
+
|
|
48
|
+
it("starts after the boot read and before the transport branch", () => {
|
|
49
|
+
const initIdx = src.indexOf("await initMcpCommerceClaim()");
|
|
50
|
+
const callMatches = [...src.matchAll(/^startMcpClaimRefresh\(\);$/gm)];
|
|
51
|
+
const transportBranchIdx = src.indexOf('if (transport === "http")');
|
|
52
|
+
|
|
53
|
+
expect(initIdx).toBeGreaterThan(-1);
|
|
54
|
+
expect(callMatches).toHaveLength(1);
|
|
55
|
+
expect(transportBranchIdx).toBeGreaterThan(-1);
|
|
56
|
+
|
|
57
|
+
const callIdx = callMatches[0]!.index!;
|
|
58
|
+
expect(initIdx).toBeLessThan(callIdx);
|
|
59
|
+
expect(callIdx).toBeLessThan(transportBranchIdx);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("apps/mcp/src/index.ts — the boot warning cannot stop the server (#7156 AC-2, D2)", () => {
|
|
64
|
+
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The exact behavioural slice AC-2 specifies: from the last `try {` before
|
|
68
|
+
* the `reportUnlicensedProTools();` call statement, up to `const transport
|
|
69
|
+
* =`. Transpiled and run in a fresh vm context so a real throw from the
|
|
70
|
+
* stubbed function proves the guard, not a mock of the guard itself.
|
|
71
|
+
*/
|
|
72
|
+
function guardedSlice(): string {
|
|
73
|
+
const callMatches = [
|
|
74
|
+
...src.matchAll(/^\s*reportUnlicensedProTools\(\);$/gm),
|
|
75
|
+
];
|
|
76
|
+
expect(callMatches).toHaveLength(1);
|
|
77
|
+
const callIdx = callMatches[0]!.index!;
|
|
78
|
+
const tryIdx = src.lastIndexOf("try {", callIdx);
|
|
79
|
+
const transportIdx = src.indexOf("const transport =");
|
|
80
|
+
|
|
81
|
+
expect(tryIdx).toBeGreaterThan(-1);
|
|
82
|
+
expect(transportIdx).toBeGreaterThan(callIdx);
|
|
83
|
+
|
|
84
|
+
const slice = src.slice(tryIdx, transportIdx);
|
|
85
|
+
// The extracted try block must be the one that immediately wraps the
|
|
86
|
+
// call, not an earlier try{}/await pair (code review, #7156): the two
|
|
87
|
+
// other try blocks in this file each await a boot call, so a regressed
|
|
88
|
+
// boundary would carry "await" into the slice. Asserting that here
|
|
89
|
+
// turns a boundary regression into a clear failure instead of an opaque
|
|
90
|
+
// vm ReferenceError from the un-injected awaited function.
|
|
91
|
+
expect(slice).not.toContain("await");
|
|
92
|
+
|
|
93
|
+
return slice;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runGuardedSlice(
|
|
97
|
+
reportUnlicensedProTools: () => unknown,
|
|
98
|
+
errorSpy: jest.Mock,
|
|
99
|
+
): void {
|
|
100
|
+
const slice = guardedSlice();
|
|
101
|
+
const { outputText } = ts.transpileModule(slice, {
|
|
102
|
+
compilerOptions: {
|
|
103
|
+
module: ts.ModuleKind.CommonJS,
|
|
104
|
+
target: ts.ScriptTarget.ES2020,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
vm.runInNewContext(outputText, {
|
|
108
|
+
reportUnlicensedProTools,
|
|
109
|
+
console: { error: errorSpy },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("a throw is logged to stderr and does not escape", () => {
|
|
114
|
+
const errorSpy = jest.fn();
|
|
115
|
+
|
|
116
|
+
expect(() =>
|
|
117
|
+
runGuardedSlice(() => {
|
|
118
|
+
throw new Error("boom");
|
|
119
|
+
}, errorSpy),
|
|
120
|
+
).not.toThrow();
|
|
121
|
+
|
|
122
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
123
|
+
const call = errorSpy.mock.calls[0]!;
|
|
124
|
+
expect(String(call[0])).toContain("reportUnlicensedProTools");
|
|
125
|
+
const lastArg = call[call.length - 1];
|
|
126
|
+
expect(lastArg).toBeInstanceOf(Error);
|
|
127
|
+
expect((lastArg as Error).message).toBe("boom");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("a successful call is not logged", () => {
|
|
131
|
+
const errorSpy = jest.fn();
|
|
132
|
+
const report = jest.fn(() => []);
|
|
133
|
+
|
|
134
|
+
runGuardedSlice(report, errorSpy);
|
|
135
|
+
|
|
136
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("the call statement occurs exactly once, before the transport branch", () => {
|
|
141
|
+
const callMatches = [
|
|
142
|
+
...src.matchAll(/^\s*reportUnlicensedProTools\(\);$/gm),
|
|
143
|
+
];
|
|
144
|
+
const transportBranchIdx = src.indexOf('if (transport === "http")');
|
|
145
|
+
|
|
146
|
+
expect(callMatches).toHaveLength(1);
|
|
147
|
+
expect(callMatches[0]!.index!).toBeLessThan(transportBranchIdx);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("logs to stderr only — the guarded slice never writes stdout", () => {
|
|
151
|
+
const slice = guardedSlice();
|
|
152
|
+
|
|
153
|
+
expect(slice).toContain("console.error");
|
|
154
|
+
expect(slice).not.toContain("console.log");
|
|
155
|
+
});
|
|
156
|
+
});
|