@mandujs/core 0.17.0 → 0.18.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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/paths.ts +16 -0
- package/src/resource/__tests__/backward-compat.test.ts +302 -0
- package/src/resource/__tests__/edge-cases.test.ts +514 -0
- package/src/resource/__tests__/fixtures.ts +203 -0
- package/src/resource/__tests__/generator.test.ts +324 -0
- package/src/resource/__tests__/performance.test.ts +311 -0
- package/src/resource/__tests__/schema.test.ts +184 -0
- package/src/resource/generator.ts +277 -0
- package/src/resource/generators/client.ts +199 -0
- package/src/resource/generators/contract.ts +264 -0
- package/src/resource/generators/slot.ts +193 -0
- package/src/resource/generators/types.ts +83 -0
- package/src/resource/index.ts +42 -0
- package/src/resource/parser.ts +139 -0
- package/src/resource/schema.ts +252 -0
- package/src/router/fs-scanner.ts +21 -6
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Generator Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
6
|
+
import { generateResourceArtifacts } from "../generator";
|
|
7
|
+
import type { ParsedResource } from "../parser";
|
|
8
|
+
import { resolveGeneratedPaths } from "../../paths";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import os from "os";
|
|
12
|
+
|
|
13
|
+
// Test utilities
|
|
14
|
+
let testDir: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
// Create temporary test directory
|
|
18
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-resource-test-"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
// Clean up test directory
|
|
23
|
+
try {
|
|
24
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Ignore cleanup errors
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a test parsed resource (no file import needed)
|
|
32
|
+
*/
|
|
33
|
+
function createTestParsedResource(resourceName: string, definition: any): ParsedResource {
|
|
34
|
+
return {
|
|
35
|
+
definition,
|
|
36
|
+
filePath: path.join(testDir, "spec", "resources", `${resourceName}.resource.ts`),
|
|
37
|
+
fileName: resourceName,
|
|
38
|
+
resourceName: definition.name,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if file exists
|
|
44
|
+
*/
|
|
45
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(filePath);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("generateResourceArtifacts", () => {
|
|
55
|
+
test("should generate all artifacts for a resource", async () => {
|
|
56
|
+
// Create test resource definition
|
|
57
|
+
const parsed = createTestParsedResource("user", {
|
|
58
|
+
name: "user",
|
|
59
|
+
fields: {
|
|
60
|
+
id: { type: "uuid", required: true },
|
|
61
|
+
email: { type: "email", required: true },
|
|
62
|
+
name: { type: "string", required: true },
|
|
63
|
+
createdAt: { type: "date", required: true },
|
|
64
|
+
},
|
|
65
|
+
options: {
|
|
66
|
+
description: "User management API",
|
|
67
|
+
tags: ["users"],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Generate artifacts
|
|
72
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
73
|
+
rootDir: testDir,
|
|
74
|
+
force: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Verify result
|
|
78
|
+
expect(result.success).toBe(true);
|
|
79
|
+
expect(result.errors.length).toBe(0);
|
|
80
|
+
expect(result.created.length).toBeGreaterThan(0);
|
|
81
|
+
|
|
82
|
+
// Verify files were created
|
|
83
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
84
|
+
|
|
85
|
+
const contractPath = path.join(paths.resourceContractsDir, "user.contract.ts");
|
|
86
|
+
const typesPath = path.join(paths.resourceTypesDir, "user.types.ts");
|
|
87
|
+
const slotPath = path.join(paths.resourceSlotsDir, "user.slot.ts");
|
|
88
|
+
const clientPath = path.join(paths.resourceClientDir, "user.client.ts");
|
|
89
|
+
|
|
90
|
+
expect(await fileExists(contractPath)).toBe(true);
|
|
91
|
+
expect(await fileExists(typesPath)).toBe(true);
|
|
92
|
+
expect(await fileExists(slotPath)).toBe(true);
|
|
93
|
+
expect(await fileExists(clientPath)).toBe(true);
|
|
94
|
+
|
|
95
|
+
// Verify created list includes all files
|
|
96
|
+
expect(result.created).toContain(contractPath);
|
|
97
|
+
expect(result.created).toContain(typesPath);
|
|
98
|
+
expect(result.created).toContain(slotPath);
|
|
99
|
+
expect(result.created).toContain(clientPath);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("should preserve existing slot without --force", async () => {
|
|
103
|
+
// Create test resource
|
|
104
|
+
const parsed = createTestParsedResource("post", {
|
|
105
|
+
name: "post",
|
|
106
|
+
fields: {
|
|
107
|
+
id: { type: "uuid", required: true },
|
|
108
|
+
title: { type: "string", required: true },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// First generation
|
|
113
|
+
const result1 = await generateResourceArtifacts(parsed, {
|
|
114
|
+
rootDir: testDir,
|
|
115
|
+
force: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result1.success).toBe(true);
|
|
119
|
+
|
|
120
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
121
|
+
const slotPath = path.join(paths.resourceSlotsDir, "post.slot.ts");
|
|
122
|
+
|
|
123
|
+
// Read original slot content
|
|
124
|
+
const originalContent = await fs.readFile(slotPath, "utf-8");
|
|
125
|
+
|
|
126
|
+
// Modify slot file
|
|
127
|
+
const modifiedContent = `${originalContent}\n// Custom modification`;
|
|
128
|
+
await fs.writeFile(slotPath, modifiedContent);
|
|
129
|
+
|
|
130
|
+
// Second generation (should preserve slot)
|
|
131
|
+
const result2 = await generateResourceArtifacts(parsed, {
|
|
132
|
+
rootDir: testDir,
|
|
133
|
+
force: false,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result2.success).toBe(true);
|
|
137
|
+
expect(result2.skipped).toContain(slotPath);
|
|
138
|
+
expect(result2.created).not.toContain(slotPath);
|
|
139
|
+
|
|
140
|
+
// Verify slot was preserved
|
|
141
|
+
const currentContent = await fs.readFile(slotPath, "utf-8");
|
|
142
|
+
expect(currentContent).toBe(modifiedContent);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("should overwrite slot with --force", async () => {
|
|
146
|
+
// Create test resource
|
|
147
|
+
const parsed = createTestParsedResource("product", {
|
|
148
|
+
name: "product",
|
|
149
|
+
fields: {
|
|
150
|
+
id: { type: "uuid", required: true },
|
|
151
|
+
name: { type: "string", required: true },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// First generation
|
|
156
|
+
await generateResourceArtifacts(parsed, {
|
|
157
|
+
rootDir: testDir,
|
|
158
|
+
force: false,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
162
|
+
const slotPath = path.join(paths.resourceSlotsDir, "product.slot.ts");
|
|
163
|
+
|
|
164
|
+
// Modify slot file
|
|
165
|
+
await fs.writeFile(slotPath, "// Custom content");
|
|
166
|
+
|
|
167
|
+
// Second generation with --force
|
|
168
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
169
|
+
rootDir: testDir,
|
|
170
|
+
force: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result.success).toBe(true);
|
|
174
|
+
expect(result.created).toContain(slotPath);
|
|
175
|
+
expect(result.skipped).not.toContain(slotPath);
|
|
176
|
+
|
|
177
|
+
// Verify slot was overwritten
|
|
178
|
+
const currentContent = await fs.readFile(slotPath, "utf-8");
|
|
179
|
+
expect(currentContent).not.toBe("// Custom content");
|
|
180
|
+
expect(currentContent).toContain("Mandu Filling");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("should regenerate contract, types, and client on every run", async () => {
|
|
184
|
+
// Create test resource
|
|
185
|
+
const parsed = createTestParsedResource("item", {
|
|
186
|
+
name: "item",
|
|
187
|
+
fields: {
|
|
188
|
+
id: { type: "uuid", required: true },
|
|
189
|
+
name: { type: "string", required: true },
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
194
|
+
|
|
195
|
+
// First generation
|
|
196
|
+
await generateResourceArtifacts(parsed, {
|
|
197
|
+
rootDir: testDir,
|
|
198
|
+
force: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const contractPath = path.join(paths.resourceContractsDir, "item.contract.ts");
|
|
202
|
+
const typesPath = path.join(paths.resourceTypesDir, "item.types.ts");
|
|
203
|
+
const clientPath = path.join(paths.resourceClientDir, "item.client.ts");
|
|
204
|
+
|
|
205
|
+
// Modify generated files
|
|
206
|
+
await fs.writeFile(contractPath, "// Modified contract");
|
|
207
|
+
await fs.writeFile(typesPath, "// Modified types");
|
|
208
|
+
await fs.writeFile(clientPath, "// Modified client");
|
|
209
|
+
|
|
210
|
+
// Second generation
|
|
211
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
212
|
+
rootDir: testDir,
|
|
213
|
+
force: false,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(true);
|
|
217
|
+
|
|
218
|
+
// Verify files were regenerated
|
|
219
|
+
const contractContent = await fs.readFile(contractPath, "utf-8");
|
|
220
|
+
const typesContent = await fs.readFile(typesPath, "utf-8");
|
|
221
|
+
const clientContent = await fs.readFile(clientPath, "utf-8");
|
|
222
|
+
|
|
223
|
+
expect(contractContent).not.toBe("// Modified contract");
|
|
224
|
+
expect(typesContent).not.toBe("// Modified types");
|
|
225
|
+
expect(clientContent).not.toBe("// Modified client");
|
|
226
|
+
|
|
227
|
+
expect(contractContent).toContain("Mandu.contract");
|
|
228
|
+
expect(typesContent).toContain("InferContract");
|
|
229
|
+
expect(clientContent).toContain("Client");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("should support 'only' option to generate specific files", async () => {
|
|
233
|
+
// Create test resource
|
|
234
|
+
const parsed = createTestParsedResource("category", {
|
|
235
|
+
name: "category",
|
|
236
|
+
fields: {
|
|
237
|
+
id: { type: "uuid", required: true },
|
|
238
|
+
name: { type: "string", required: true },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Generate only contract and types
|
|
243
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
244
|
+
rootDir: testDir,
|
|
245
|
+
force: false,
|
|
246
|
+
only: ["contract", "types"],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(result.success).toBe(true);
|
|
250
|
+
|
|
251
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
252
|
+
|
|
253
|
+
const contractPath = path.join(paths.resourceContractsDir, "category.contract.ts");
|
|
254
|
+
const typesPath = path.join(paths.resourceTypesDir, "category.types.ts");
|
|
255
|
+
const slotPath = path.join(paths.resourceSlotsDir, "category.slot.ts");
|
|
256
|
+
const clientPath = path.join(paths.resourceClientDir, "category.client.ts");
|
|
257
|
+
|
|
258
|
+
// Only contract and types should exist
|
|
259
|
+
expect(await fileExists(contractPath)).toBe(true);
|
|
260
|
+
expect(await fileExists(typesPath)).toBe(true);
|
|
261
|
+
expect(await fileExists(slotPath)).toBe(false);
|
|
262
|
+
expect(await fileExists(clientPath)).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("Generated Content Validation", () => {
|
|
267
|
+
test("contract should contain Mandu.contract definition", async () => {
|
|
268
|
+
const parsed = createTestParsedResource("test", {
|
|
269
|
+
name: "test",
|
|
270
|
+
fields: {
|
|
271
|
+
id: { type: "uuid", required: true },
|
|
272
|
+
name: { type: "string", required: true },
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await generateResourceArtifacts(parsed, {
|
|
277
|
+
rootDir: testDir,
|
|
278
|
+
force: false,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
282
|
+
const contractPath = path.join(paths.resourceContractsDir, "test.contract.ts");
|
|
283
|
+
const contractContent = await fs.readFile(contractPath, "utf-8");
|
|
284
|
+
|
|
285
|
+
expect(contractContent).toContain("Mandu.contract");
|
|
286
|
+
expect(contractContent).toContain("z.object");
|
|
287
|
+
expect(contractContent).toContain("TestSchema");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("types should export TypeScript types", async () => {
|
|
291
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
292
|
+
const typesPath = path.join(paths.resourceTypesDir, "test.types.ts");
|
|
293
|
+
const typesContent = await fs.readFile(typesPath, "utf-8");
|
|
294
|
+
|
|
295
|
+
expect(typesContent).toContain("InferContract");
|
|
296
|
+
expect(typesContent).toContain("InferQuery");
|
|
297
|
+
expect(typesContent).toContain("InferBody");
|
|
298
|
+
expect(typesContent).toContain("export type");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("slot should contain Mandu.filling definition", async () => {
|
|
302
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
303
|
+
const slotPath = path.join(paths.resourceSlotsDir, "test.slot.ts");
|
|
304
|
+
const slotContent = await fs.readFile(slotPath, "utf-8");
|
|
305
|
+
|
|
306
|
+
expect(slotContent).toContain("Mandu.filling()");
|
|
307
|
+
expect(slotContent).toContain(".get(");
|
|
308
|
+
expect(slotContent).toContain(".post(");
|
|
309
|
+
expect(slotContent).toContain("ctx.input");
|
|
310
|
+
expect(slotContent).toContain("ctx.output");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("client should export Client class", async () => {
|
|
314
|
+
const paths = resolveGeneratedPaths(testDir);
|
|
315
|
+
const clientPath = path.join(paths.resourceClientDir, "test.client.ts");
|
|
316
|
+
const clientContent = await fs.readFile(clientPath, "utf-8");
|
|
317
|
+
|
|
318
|
+
expect(clientContent).toContain("export class");
|
|
319
|
+
expect(clientContent).toContain("Client");
|
|
320
|
+
expect(clientContent).toContain("async list(");
|
|
321
|
+
expect(clientContent).toContain("async get(");
|
|
322
|
+
expect(clientContent).toContain("async create(");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Performance Tests
|
|
3
|
+
*
|
|
4
|
+
* QA Engineer: Performance benchmarks for resource generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
8
|
+
import { generateResourceArtifacts } from "../generator";
|
|
9
|
+
import type { ParsedResource } from "../parser";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import os from "os";
|
|
13
|
+
|
|
14
|
+
// Test utilities
|
|
15
|
+
let testDir: string;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-perf-test-"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
try {
|
|
23
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// Ignore cleanup errors
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a test parsed resource
|
|
31
|
+
*/
|
|
32
|
+
function createTestParsedResource(resourceName: string, definition: any): ParsedResource {
|
|
33
|
+
return {
|
|
34
|
+
definition,
|
|
35
|
+
filePath: path.join(testDir, "spec", "resources", `${resourceName}.resource.ts`),
|
|
36
|
+
fileName: resourceName,
|
|
37
|
+
resourceName: definition.name,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Measure execution time
|
|
43
|
+
*/
|
|
44
|
+
async function measureTime<T>(fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
const result = await fn();
|
|
47
|
+
const duration = performance.now() - start;
|
|
48
|
+
return { result, duration };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("Performance - Resource Generation", () => {
|
|
52
|
+
test("should generate single resource in < 500ms", async () => {
|
|
53
|
+
const parsed = createTestParsedResource("user", {
|
|
54
|
+
name: "user",
|
|
55
|
+
fields: {
|
|
56
|
+
id: { type: "uuid", required: true },
|
|
57
|
+
email: { type: "email", required: true },
|
|
58
|
+
name: { type: "string", required: true },
|
|
59
|
+
createdAt: { type: "date", required: true },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { result, duration } = await measureTime(() =>
|
|
64
|
+
generateResourceArtifacts(parsed, { rootDir: testDir, force: false })
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.success).toBe(true);
|
|
68
|
+
expect(duration).toBeLessThan(500);
|
|
69
|
+
console.log(` ⚡ Single resource generation: ${duration.toFixed(2)}ms`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("should handle resource with 50 fields in < 1000ms", async () => {
|
|
73
|
+
const fields: Record<string, any> = {};
|
|
74
|
+
for (let i = 0; i < 50; i++) {
|
|
75
|
+
fields[`field${i}`] = {
|
|
76
|
+
type: i % 5 === 0 ? "number" : "string",
|
|
77
|
+
required: i % 2 === 0,
|
|
78
|
+
default: i % 3 === 0 ? `value${i}` : undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parsed = createTestParsedResource("largescale", {
|
|
83
|
+
name: "largescale",
|
|
84
|
+
fields,
|
|
85
|
+
options: {
|
|
86
|
+
description: "Resource with 50 fields",
|
|
87
|
+
tags: Array.from({ length: 10 }, (_, i) => `tag${i}`),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { result, duration } = await measureTime(() =>
|
|
92
|
+
generateResourceArtifacts(parsed, { rootDir: testDir, force: false })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(result.success).toBe(true);
|
|
96
|
+
expect(duration).toBeLessThan(1000);
|
|
97
|
+
console.log(` ⚡ 50-field resource generation: ${duration.toFixed(2)}ms`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should generate 10 resources sequentially in < 3000ms", async () => {
|
|
101
|
+
const resources: ParsedResource[] = [];
|
|
102
|
+
for (let i = 0; i < 10; i++) {
|
|
103
|
+
resources.push(
|
|
104
|
+
createTestParsedResource(`resource${i}`, {
|
|
105
|
+
name: `resource${i}`,
|
|
106
|
+
fields: {
|
|
107
|
+
id: { type: "uuid", required: true },
|
|
108
|
+
name: { type: "string", required: true },
|
|
109
|
+
count: { type: "number", required: false, default: 0 },
|
|
110
|
+
isActive: { type: "boolean", required: false, default: true },
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { duration } = await measureTime(async () => {
|
|
117
|
+
for (const parsed of resources) {
|
|
118
|
+
await generateResourceArtifacts(parsed, { rootDir: testDir, force: false });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(duration).toBeLessThan(3000);
|
|
123
|
+
console.log(` ⚡ 10 resources sequential: ${duration.toFixed(2)}ms`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should regenerate existing resource (with slot preservation) in < 200ms", async () => {
|
|
127
|
+
const parsed = createTestParsedResource("existingresource", {
|
|
128
|
+
name: "existingresource",
|
|
129
|
+
fields: {
|
|
130
|
+
id: { type: "uuid", required: true },
|
|
131
|
+
name: { type: "string", required: true },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// First generation
|
|
136
|
+
await generateResourceArtifacts(parsed, { rootDir: testDir, force: false });
|
|
137
|
+
|
|
138
|
+
// Second generation (with slot preservation)
|
|
139
|
+
const { result, duration } = await measureTime(() =>
|
|
140
|
+
generateResourceArtifacts(parsed, { rootDir: testDir, force: false })
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(duration).toBeLessThan(200);
|
|
145
|
+
console.log(` ⚡ Regeneration with slot preservation: ${duration.toFixed(2)}ms`);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Performance - Schema Validation", () => {
|
|
150
|
+
test("should validate simple schema in < 10ms", async () => {
|
|
151
|
+
const { validateResourceDefinition } = await import("../schema");
|
|
152
|
+
|
|
153
|
+
const definition = {
|
|
154
|
+
name: "simple",
|
|
155
|
+
fields: {
|
|
156
|
+
id: { type: "uuid" as const, required: true },
|
|
157
|
+
name: { type: "string" as const, required: true },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const { duration } = await measureTime(async () => {
|
|
162
|
+
validateResourceDefinition(definition);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(duration).toBeLessThan(10);
|
|
166
|
+
console.log(` ⚡ Simple schema validation: ${duration.toFixed(2)}ms`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should validate complex schema (50 fields) in < 50ms", async () => {
|
|
170
|
+
const { validateResourceDefinition } = await import("../schema");
|
|
171
|
+
|
|
172
|
+
const fields: Record<string, any> = {};
|
|
173
|
+
for (let i = 0; i < 50; i++) {
|
|
174
|
+
fields[`field${i}`] = {
|
|
175
|
+
type: (["string", "number", "boolean", "date", "email"] as const)[i % 5],
|
|
176
|
+
required: i % 2 === 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const definition = {
|
|
181
|
+
name: "complex",
|
|
182
|
+
fields,
|
|
183
|
+
options: {
|
|
184
|
+
description: "Complex schema with 50 fields",
|
|
185
|
+
tags: Array.from({ length: 20 }, (_, i) => `tag${i}`),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const { duration } = await measureTime(async () => {
|
|
190
|
+
validateResourceDefinition(definition);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(duration).toBeLessThan(50);
|
|
194
|
+
console.log(` ⚡ Complex schema (50 fields) validation: ${duration.toFixed(2)}ms`);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Performance - Memory Usage", () => {
|
|
199
|
+
test("should handle 20 resources without memory issues", async () => {
|
|
200
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < 20; i++) {
|
|
203
|
+
const parsed = createTestParsedResource(`memtest${i}`, {
|
|
204
|
+
name: `memtest${i}`,
|
|
205
|
+
fields: {
|
|
206
|
+
id: { type: "uuid", required: true },
|
|
207
|
+
name: { type: "string", required: true },
|
|
208
|
+
description: { type: "string", required: false },
|
|
209
|
+
count: { type: "number", default: 0 },
|
|
210
|
+
tags: { type: "array", items: "string", default: [] },
|
|
211
|
+
metadata: { type: "object", default: {} },
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await generateResourceArtifacts(parsed, { rootDir: testDir, force: false });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
219
|
+
const memoryIncrease = (finalMemory - initialMemory) / 1024 / 1024; // MB
|
|
220
|
+
|
|
221
|
+
console.log(` 💾 Memory increase for 20 resources: ${memoryIncrease.toFixed(2)}MB`);
|
|
222
|
+
|
|
223
|
+
// Expect reasonable memory usage (< 50MB for 20 resources)
|
|
224
|
+
expect(memoryIncrease).toBeLessThan(50);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Performance - File I/O", () => {
|
|
229
|
+
test("should handle rapid file writes efficiently", async () => {
|
|
230
|
+
const parsed = createTestParsedResource("iotest", {
|
|
231
|
+
name: "iotest",
|
|
232
|
+
fields: {
|
|
233
|
+
id: { type: "uuid", required: true },
|
|
234
|
+
data: { type: "string", required: true },
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Generate 5 times in rapid succession
|
|
239
|
+
const { duration } = await measureTime(async () => {
|
|
240
|
+
for (let i = 0; i < 5; i++) {
|
|
241
|
+
await generateResourceArtifacts(parsed, { rootDir: testDir, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(duration).toBeLessThan(2000);
|
|
246
|
+
console.log(` ⚡ 5 rapid regenerations (--force): ${duration.toFixed(2)}ms`);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("Performance - Comparison Benchmarks", () => {
|
|
251
|
+
test("benchmark: minimal vs medium vs large resource", async () => {
|
|
252
|
+
// Minimal resource
|
|
253
|
+
const minimal = createTestParsedResource("minimal", {
|
|
254
|
+
name: "minimal",
|
|
255
|
+
fields: {
|
|
256
|
+
id: { type: "uuid", required: true },
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { duration: minimalTime } = await measureTime(() =>
|
|
261
|
+
generateResourceArtifacts(minimal, { rootDir: testDir, force: false })
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Medium resource
|
|
265
|
+
const medium = createTestParsedResource("medium", {
|
|
266
|
+
name: "medium",
|
|
267
|
+
fields: {
|
|
268
|
+
id: { type: "uuid", required: true },
|
|
269
|
+
name: { type: "string", required: true },
|
|
270
|
+
email: { type: "email", required: true },
|
|
271
|
+
age: { type: "number", required: false },
|
|
272
|
+
isActive: { type: "boolean", default: true },
|
|
273
|
+
tags: { type: "array", items: "string", default: [] },
|
|
274
|
+
metadata: { type: "object", default: {} },
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const { duration: mediumTime } = await measureTime(() =>
|
|
279
|
+
generateResourceArtifacts(medium, { rootDir: testDir, force: false })
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Large resource
|
|
283
|
+
const largeFields: Record<string, any> = {};
|
|
284
|
+
for (let i = 0; i < 30; i++) {
|
|
285
|
+
largeFields[`field${i}`] = {
|
|
286
|
+
type: (["string", "number", "boolean"] as const)[i % 3],
|
|
287
|
+
required: i % 2 === 0,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const large = createTestParsedResource("large", {
|
|
292
|
+
name: "large",
|
|
293
|
+
fields: largeFields,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const { duration: largeTime } = await measureTime(() =>
|
|
297
|
+
generateResourceArtifacts(large, { rootDir: testDir, force: false })
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
console.log(`\n 📊 Benchmark Results:`);
|
|
301
|
+
console.log(` Minimal (1 field): ${minimalTime.toFixed(2)}ms`);
|
|
302
|
+
console.log(` Medium (7 fields): ${mediumTime.toFixed(2)}ms`);
|
|
303
|
+
console.log(` Large (30 fields): ${largeTime.toFixed(2)}ms`);
|
|
304
|
+
console.log(
|
|
305
|
+
` Scaling factor: ${(largeTime / minimalTime).toFixed(2)}x (30x fields)`
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Ensure reasonable scaling (should not be exponential)
|
|
309
|
+
expect(largeTime / minimalTime).toBeLessThan(10);
|
|
310
|
+
});
|
|
311
|
+
});
|