@atomixstudio/mcp 0.1.0 → 1.0.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 +72 -288
- package/dist/index.d.ts +1 -201
- package/dist/index.js +1288 -2740
- package/dist/index.js.map +1 -1
- package/package.json +30 -30
- package/data/component-tokens-snapshot.json +0 -659
- package/data/tenants/default.json +0 -73
- package/scripts/sync-component-tokens.cjs +0 -974
- package/scripts/sync-component-tokens.js +0 -678
- package/src/ai-rules-generator.ts +0 -1144
- package/src/component-tokens.ts +0 -702
- package/src/index.ts +0 -1155
- package/src/tenant-store.ts +0 -436
- package/src/tokens.ts +0 -208
- package/src/user-tokens.ts +0 -268
- package/src/utils.ts +0 -465
- package/tests/stress-test.cjs +0 -907
- package/tsconfig.json +0 -21
- package/tsup.config.ts +0 -16
package/tests/stress-test.cjs
DELETED
|
@@ -1,907 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Atomix MCP Stress Test Suite
|
|
5
|
-
*
|
|
6
|
-
* Simulates multi-tenant scenarios to validate the build/sync pipeline
|
|
7
|
-
* and MCP server architecture.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node tests/stress-test.cjs [scenario]
|
|
11
|
-
* node tests/stress-test.cjs --all
|
|
12
|
-
* node tests/stress-test.cjs --scenario=5
|
|
13
|
-
*
|
|
14
|
-
* Scenarios:
|
|
15
|
-
* 1. Brand Color Override Storm
|
|
16
|
-
* 2. New Variant Hot-Add
|
|
17
|
-
* 3. Size Matrix Explosion
|
|
18
|
-
* 4. Cross-Component Cascade
|
|
19
|
-
* 5. Concurrent Multi-Tenant Saves
|
|
20
|
-
* 6. Primitive Token Drift
|
|
21
|
-
* 7. Complete Component Overhaul
|
|
22
|
-
* 8. Locked Token Violation
|
|
23
|
-
* 9. AI Tool Rules Generation Load
|
|
24
|
-
* 10. Hot Reload Chain Reaction
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const fs = require("fs");
|
|
28
|
-
const path = require("path");
|
|
29
|
-
const { execSync, spawn } = require("child_process");
|
|
30
|
-
|
|
31
|
-
// ============================================
|
|
32
|
-
// CONFIGURATION
|
|
33
|
-
// ============================================
|
|
34
|
-
|
|
35
|
-
const CONFIG = {
|
|
36
|
-
atomixRoot: path.resolve(__dirname, "../../../"),
|
|
37
|
-
atomixPackage: path.resolve(__dirname, "../../atomix"),
|
|
38
|
-
mcpPackage: path.resolve(__dirname, ".."),
|
|
39
|
-
labPackage: path.resolve(__dirname, "../../atomix-lab"),
|
|
40
|
-
componentDefaultsPath: path.resolve(__dirname, "../../../apps/atomix-lab/src/config/component-defaults.ts"),
|
|
41
|
-
tokenMapPath: path.resolve(__dirname, "../../atomix/dist/token-map.json"),
|
|
42
|
-
componentTokensPath: path.resolve(__dirname, "../src/component-tokens.ts"),
|
|
43
|
-
backupSuffix: ".stress-test-backup",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const COLORS = {
|
|
47
|
-
reset: "\x1b[0m",
|
|
48
|
-
red: "\x1b[31m",
|
|
49
|
-
green: "\x1b[32m",
|
|
50
|
-
yellow: "\x1b[33m",
|
|
51
|
-
blue: "\x1b[34m",
|
|
52
|
-
magenta: "\x1b[35m",
|
|
53
|
-
cyan: "\x1b[36m",
|
|
54
|
-
dim: "\x1b[2m",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// ============================================
|
|
58
|
-
// UTILITIES
|
|
59
|
-
// ============================================
|
|
60
|
-
|
|
61
|
-
function log(message, color = "reset") {
|
|
62
|
-
console.log(`${COLORS[color]}${message}${COLORS.reset}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function logSection(title) {
|
|
66
|
-
console.log("\n" + "=".repeat(60));
|
|
67
|
-
log(` ${title}`, "cyan");
|
|
68
|
-
console.log("=".repeat(60) + "\n");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function logStep(step, message) {
|
|
72
|
-
log(` [${step}] ${message}`, "dim");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function logSuccess(message) {
|
|
76
|
-
log(` ✓ ${message}`, "green");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function logError(message) {
|
|
80
|
-
log(` ✗ ${message}`, "red");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function logWarning(message) {
|
|
84
|
-
log(` ⚠ ${message}`, "yellow");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function timer() {
|
|
88
|
-
const start = Date.now();
|
|
89
|
-
return () => Date.now() - start;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function backup(filePath) {
|
|
93
|
-
const backupPath = filePath + CONFIG.backupSuffix;
|
|
94
|
-
if (fs.existsSync(filePath)) {
|
|
95
|
-
fs.copyFileSync(filePath, backupPath);
|
|
96
|
-
return backupPath;
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function restore(filePath) {
|
|
102
|
-
const backupPath = filePath + CONFIG.backupSuffix;
|
|
103
|
-
if (fs.existsSync(backupPath)) {
|
|
104
|
-
fs.copyFileSync(backupPath, filePath);
|
|
105
|
-
fs.unlinkSync(backupPath);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function runCommand(command, cwd = CONFIG.atomixRoot) {
|
|
112
|
-
try {
|
|
113
|
-
const output = execSync(command, {
|
|
114
|
-
cwd,
|
|
115
|
-
encoding: "utf-8",
|
|
116
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
117
|
-
});
|
|
118
|
-
return { success: true, output };
|
|
119
|
-
} catch (error) {
|
|
120
|
-
return { success: false, output: error.message, error };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function sleep(ms) {
|
|
125
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ============================================
|
|
129
|
-
// TEST HELPERS
|
|
130
|
-
// ============================================
|
|
131
|
-
|
|
132
|
-
function readComponentDefaults() {
|
|
133
|
-
return fs.readFileSync(CONFIG.componentDefaultsPath, "utf-8");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function writeComponentDefaults(content) {
|
|
137
|
-
fs.writeFileSync(CONFIG.componentDefaultsPath, content, "utf-8");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function readTokenMap() {
|
|
141
|
-
if (!fs.existsSync(CONFIG.tokenMapPath)) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
return JSON.parse(fs.readFileSync(CONFIG.tokenMapPath, "utf-8"));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function readComponentTokens() {
|
|
148
|
-
return fs.readFileSync(CONFIG.componentTokensPath, "utf-8");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function modifyButtonVariant(content, variant, property, value) {
|
|
152
|
-
// Simple regex-based modification for testing
|
|
153
|
-
const regex = new RegExp(
|
|
154
|
-
`(${variant}:\\s*\\{[^}]*${property}:\\s*)"([^"]*)"`,
|
|
155
|
-
"g"
|
|
156
|
-
);
|
|
157
|
-
return content.replace(regex, `$1"${value}"`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function addButtonVariant(content, variantName, config) {
|
|
161
|
-
// Find the first variant definition in BUTTON_DEFAULTS and add new variant before it
|
|
162
|
-
// Look for the pattern: variants: {\n primary: {
|
|
163
|
-
const insertPoint = content.indexOf("BUTTON_DEFAULTS");
|
|
164
|
-
if (insertPoint === -1) return content;
|
|
165
|
-
|
|
166
|
-
// Find "variants: {" after BUTTON_DEFAULTS
|
|
167
|
-
const variantsStart = content.indexOf("variants: {", insertPoint);
|
|
168
|
-
if (variantsStart === -1) return content;
|
|
169
|
-
|
|
170
|
-
// Find the opening brace and add after it
|
|
171
|
-
const bracePos = content.indexOf("{", variantsStart + 10);
|
|
172
|
-
if (bracePos === -1) return content;
|
|
173
|
-
|
|
174
|
-
const newVariant = `
|
|
175
|
-
${variantName}: {
|
|
176
|
-
bgColor: "${config.bgColor || "action-primary"}",
|
|
177
|
-
interaction: "${config.interaction || "none"}",
|
|
178
|
-
},`;
|
|
179
|
-
|
|
180
|
-
return content.slice(0, bracePos + 1) + newVariant + content.slice(bracePos + 1);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ============================================
|
|
184
|
-
// SCENARIO TESTS
|
|
185
|
-
// ============================================
|
|
186
|
-
|
|
187
|
-
const scenarios = {
|
|
188
|
-
// Scenario 1: Brand Color Override Storm
|
|
189
|
-
async scenario1() {
|
|
190
|
-
logSection("Scenario 1: Brand Color Override Storm");
|
|
191
|
-
log("Simulating rapid consecutive saves with brand color changes...\n");
|
|
192
|
-
|
|
193
|
-
const elapsed = timer();
|
|
194
|
-
let originalContent;
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
// Backup
|
|
198
|
-
backup(CONFIG.componentDefaultsPath);
|
|
199
|
-
originalContent = readComponentDefaults();
|
|
200
|
-
|
|
201
|
-
// Rapid changes
|
|
202
|
-
logStep(1, "First save: Change primary bgColor");
|
|
203
|
-
let content = modifyButtonVariant(originalContent, "primary", "bgColor", "brand-tenant-blue");
|
|
204
|
-
writeComponentDefaults(content);
|
|
205
|
-
|
|
206
|
-
await sleep(100);
|
|
207
|
-
|
|
208
|
-
logStep(2, "Second save (100ms later): Change secondary bgColor");
|
|
209
|
-
content = modifyButtonVariant(content, "secondary", "bgColor", "brand-tenant-light");
|
|
210
|
-
writeComponentDefaults(content);
|
|
211
|
-
|
|
212
|
-
await sleep(100);
|
|
213
|
-
|
|
214
|
-
logStep(3, "Third save (100ms later): Change outline bgColor");
|
|
215
|
-
content = modifyButtonVariant(content, "outline", "bgColor", "transparent");
|
|
216
|
-
writeComponentDefaults(content);
|
|
217
|
-
|
|
218
|
-
// Run sync
|
|
219
|
-
logStep(4, "Running npm run sync...");
|
|
220
|
-
const syncResult = runCommand("npm run sync", CONFIG.mcpPackage);
|
|
221
|
-
|
|
222
|
-
if (syncResult.success) {
|
|
223
|
-
logSuccess(`Sync completed in ${elapsed()}ms`);
|
|
224
|
-
|
|
225
|
-
// Check for warnings about unknown tokens
|
|
226
|
-
if (syncResult.output.includes("unknown") || syncResult.output.includes("Unknown")) {
|
|
227
|
-
logWarning("Sync reported unknown token warnings (expected for tenant-specific tokens)");
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
logError(`Sync failed: ${syncResult.output}`);
|
|
231
|
-
return { passed: false, error: syncResult.output };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Verify component-tokens.ts was updated
|
|
235
|
-
const componentTokens = readComponentTokens();
|
|
236
|
-
if (componentTokens.includes("brand-tenant-blue") || componentTokens.includes("transparent")) {
|
|
237
|
-
logSuccess("Component tokens reflect changes");
|
|
238
|
-
} else {
|
|
239
|
-
logWarning("Component tokens may not reflect all changes (semantic resolution)");
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
logSuccess(`Scenario 1 completed in ${elapsed()}ms`);
|
|
243
|
-
return { passed: true, time: elapsed() };
|
|
244
|
-
|
|
245
|
-
} finally {
|
|
246
|
-
// Restore
|
|
247
|
-
restore(CONFIG.componentDefaultsPath);
|
|
248
|
-
logStep("cleanup", "Restored original component-defaults.ts");
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
// Scenario 2: New Variant Hot-Add
|
|
253
|
-
async scenario2() {
|
|
254
|
-
logSection("Scenario 2: New Variant Hot-Add");
|
|
255
|
-
log("Adding a new variant dynamically and testing MCP discovery...\n");
|
|
256
|
-
|
|
257
|
-
const elapsed = timer();
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
backup(CONFIG.componentDefaultsPath);
|
|
261
|
-
const originalContent = readComponentDefaults();
|
|
262
|
-
|
|
263
|
-
// Add new variant
|
|
264
|
-
logStep(1, 'Adding new variant "caution" to Button');
|
|
265
|
-
const content = addButtonVariant(originalContent, "caution", {
|
|
266
|
-
bgColor: "feedback-warning",
|
|
267
|
-
interaction: "subtle",
|
|
268
|
-
});
|
|
269
|
-
writeComponentDefaults(content);
|
|
270
|
-
|
|
271
|
-
// Run sync
|
|
272
|
-
logStep(2, "Running npm run sync...");
|
|
273
|
-
const syncResult = runCommand("npm run sync", CONFIG.mcpPackage);
|
|
274
|
-
|
|
275
|
-
if (!syncResult.success) {
|
|
276
|
-
logError(`Sync failed: ${syncResult.output}`);
|
|
277
|
-
return { passed: false, error: syncResult.output };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Check if new variant appears in component-tokens.ts
|
|
281
|
-
logStep(3, "Verifying new variant in component-tokens.ts");
|
|
282
|
-
const componentTokens = readComponentTokens();
|
|
283
|
-
|
|
284
|
-
if (componentTokens.includes("caution")) {
|
|
285
|
-
logSuccess('New variant "caution" discovered and synced');
|
|
286
|
-
} else {
|
|
287
|
-
logError('New variant "caution" not found in component-tokens.ts');
|
|
288
|
-
return { passed: false, error: "Variant not discovered" };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
logSuccess(`Scenario 2 completed in ${elapsed()}ms`);
|
|
292
|
-
return { passed: true, time: elapsed() };
|
|
293
|
-
|
|
294
|
-
} finally {
|
|
295
|
-
restore(CONFIG.componentDefaultsPath);
|
|
296
|
-
logStep("cleanup", "Restored original component-defaults.ts");
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
// Scenario 3: Size Matrix Explosion
|
|
301
|
-
async scenario3() {
|
|
302
|
-
logSection("Scenario 3: Size Matrix Explosion");
|
|
303
|
-
log("Testing token resolution performance with many size variants...\n");
|
|
304
|
-
|
|
305
|
-
const elapsed = timer();
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
// Just test the current token map performance
|
|
309
|
-
logStep(1, "Loading token-map.json");
|
|
310
|
-
const tokenMap = readTokenMap();
|
|
311
|
-
|
|
312
|
-
if (!tokenMap) {
|
|
313
|
-
logError("token-map.json not found. Run npm run build in packages/atomix first.");
|
|
314
|
-
return { passed: false, error: "Token map not found" };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
logSuccess(`Loaded ${tokenMap.tokenCount} tokens`);
|
|
318
|
-
|
|
319
|
-
// Simulate lookups
|
|
320
|
-
logStep(2, "Simulating 1000 token lookups");
|
|
321
|
-
const lookupStart = Date.now();
|
|
322
|
-
const tokenPaths = Object.keys(tokenMap.tokens);
|
|
323
|
-
|
|
324
|
-
for (let i = 0; i < 1000; i++) {
|
|
325
|
-
const randomPath = tokenPaths[Math.floor(Math.random() * tokenPaths.length)];
|
|
326
|
-
const token = tokenMap.tokens[randomPath];
|
|
327
|
-
// Simulate accessing token properties
|
|
328
|
-
const _ = token.cssVar + token.value + token.category;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const lookupTime = Date.now() - lookupStart;
|
|
332
|
-
logSuccess(`1000 lookups completed in ${lookupTime}ms (${(lookupTime / 1000).toFixed(2)}ms per lookup)`);
|
|
333
|
-
|
|
334
|
-
// Check semantic key resolution
|
|
335
|
-
logStep(3, "Testing semantic key resolution");
|
|
336
|
-
const semanticKeys = Object.keys(tokenMap.semanticKeys || {});
|
|
337
|
-
logSuccess(`${semanticKeys.length} semantic keys available`);
|
|
338
|
-
|
|
339
|
-
if (lookupTime < 100) {
|
|
340
|
-
logSuccess(`Scenario 3 completed in ${elapsed()}ms - Performance OK`);
|
|
341
|
-
return { passed: true, time: elapsed() };
|
|
342
|
-
} else {
|
|
343
|
-
logWarning(`Lookups took ${lookupTime}ms - may need optimization`);
|
|
344
|
-
return { passed: true, time: elapsed(), warning: "Performance may need optimization" };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
} catch (error) {
|
|
348
|
-
logError(`Error: ${error.message}`);
|
|
349
|
-
return { passed: false, error: error.message };
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
// Scenario 4: Cross-Component Cascade
|
|
354
|
-
async scenario4() {
|
|
355
|
-
logSection("Scenario 4: Cross-Component Cascade");
|
|
356
|
-
log("Testing that changes to shared components cascade correctly...\n");
|
|
357
|
-
|
|
358
|
-
const elapsed = timer();
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
// Read component tokens and check for shared base usage
|
|
362
|
-
logStep(1, "Analyzing component token structure");
|
|
363
|
-
const componentTokens = readComponentTokens();
|
|
364
|
-
|
|
365
|
-
// Check for HeadingBase usage indicators
|
|
366
|
-
const hasHeadingTokens = componentTokens.includes("heading");
|
|
367
|
-
const hasCardTokens = componentTokens.includes("card");
|
|
368
|
-
const hasDialogTokens = componentTokens.includes("dialog");
|
|
369
|
-
|
|
370
|
-
logSuccess(`Found components: heading=${hasHeadingTokens}, card=${hasCardTokens}, dialog=${hasDialogTokens}`);
|
|
371
|
-
|
|
372
|
-
// Verify token inheritance patterns
|
|
373
|
-
logStep(2, "Checking for typography token patterns");
|
|
374
|
-
const tokenMap = readTokenMap();
|
|
375
|
-
|
|
376
|
-
if (tokenMap) {
|
|
377
|
-
const typographyTokens = Object.keys(tokenMap.tokens).filter(k => k.includes("typography"));
|
|
378
|
-
logSuccess(`${typographyTokens.length} typography tokens available for inheritance`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
logStep(3, "Verifying component manifest includes all base components");
|
|
382
|
-
const defaults = readComponentDefaults();
|
|
383
|
-
const manifestMatch = defaults.match(/COMPONENT_MANIFEST[^}]+\{[\s\S]*?\n\};/);
|
|
384
|
-
|
|
385
|
-
if (manifestMatch) {
|
|
386
|
-
logSuccess("Component manifest found");
|
|
387
|
-
} else {
|
|
388
|
-
logWarning("Component manifest not found - cascade tracking may be limited");
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
logSuccess(`Scenario 4 completed in ${elapsed()}ms`);
|
|
392
|
-
return { passed: true, time: elapsed() };
|
|
393
|
-
|
|
394
|
-
} catch (error) {
|
|
395
|
-
logError(`Error: ${error.message}`);
|
|
396
|
-
return { passed: false, error: error.message };
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
// Scenario 5: Concurrent Multi-Tenant Saves (Simulated)
|
|
401
|
-
async scenario5() {
|
|
402
|
-
logSection("Scenario 5: Concurrent Multi-Tenant Saves");
|
|
403
|
-
log("Simulating concurrent saves from multiple tenants...\n");
|
|
404
|
-
|
|
405
|
-
const elapsed = timer();
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
backup(CONFIG.componentDefaultsPath);
|
|
409
|
-
const originalContent = readComponentDefaults();
|
|
410
|
-
|
|
411
|
-
// Simulate rapid concurrent writes
|
|
412
|
-
logStep(1, "Simulating TenantA save (T=0ms)");
|
|
413
|
-
let contentA = modifyButtonVariant(originalContent, "primary", "bgColor", "tenant-a-primary");
|
|
414
|
-
|
|
415
|
-
logStep(2, "Simulating TenantB save (T=50ms) - concurrent");
|
|
416
|
-
let contentB = modifyButtonVariant(originalContent, "secondary", "bgColor", "tenant-b-secondary");
|
|
417
|
-
|
|
418
|
-
// Write A
|
|
419
|
-
writeComponentDefaults(contentA);
|
|
420
|
-
|
|
421
|
-
// Small delay
|
|
422
|
-
await sleep(50);
|
|
423
|
-
|
|
424
|
-
// Write B (should overwrite A in single-tenant mode)
|
|
425
|
-
writeComponentDefaults(contentB);
|
|
426
|
-
|
|
427
|
-
// Read back
|
|
428
|
-
logStep(3, "Reading final state");
|
|
429
|
-
const finalContent = readComponentDefaults();
|
|
430
|
-
|
|
431
|
-
const hasTenantA = finalContent.includes("tenant-a-primary");
|
|
432
|
-
const hasTenantB = finalContent.includes("tenant-b-secondary");
|
|
433
|
-
|
|
434
|
-
if (hasTenantA && hasTenantB) {
|
|
435
|
-
logSuccess("Both tenant changes preserved (unlikely in current architecture)");
|
|
436
|
-
} else if (hasTenantB && !hasTenantA) {
|
|
437
|
-
logWarning("TenantB overwrote TenantA (expected in single-tenant mode)");
|
|
438
|
-
logWarning("Multi-tenant isolation NOT YET IMPLEMENTED");
|
|
439
|
-
} else if (hasTenantA && !hasTenantB) {
|
|
440
|
-
logWarning("TenantA survived (race condition)");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// This scenario documents the current gap
|
|
444
|
-
logSuccess(`Scenario 5 completed in ${elapsed()}ms`);
|
|
445
|
-
return {
|
|
446
|
-
passed: true,
|
|
447
|
-
time: elapsed(),
|
|
448
|
-
warning: "Multi-tenant isolation requires separate storage per tenant"
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
} finally {
|
|
452
|
-
restore(CONFIG.componentDefaultsPath);
|
|
453
|
-
logStep("cleanup", "Restored original component-defaults.ts");
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
|
|
457
|
-
// Scenario 6: Primitive Token Drift Detection
|
|
458
|
-
async scenario6() {
|
|
459
|
-
logSection("Scenario 6: Primitive Token Drift Detection");
|
|
460
|
-
log("Testing detection of invalid/renamed primitive references...\n");
|
|
461
|
-
|
|
462
|
-
const elapsed = timer();
|
|
463
|
-
|
|
464
|
-
try {
|
|
465
|
-
const tokenMap = readTokenMap();
|
|
466
|
-
|
|
467
|
-
if (!tokenMap) {
|
|
468
|
-
logError("token-map.json not found");
|
|
469
|
-
return { passed: false, error: "Token map not found" };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
logStep(1, "Checking for orphaned semantic keys");
|
|
473
|
-
const semanticKeys = tokenMap.semanticKeys || {};
|
|
474
|
-
const tokenPaths = Object.keys(tokenMap.tokens);
|
|
475
|
-
|
|
476
|
-
let orphanedCount = 0;
|
|
477
|
-
for (const [key, semanticPath] of Object.entries(semanticKeys)) {
|
|
478
|
-
const pathStr = String(semanticPath);
|
|
479
|
-
if (!tokenPaths.includes(pathStr) && !pathStr.startsWith("var(")) {
|
|
480
|
-
orphanedCount++;
|
|
481
|
-
logWarning(`Orphaned semantic key: ${key} -> ${pathStr}`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (orphanedCount === 0) {
|
|
486
|
-
logSuccess("No orphaned semantic keys found");
|
|
487
|
-
} else {
|
|
488
|
-
logWarning(`Found ${orphanedCount} orphaned semantic keys`);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
logStep(2, "Testing invalid token reference");
|
|
492
|
-
const invalidToken = "colors.scales.doesNotExist.500";
|
|
493
|
-
const exists = tokenPaths.includes(invalidToken);
|
|
494
|
-
|
|
495
|
-
if (!exists) {
|
|
496
|
-
logSuccess(`Correctly identified invalid token: ${invalidToken}`);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
logStep(3, "Verifying all mode tokens have counterparts");
|
|
500
|
-
const lightModeTokens = tokenPaths.filter(p => p.includes("modes.light"));
|
|
501
|
-
const darkModeTokens = tokenPaths.filter(p => p.includes("modes.dark"));
|
|
502
|
-
|
|
503
|
-
logSuccess(`Light mode tokens: ${lightModeTokens.length}`);
|
|
504
|
-
logSuccess(`Dark mode tokens: ${darkModeTokens.length}`);
|
|
505
|
-
|
|
506
|
-
if (Math.abs(lightModeTokens.length - darkModeTokens.length) > 5) {
|
|
507
|
-
logWarning("Significant mismatch between light/dark mode token counts");
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
logSuccess(`Scenario 6 completed in ${elapsed()}ms`);
|
|
511
|
-
return { passed: true, time: elapsed() };
|
|
512
|
-
|
|
513
|
-
} catch (error) {
|
|
514
|
-
logError(`Error: ${error.message}`);
|
|
515
|
-
return { passed: false, error: error.message };
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
// Scenario 7: Breaking Change Detection (Simulated)
|
|
520
|
-
async scenario7() {
|
|
521
|
-
logSection("Scenario 7: Breaking Change Detection");
|
|
522
|
-
log("Simulating removal of variants and checking for warnings...\n");
|
|
523
|
-
|
|
524
|
-
const elapsed = timer();
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
// Read current component tokens
|
|
528
|
-
const componentTokens = readComponentTokens();
|
|
529
|
-
|
|
530
|
-
logStep(1, "Identifying existing variants");
|
|
531
|
-
|
|
532
|
-
// Extract variant names from BUTTON_TOKENS
|
|
533
|
-
const buttonMatch = componentTokens.match(/button:\s*\{[\s\S]*?variants:\s*\{([^}]+)\}/);
|
|
534
|
-
if (buttonMatch) {
|
|
535
|
-
const variants = buttonMatch[1].match(/(\w+):/g) || [];
|
|
536
|
-
logSuccess(`Current Button variants: ${variants.map(v => v.replace(":", "")).join(", ")}`);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
logStep(2, "Simulating variant removal detection");
|
|
540
|
-
// This documents what SHOULD happen
|
|
541
|
-
log(" Expected behavior when variants are removed:", "dim");
|
|
542
|
-
log(" - Sync script should detect removed variants", "dim");
|
|
543
|
-
log(" - Warning should be emitted for each removed variant", "dim");
|
|
544
|
-
log(" - MCP validateUsage should return breaking=true", "dim");
|
|
545
|
-
|
|
546
|
-
logWarning("Breaking change detection NOT YET IMPLEMENTED");
|
|
547
|
-
logWarning("Current sync overwrites without comparison");
|
|
548
|
-
|
|
549
|
-
logSuccess(`Scenario 7 completed in ${elapsed()}ms`);
|
|
550
|
-
return {
|
|
551
|
-
passed: true,
|
|
552
|
-
time: elapsed(),
|
|
553
|
-
warning: "Breaking change detection requires diff comparison in sync script"
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
} catch (error) {
|
|
557
|
-
logError(`Error: ${error.message}`);
|
|
558
|
-
return { passed: false, error: error.message };
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
|
|
562
|
-
// Scenario 8: Locked Token Enforcement
|
|
563
|
-
async scenario8() {
|
|
564
|
-
logSection("Scenario 8: Locked Token Enforcement");
|
|
565
|
-
log("Testing locked token validation...\n");
|
|
566
|
-
|
|
567
|
-
const elapsed = timer();
|
|
568
|
-
|
|
569
|
-
try {
|
|
570
|
-
logStep(1, "Checking tenant config for locked tokens");
|
|
571
|
-
const tenantConfigPath = path.join(CONFIG.mcpPackage, "data/tenants/default.json");
|
|
572
|
-
|
|
573
|
-
if (fs.existsSync(tenantConfigPath)) {
|
|
574
|
-
const tenantConfig = JSON.parse(fs.readFileSync(tenantConfigPath, "utf-8"));
|
|
575
|
-
|
|
576
|
-
if (tenantConfig.lockedTokens && tenantConfig.lockedTokens.length > 0) {
|
|
577
|
-
logSuccess(`Tenant config found with ${tenantConfig.lockedTokens.length} locked tokens`);
|
|
578
|
-
|
|
579
|
-
for (const locked of tenantConfig.lockedTokens) {
|
|
580
|
-
log(` - ${locked}`, "dim");
|
|
581
|
-
}
|
|
582
|
-
} else {
|
|
583
|
-
logWarning("No locked tokens configured in tenant config");
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
logWarning("Tenant config not found - creating sample config");
|
|
587
|
-
|
|
588
|
-
// Create sample tenant config
|
|
589
|
-
const tenantDir = path.dirname(tenantConfigPath);
|
|
590
|
-
if (!fs.existsSync(tenantDir)) {
|
|
591
|
-
fs.mkdirSync(tenantDir, { recursive: true });
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const sampleConfig = {
|
|
595
|
-
tenantId: "default",
|
|
596
|
-
createdAt: new Date().toISOString(),
|
|
597
|
-
updatedAt: new Date().toISOString(),
|
|
598
|
-
componentDefaults: {},
|
|
599
|
-
lockedTokens: [
|
|
600
|
-
"button.variants.primary.bgColor",
|
|
601
|
-
"card.variants.*.shadow"
|
|
602
|
-
],
|
|
603
|
-
tokenOverrides: {},
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
fs.writeFileSync(tenantConfigPath, JSON.stringify(sampleConfig, null, 2), "utf-8");
|
|
607
|
-
logSuccess("Created sample tenant config with locked tokens");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
logStep(2, "Verifying TenantStore module exists");
|
|
611
|
-
const tenantStorePath = path.join(CONFIG.mcpPackage, "src/tenant-store.ts");
|
|
612
|
-
|
|
613
|
-
if (fs.existsSync(tenantStorePath)) {
|
|
614
|
-
logSuccess("TenantStore module found");
|
|
615
|
-
|
|
616
|
-
const storeContent = fs.readFileSync(tenantStorePath, "utf-8");
|
|
617
|
-
|
|
618
|
-
// Check for key functions
|
|
619
|
-
const hasTenantClass = storeContent.includes("class TenantStore");
|
|
620
|
-
const hasLockCheck = storeContent.includes("isTokenLocked");
|
|
621
|
-
const hasValidation = storeContent.includes("validateTokenChanges");
|
|
622
|
-
|
|
623
|
-
if (hasTenantClass) logSuccess("TenantStore class implemented");
|
|
624
|
-
if (hasLockCheck) logSuccess("isTokenLocked method implemented");
|
|
625
|
-
if (hasValidation) logSuccess("validateTokenChanges helper implemented");
|
|
626
|
-
} else {
|
|
627
|
-
logWarning("TenantStore module not found");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
logStep(3, "Verifying save API includes lock enforcement");
|
|
631
|
-
const saveApiPath = path.join(CONFIG.labPackage, "src/app/api/save-component-defaults/route.ts");
|
|
632
|
-
|
|
633
|
-
if (fs.existsSync(saveApiPath)) {
|
|
634
|
-
const apiContent = fs.readFileSync(saveApiPath, "utf-8");
|
|
635
|
-
|
|
636
|
-
const hasLockValidation = apiContent.includes("validateLockedTokens") ||
|
|
637
|
-
apiContent.includes("LOCKED TOKEN ENFORCEMENT");
|
|
638
|
-
|
|
639
|
-
if (hasLockValidation) {
|
|
640
|
-
logSuccess("Save API includes locked token validation");
|
|
641
|
-
} else {
|
|
642
|
-
logWarning("Save API may not have locked token validation");
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
logSuccess(`Scenario 8 completed in ${elapsed()}ms`);
|
|
647
|
-
return { passed: true, time: elapsed() };
|
|
648
|
-
|
|
649
|
-
} catch (error) {
|
|
650
|
-
logError(`Error: ${error.message}`);
|
|
651
|
-
return { passed: false, error: error.message };
|
|
652
|
-
}
|
|
653
|
-
},
|
|
654
|
-
|
|
655
|
-
// Scenario 9: AI Tool Rules Generation Load
|
|
656
|
-
async scenario9() {
|
|
657
|
-
logSection("Scenario 9: AI Tool Rules Generation Load");
|
|
658
|
-
log("Testing rules generation performance for multiple tools/projects...\n");
|
|
659
|
-
|
|
660
|
-
const elapsed = timer();
|
|
661
|
-
|
|
662
|
-
try {
|
|
663
|
-
// Import the cursorrules module
|
|
664
|
-
logStep(1, "Loading cursorrules generator");
|
|
665
|
-
|
|
666
|
-
// We can't directly import ES modules in CJS, so we'll test the build output
|
|
667
|
-
logStep(2, "Testing build contains rules generator");
|
|
668
|
-
const distIndexPath = path.join(CONFIG.mcpPackage, "dist", "index.js");
|
|
669
|
-
|
|
670
|
-
if (!fs.existsSync(distIndexPath)) {
|
|
671
|
-
logError("dist/index.js not found. Run npm run build first.");
|
|
672
|
-
return { passed: false, error: "Build not found" };
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const distContent = fs.readFileSync(distIndexPath, "utf-8");
|
|
676
|
-
|
|
677
|
-
const hasRulesGenerator = distContent.includes("getAIToolRules") ||
|
|
678
|
-
distContent.includes("generateRulesForTool");
|
|
679
|
-
|
|
680
|
-
if (hasRulesGenerator) {
|
|
681
|
-
logSuccess("Rules generator functions found in build");
|
|
682
|
-
} else {
|
|
683
|
-
logError("Rules generator functions not found");
|
|
684
|
-
return { passed: false, error: "Generator not found" };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
logStep(3, "Checking AI_TOOLS registry");
|
|
688
|
-
const hasToolRegistry = distContent.includes("AI_TOOLS") || distContent.includes("cursor");
|
|
689
|
-
|
|
690
|
-
if (hasToolRegistry) {
|
|
691
|
-
logSuccess("AI tools registry found");
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
logStep(4, "Estimating generation load");
|
|
695
|
-
log(" 7 AI tools × 50 projects = 350 generations", "dim");
|
|
696
|
-
log(" Expected time: < 30 seconds total", "dim");
|
|
697
|
-
log(" Expected memory: Stable (no leaks)", "dim");
|
|
698
|
-
|
|
699
|
-
logSuccess(`Scenario 9 completed in ${elapsed()}ms`);
|
|
700
|
-
return { passed: true, time: elapsed() };
|
|
701
|
-
|
|
702
|
-
} catch (error) {
|
|
703
|
-
logError(`Error: ${error.message}`);
|
|
704
|
-
return { passed: false, error: error.message };
|
|
705
|
-
}
|
|
706
|
-
},
|
|
707
|
-
|
|
708
|
-
// Scenario 10: Hot Reload Chain Reaction
|
|
709
|
-
async scenario10() {
|
|
710
|
-
logSection("Scenario 10: Hot Reload Chain Reaction");
|
|
711
|
-
log("Testing rapid consecutive saves and final state consistency...\n");
|
|
712
|
-
|
|
713
|
-
const elapsed = timer();
|
|
714
|
-
|
|
715
|
-
try {
|
|
716
|
-
backup(CONFIG.componentDefaultsPath);
|
|
717
|
-
const originalContent = readComponentDefaults();
|
|
718
|
-
|
|
719
|
-
// Rapid fire changes
|
|
720
|
-
const changes = [
|
|
721
|
-
{ variant: "primary", property: "bgColor", value: "change-1" },
|
|
722
|
-
{ variant: "secondary", property: "bgColor", value: "change-2" },
|
|
723
|
-
{ variant: "outline", property: "bgColor", value: "change-3" },
|
|
724
|
-
{ variant: "ghost", property: "bgColor", value: "change-4" },
|
|
725
|
-
];
|
|
726
|
-
|
|
727
|
-
logStep(1, "Executing 4 rapid saves (100ms apart)");
|
|
728
|
-
|
|
729
|
-
let content = originalContent;
|
|
730
|
-
for (let i = 0; i < changes.length; i++) {
|
|
731
|
-
const { variant, property, value } = changes[i];
|
|
732
|
-
content = modifyButtonVariant(content, variant, property, value);
|
|
733
|
-
writeComponentDefaults(content);
|
|
734
|
-
log(` Save ${i + 1}: ${variant}.${property} = ${value}`, "dim");
|
|
735
|
-
await sleep(100);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Read final state
|
|
739
|
-
logStep(2, "Verifying final state");
|
|
740
|
-
const finalContent = readComponentDefaults();
|
|
741
|
-
|
|
742
|
-
let allChangesPresent = true;
|
|
743
|
-
for (const { value } of changes) {
|
|
744
|
-
if (!finalContent.includes(value)) {
|
|
745
|
-
allChangesPresent = false;
|
|
746
|
-
logError(`Missing change: ${value}`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (allChangesPresent) {
|
|
751
|
-
logSuccess("All 4 changes present in final state");
|
|
752
|
-
} else {
|
|
753
|
-
logError("Some changes were lost");
|
|
754
|
-
return { passed: false, error: "Changes lost during rapid saves" };
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Run sync and verify
|
|
758
|
-
logStep(3, "Running sync after rapid changes");
|
|
759
|
-
const syncResult = runCommand("npm run sync", CONFIG.mcpPackage);
|
|
760
|
-
|
|
761
|
-
if (syncResult.success) {
|
|
762
|
-
logSuccess("Sync completed successfully");
|
|
763
|
-
} else {
|
|
764
|
-
logError(`Sync failed: ${syncResult.output}`);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
logSuccess(`Scenario 10 completed in ${elapsed()}ms`);
|
|
768
|
-
return { passed: true, time: elapsed() };
|
|
769
|
-
|
|
770
|
-
} finally {
|
|
771
|
-
restore(CONFIG.componentDefaultsPath);
|
|
772
|
-
logStep("cleanup", "Restored original component-defaults.ts");
|
|
773
|
-
}
|
|
774
|
-
},
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
// ============================================
|
|
778
|
-
// TEST RUNNER
|
|
779
|
-
// ============================================
|
|
780
|
-
|
|
781
|
-
async function runAllScenarios() {
|
|
782
|
-
logSection("ATOMIX MCP STRESS TEST SUITE");
|
|
783
|
-
log("Running all 10 multi-tenant stress test scenarios...\n");
|
|
784
|
-
|
|
785
|
-
const results = {};
|
|
786
|
-
const totalElapsed = timer();
|
|
787
|
-
|
|
788
|
-
for (let i = 1; i <= 10; i++) {
|
|
789
|
-
const scenarioName = `scenario${i}`;
|
|
790
|
-
try {
|
|
791
|
-
results[scenarioName] = await scenarios[scenarioName]();
|
|
792
|
-
} catch (error) {
|
|
793
|
-
results[scenarioName] = { passed: false, error: error.message };
|
|
794
|
-
logError(`Scenario ${i} crashed: ${error.message}`);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Small delay between scenarios
|
|
798
|
-
await sleep(500);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Summary
|
|
802
|
-
logSection("TEST SUMMARY");
|
|
803
|
-
|
|
804
|
-
let passed = 0;
|
|
805
|
-
let failed = 0;
|
|
806
|
-
let warnings = 0;
|
|
807
|
-
|
|
808
|
-
for (let i = 1; i <= 10; i++) {
|
|
809
|
-
const result = results[`scenario${i}`];
|
|
810
|
-
const status = result.passed ? "PASS" : "FAIL";
|
|
811
|
-
const color = result.passed ? "green" : "red";
|
|
812
|
-
const time = result.time ? ` (${result.time}ms)` : "";
|
|
813
|
-
const warning = result.warning ? ` ⚠ ${result.warning}` : "";
|
|
814
|
-
|
|
815
|
-
log(` Scenario ${i}: ${status}${time}${warning}`, color);
|
|
816
|
-
|
|
817
|
-
if (result.passed) {
|
|
818
|
-
passed++;
|
|
819
|
-
if (result.warning) warnings++;
|
|
820
|
-
} else {
|
|
821
|
-
failed++;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
console.log("\n" + "-".repeat(60));
|
|
826
|
-
log(` Total: ${passed} passed, ${failed} failed, ${warnings} warnings`, passed === 10 ? "green" : "yellow");
|
|
827
|
-
log(` Time: ${totalElapsed()}ms`, "dim");
|
|
828
|
-
console.log("-".repeat(60) + "\n");
|
|
829
|
-
|
|
830
|
-
return failed === 0;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
async function runScenario(num) {
|
|
834
|
-
const scenarioName = `scenario${num}`;
|
|
835
|
-
if (!scenarios[scenarioName]) {
|
|
836
|
-
logError(`Unknown scenario: ${num}`);
|
|
837
|
-
process.exit(1);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
try {
|
|
841
|
-
const result = await scenarios[scenarioName]();
|
|
842
|
-
return result.passed;
|
|
843
|
-
} catch (error) {
|
|
844
|
-
logError(`Scenario ${num} crashed: ${error.message}`);
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// ============================================
|
|
850
|
-
// CLI
|
|
851
|
-
// ============================================
|
|
852
|
-
|
|
853
|
-
async function main() {
|
|
854
|
-
const args = process.argv.slice(2);
|
|
855
|
-
|
|
856
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
857
|
-
console.log(`
|
|
858
|
-
Atomix MCP Stress Test Suite
|
|
859
|
-
|
|
860
|
-
Usage:
|
|
861
|
-
node tests/stress-test.cjs Run all scenarios
|
|
862
|
-
node tests/stress-test.cjs --all Run all scenarios
|
|
863
|
-
node tests/stress-test.cjs --scenario=5 Run specific scenario
|
|
864
|
-
node tests/stress-test.cjs 3 Run scenario 3
|
|
865
|
-
|
|
866
|
-
Scenarios:
|
|
867
|
-
1. Brand Color Override Storm
|
|
868
|
-
2. New Variant Hot-Add
|
|
869
|
-
3. Size Matrix Explosion
|
|
870
|
-
4. Cross-Component Cascade
|
|
871
|
-
5. Concurrent Multi-Tenant Saves
|
|
872
|
-
6. Primitive Token Drift
|
|
873
|
-
7. Breaking Change Detection
|
|
874
|
-
8. Locked Token Enforcement
|
|
875
|
-
9. AI Tool Rules Generation Load
|
|
876
|
-
10. Hot Reload Chain Reaction
|
|
877
|
-
`);
|
|
878
|
-
process.exit(0);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Check prerequisites
|
|
882
|
-
if (!fs.existsSync(CONFIG.componentDefaultsPath)) {
|
|
883
|
-
logError("component-defaults.ts not found. Is Atomix Lab installed?");
|
|
884
|
-
process.exit(1);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
let success;
|
|
888
|
-
|
|
889
|
-
const scenarioArg = args.find(a => a.startsWith("--scenario="));
|
|
890
|
-
if (scenarioArg) {
|
|
891
|
-
const num = parseInt(scenarioArg.split("=")[1], 10);
|
|
892
|
-
success = await runScenario(num);
|
|
893
|
-
} else if (args[0] && !args[0].startsWith("-")) {
|
|
894
|
-
const num = parseInt(args[0], 10);
|
|
895
|
-
success = await runScenario(num);
|
|
896
|
-
} else {
|
|
897
|
-
success = await runAllScenarios();
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
process.exit(success ? 0 : 1);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
main().catch(error => {
|
|
904
|
-
logError(`Fatal error: ${error.message}`);
|
|
905
|
-
process.exit(1);
|
|
906
|
-
});
|
|
907
|
-
|