@mandujs/core 0.16.0 → 0.18.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.
@@ -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
+ });