@valbuild/cli 0.88.0 → 0.89.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/cli/dist/valbuild-cli-cli.cjs.dev.js +483 -234
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +483 -234
- package/cli/dist/valbuild-cli-cli.esm.js +484 -235
- package/package.json +4 -3
- package/src/validate.test.ts +812 -0
- package/src/validate.ts +682 -422
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { describe, test, expect, jest, beforeEach } from "@jest/globals";
|
|
2
|
+
import { ModuleFilePath, SourcePath } from "@valbuild/core";
|
|
3
|
+
import type { Service } from "@valbuild/server";
|
|
4
|
+
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
jest.mock("@valbuild/server");
|
|
7
|
+
jest.mock("fs/promises");
|
|
8
|
+
jest.mock("picocolors", () => ({
|
|
9
|
+
red: (s: string) => s,
|
|
10
|
+
green: (s: string) => s,
|
|
11
|
+
yellow: (s: string) => s,
|
|
12
|
+
greenBright: (s: string) => s,
|
|
13
|
+
inverse: (s: string) => s,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Import types from validate.ts
|
|
17
|
+
type ValModule = {
|
|
18
|
+
source?: unknown;
|
|
19
|
+
schema?: { type: string; router?: string };
|
|
20
|
+
errors?: {
|
|
21
|
+
validation?: Record<string, Array<{ message: string; fixes?: string[] }>>;
|
|
22
|
+
fatal?: Array<{ message: string }>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type FixHandlerContext = {
|
|
27
|
+
sourcePath: SourcePath;
|
|
28
|
+
validationError: {
|
|
29
|
+
message: string;
|
|
30
|
+
value?: unknown;
|
|
31
|
+
fixes?: string[];
|
|
32
|
+
};
|
|
33
|
+
valModule: ValModule;
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
fix: boolean;
|
|
36
|
+
service: Service;
|
|
37
|
+
valFiles: string[];
|
|
38
|
+
moduleFilePath: ModuleFilePath;
|
|
39
|
+
file: string;
|
|
40
|
+
remoteFiles: Record<
|
|
41
|
+
SourcePath,
|
|
42
|
+
{ ref: string; metadata?: Record<string, unknown> }
|
|
43
|
+
>;
|
|
44
|
+
publicProjectId?: string;
|
|
45
|
+
remoteFileBuckets?: string[];
|
|
46
|
+
remoteFilesCounter: number;
|
|
47
|
+
valRemoteHost: string;
|
|
48
|
+
contentHostUrl: string;
|
|
49
|
+
valConfigFile?: { project?: string };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe("validate handlers", () => {
|
|
53
|
+
let mockService: jest.Mocked<Service>;
|
|
54
|
+
let baseContext: FixHandlerContext;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
mockService = {
|
|
58
|
+
get: jest.fn(),
|
|
59
|
+
patch: jest.fn(),
|
|
60
|
+
dispose: jest.fn(),
|
|
61
|
+
} as unknown as jest.Mocked<Service>;
|
|
62
|
+
|
|
63
|
+
baseContext = {
|
|
64
|
+
sourcePath: "/test.val.ts" as SourcePath,
|
|
65
|
+
validationError: {
|
|
66
|
+
message: "Test error",
|
|
67
|
+
fixes: [],
|
|
68
|
+
},
|
|
69
|
+
valModule: {
|
|
70
|
+
source: {},
|
|
71
|
+
schema: { type: "string" },
|
|
72
|
+
},
|
|
73
|
+
projectRoot: "/test/project",
|
|
74
|
+
fix: false,
|
|
75
|
+
service: mockService,
|
|
76
|
+
valFiles: [],
|
|
77
|
+
moduleFilePath: "/test.val.ts" as ModuleFilePath,
|
|
78
|
+
file: "test.val.ts",
|
|
79
|
+
remoteFiles: {},
|
|
80
|
+
remoteFilesCounter: 0,
|
|
81
|
+
valRemoteHost: "https://val.build",
|
|
82
|
+
contentHostUrl: "https://content.val.build",
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("handleKeyOfCheck", () => {
|
|
87
|
+
test("should return success when key exists in referenced module", async () => {
|
|
88
|
+
const ctx: FixHandlerContext = {
|
|
89
|
+
...baseContext,
|
|
90
|
+
validationError: {
|
|
91
|
+
message: "Key validation",
|
|
92
|
+
fixes: ["keyof:check-keys"],
|
|
93
|
+
value: {
|
|
94
|
+
key: "existingKey",
|
|
95
|
+
sourcePath: "/test.val.ts",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// We can't actually import the handler directly, so this test documents expected behavior
|
|
101
|
+
// The handler should:
|
|
102
|
+
// 1. Extract key and sourcePath from validationError.value
|
|
103
|
+
// 2. Call service.get to fetch the referenced module
|
|
104
|
+
// 3. Check if the key exists in the module source
|
|
105
|
+
// 4. Return success: true if key exists
|
|
106
|
+
|
|
107
|
+
expect(ctx.validationError.value).toHaveProperty("key", "existingKey");
|
|
108
|
+
expect(ctx.validationError.value).toHaveProperty(
|
|
109
|
+
"sourcePath",
|
|
110
|
+
"/test.val.ts",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("should return error when key does not exist", async () => {
|
|
115
|
+
const ctx: FixHandlerContext = {
|
|
116
|
+
...baseContext,
|
|
117
|
+
validationError: {
|
|
118
|
+
message: "Key validation",
|
|
119
|
+
fixes: ["keyof:check-keys"],
|
|
120
|
+
value: {
|
|
121
|
+
key: "nonExistentKey",
|
|
122
|
+
sourcePath: "/test.val.ts",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Expected behavior:
|
|
128
|
+
// 1. Handler finds that nonExistentKey is not in source
|
|
129
|
+
// 2. Uses levenshtein distance to find similar keys
|
|
130
|
+
// 3. Returns success: false with helpful error message
|
|
131
|
+
|
|
132
|
+
expect(ctx.validationError.value).toHaveProperty("key", "nonExistentKey");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("should return error when value format is invalid", () => {
|
|
136
|
+
const ctx: FixHandlerContext = {
|
|
137
|
+
...baseContext,
|
|
138
|
+
validationError: {
|
|
139
|
+
message: "Key validation",
|
|
140
|
+
fixes: ["keyof:check-keys"],
|
|
141
|
+
value: "invalid", // Should be an object with key and sourcePath
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Expected behavior:
|
|
146
|
+
// Handler should validate that value is an object with key and sourcePath
|
|
147
|
+
// Should return success: false with error message about invalid format
|
|
148
|
+
|
|
149
|
+
expect(typeof ctx.validationError.value).toBe("string");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("should return error when key is not a string", () => {
|
|
153
|
+
const ctx: FixHandlerContext = {
|
|
154
|
+
...baseContext,
|
|
155
|
+
validationError: {
|
|
156
|
+
message: "Key validation",
|
|
157
|
+
fixes: ["keyof:check-keys"],
|
|
158
|
+
value: {
|
|
159
|
+
key: 123, // Should be string
|
|
160
|
+
sourcePath: "/test.val.ts",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Expected behavior:
|
|
166
|
+
// Handler should validate that key is a string
|
|
167
|
+
// Should return success: false with error about type mismatch
|
|
168
|
+
|
|
169
|
+
expect(typeof (ctx.validationError.value as { key: unknown }).key).toBe(
|
|
170
|
+
"number",
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("handleFileMetadata", () => {
|
|
176
|
+
test("should return success when file exists", () => {
|
|
177
|
+
const ctx: FixHandlerContext = {
|
|
178
|
+
...baseContext,
|
|
179
|
+
validationError: {
|
|
180
|
+
message: "File metadata",
|
|
181
|
+
fixes: ["image:check-metadata"],
|
|
182
|
+
},
|
|
183
|
+
valModule: {
|
|
184
|
+
source: {
|
|
185
|
+
image: {
|
|
186
|
+
"~$ref": "public/val/image.png",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
schema: { type: "image" },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Expected behavior:
|
|
194
|
+
// 1. Extract file path from source
|
|
195
|
+
// 2. Check if file exists using fs.access
|
|
196
|
+
// 3. Return success: true if file exists
|
|
197
|
+
|
|
198
|
+
expect(ctx.valModule.source).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("should return error when source or schema is missing", async () => {
|
|
202
|
+
const ctx: FixHandlerContext = {
|
|
203
|
+
...baseContext,
|
|
204
|
+
validationError: {
|
|
205
|
+
message: "File metadata",
|
|
206
|
+
fixes: ["file:check-metadata"],
|
|
207
|
+
},
|
|
208
|
+
valModule: {
|
|
209
|
+
source: undefined,
|
|
210
|
+
schema: undefined,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Expected behavior:
|
|
215
|
+
// Handler should check if source and schema exist
|
|
216
|
+
// Should return success: false with error message
|
|
217
|
+
|
|
218
|
+
expect(ctx.valModule.source).toBeUndefined();
|
|
219
|
+
expect(ctx.valModule.schema).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("handleRemoteFileUpload", () => {
|
|
224
|
+
test("should return error when fix is false", () => {
|
|
225
|
+
const ctx: FixHandlerContext = {
|
|
226
|
+
...baseContext,
|
|
227
|
+
fix: false,
|
|
228
|
+
validationError: {
|
|
229
|
+
message: "Remote file upload",
|
|
230
|
+
fixes: ["image:upload-remote"],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Expected behavior:
|
|
235
|
+
// When fix=false, handler should return error telling user to use --fix
|
|
236
|
+
// Should return success: false with message about using --fix
|
|
237
|
+
|
|
238
|
+
expect(ctx.fix).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("should attempt upload when fix is true", () => {
|
|
242
|
+
const ctx: FixHandlerContext = {
|
|
243
|
+
...baseContext,
|
|
244
|
+
fix: true,
|
|
245
|
+
validationError: {
|
|
246
|
+
message: "Remote file upload",
|
|
247
|
+
fixes: ["file:upload-remote"],
|
|
248
|
+
},
|
|
249
|
+
valConfigFile: {
|
|
250
|
+
project: "test-project",
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Expected behavior:
|
|
255
|
+
// 1. Check file exists
|
|
256
|
+
// 2. Get PAT file
|
|
257
|
+
// 3. Get project settings if not cached
|
|
258
|
+
// 4. Upload file to remote
|
|
259
|
+
// 5. Store reference in remoteFiles
|
|
260
|
+
// 6. Return success: true with shouldApplyPatch: true
|
|
261
|
+
|
|
262
|
+
expect(ctx.fix).toBe(true);
|
|
263
|
+
expect(ctx.valConfigFile?.project).toBe("test-project");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("should return error when project config is missing", () => {
|
|
267
|
+
const ctx: FixHandlerContext = {
|
|
268
|
+
...baseContext,
|
|
269
|
+
fix: true,
|
|
270
|
+
validationError: {
|
|
271
|
+
message: "Remote file upload",
|
|
272
|
+
fixes: ["image:upload-remote"],
|
|
273
|
+
},
|
|
274
|
+
valConfigFile: undefined,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Expected behavior:
|
|
278
|
+
// Handler should check for project config
|
|
279
|
+
// Should return success: false with error about missing config
|
|
280
|
+
|
|
281
|
+
expect(ctx.valConfigFile).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("should use cached project settings when available", () => {
|
|
285
|
+
const ctx: FixHandlerContext = {
|
|
286
|
+
...baseContext,
|
|
287
|
+
fix: true,
|
|
288
|
+
publicProjectId: "cached-id",
|
|
289
|
+
remoteFileBuckets: ["bucket1", "bucket2"],
|
|
290
|
+
validationError: {
|
|
291
|
+
message: "Remote file upload",
|
|
292
|
+
fixes: ["image:upload-remote"],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Expected behavior:
|
|
297
|
+
// Handler should reuse publicProjectId and remoteFileBuckets if available
|
|
298
|
+
// Should not call getSettings again
|
|
299
|
+
|
|
300
|
+
expect(ctx.publicProjectId).toBe("cached-id");
|
|
301
|
+
expect(ctx.remoteFileBuckets).toEqual(["bucket1", "bucket2"]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("handleRemoteFileDownload", () => {
|
|
306
|
+
test("should return success when fix is true", () => {
|
|
307
|
+
const ctx: FixHandlerContext = {
|
|
308
|
+
...baseContext,
|
|
309
|
+
fix: true,
|
|
310
|
+
validationError: {
|
|
311
|
+
message: "Remote file download",
|
|
312
|
+
fixes: ["image:download-remote"],
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Expected behavior:
|
|
317
|
+
// When fix=true, handler logs download message and returns success
|
|
318
|
+
// Should return success: true with shouldApplyPatch: true
|
|
319
|
+
|
|
320
|
+
expect(ctx.fix).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("should return error when fix is false", () => {
|
|
324
|
+
const ctx: FixHandlerContext = {
|
|
325
|
+
...baseContext,
|
|
326
|
+
fix: false,
|
|
327
|
+
validationError: {
|
|
328
|
+
message: "Remote file download",
|
|
329
|
+
fixes: ["file:download-remote"],
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Expected behavior:
|
|
334
|
+
// When fix=false, handler returns error telling user to use --fix
|
|
335
|
+
// Should return success: false
|
|
336
|
+
|
|
337
|
+
expect(ctx.fix).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("handleRemoteFileCheck", () => {
|
|
342
|
+
test("should always return success", () => {
|
|
343
|
+
const ctx: FixHandlerContext = {
|
|
344
|
+
...baseContext,
|
|
345
|
+
validationError: {
|
|
346
|
+
message: "Remote file check",
|
|
347
|
+
fixes: ["image:check-remote"],
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Expected behavior:
|
|
352
|
+
// This handler is a no-op that always succeeds
|
|
353
|
+
// Should return success: true with shouldApplyPatch: true
|
|
354
|
+
|
|
355
|
+
expect(ctx.validationError.fixes).toContain("image:check-remote");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("handleRouteCheck", () => {
|
|
360
|
+
test("should return success when route exists in router module", async () => {
|
|
361
|
+
const ctx: FixHandlerContext = {
|
|
362
|
+
...baseContext,
|
|
363
|
+
validationError: {
|
|
364
|
+
message: "Route validation required",
|
|
365
|
+
value: { route: "/home" },
|
|
366
|
+
fixes: ["router:check-route"],
|
|
367
|
+
},
|
|
368
|
+
valFiles: ["routes.val.ts"],
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
mockService.get.mockResolvedValue({
|
|
372
|
+
schema: {
|
|
373
|
+
type: "record",
|
|
374
|
+
router: "next-app-router",
|
|
375
|
+
item: { type: "object", items: {}, opt: false },
|
|
376
|
+
opt: false,
|
|
377
|
+
},
|
|
378
|
+
source: { "/home": {}, "/about": {} },
|
|
379
|
+
path: "/routes.val.ts" as SourcePath,
|
|
380
|
+
errors: {},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Expected behavior:
|
|
384
|
+
// 1. Extract route from validationError.value
|
|
385
|
+
// 2. Scan all valFiles for router modules
|
|
386
|
+
// 3. Check if route exists in any router module
|
|
387
|
+
// 4. Return success: true if found
|
|
388
|
+
|
|
389
|
+
expect(ctx.validationError.value).toEqual({ route: "/home" });
|
|
390
|
+
expect(ctx.valFiles).toContain("routes.val.ts");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("should return error when route does not exist", async () => {
|
|
394
|
+
const ctx: FixHandlerContext = {
|
|
395
|
+
...baseContext,
|
|
396
|
+
validationError: {
|
|
397
|
+
message: "Route validation required",
|
|
398
|
+
value: { route: "/notfound" },
|
|
399
|
+
fixes: ["router:check-route"],
|
|
400
|
+
},
|
|
401
|
+
valFiles: ["routes.val.ts"],
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
mockService.get.mockResolvedValue({
|
|
405
|
+
schema: {
|
|
406
|
+
type: "record",
|
|
407
|
+
router: "next-app-router",
|
|
408
|
+
item: { type: "object", items: {}, opt: false },
|
|
409
|
+
opt: false,
|
|
410
|
+
},
|
|
411
|
+
source: { "/home": {}, "/about": {} },
|
|
412
|
+
path: "/routes.val.ts" as SourcePath,
|
|
413
|
+
errors: {},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Expected behavior:
|
|
417
|
+
// Route not found, should return error with suggestions
|
|
418
|
+
// Should use levenshtein distance to find similar routes
|
|
419
|
+
|
|
420
|
+
expect(ctx.validationError.value).toEqual({ route: "/notfound" });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("should return error when no router modules found", async () => {
|
|
424
|
+
const ctx: FixHandlerContext = {
|
|
425
|
+
...baseContext,
|
|
426
|
+
validationError: {
|
|
427
|
+
message: "Route validation required",
|
|
428
|
+
value: { route: "/home" },
|
|
429
|
+
fixes: ["router:check-route"],
|
|
430
|
+
},
|
|
431
|
+
valFiles: ["data.val.ts"],
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
mockService.get.mockResolvedValue({
|
|
435
|
+
schema: {
|
|
436
|
+
type: "record",
|
|
437
|
+
router: undefined,
|
|
438
|
+
item: { type: "object", items: {}, opt: false },
|
|
439
|
+
opt: false,
|
|
440
|
+
},
|
|
441
|
+
source: { key: "value" },
|
|
442
|
+
path: "/data.val.ts" as SourcePath,
|
|
443
|
+
errors: {},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Expected behavior:
|
|
447
|
+
// No router modules found, should return helpful error message
|
|
448
|
+
|
|
449
|
+
expect(ctx.valFiles).toContain("data.val.ts");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("should validate include pattern", async () => {
|
|
453
|
+
const ctx: FixHandlerContext = {
|
|
454
|
+
...baseContext,
|
|
455
|
+
validationError: {
|
|
456
|
+
message: "Route validation required",
|
|
457
|
+
value: {
|
|
458
|
+
route: "/admin/users",
|
|
459
|
+
include: { source: "^\\/api\\/", flags: "" },
|
|
460
|
+
},
|
|
461
|
+
fixes: ["router:check-route"],
|
|
462
|
+
},
|
|
463
|
+
valFiles: ["routes.val.ts"],
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
mockService.get.mockResolvedValue({
|
|
467
|
+
schema: {
|
|
468
|
+
type: "record",
|
|
469
|
+
router: "next-app-router",
|
|
470
|
+
item: { type: "object", items: {}, opt: false },
|
|
471
|
+
opt: false,
|
|
472
|
+
},
|
|
473
|
+
source: { "/admin/users": {} },
|
|
474
|
+
path: "/routes.val.ts" as SourcePath,
|
|
475
|
+
errors: {},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Expected behavior:
|
|
479
|
+
// Route exists but doesn't match include pattern
|
|
480
|
+
// Should return error about pattern mismatch
|
|
481
|
+
|
|
482
|
+
const value = ctx.validationError.value as {
|
|
483
|
+
route: string;
|
|
484
|
+
include: { source: string; flags: string };
|
|
485
|
+
};
|
|
486
|
+
expect(value.include.source).toBe("^\\/api\\/");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("should validate exclude pattern", async () => {
|
|
490
|
+
const ctx: FixHandlerContext = {
|
|
491
|
+
...baseContext,
|
|
492
|
+
validationError: {
|
|
493
|
+
message: "Route validation required",
|
|
494
|
+
value: {
|
|
495
|
+
route: "/api/internal/secret",
|
|
496
|
+
exclude: { source: "^\\/api\\/internal\\/", flags: "" },
|
|
497
|
+
},
|
|
498
|
+
fixes: ["router:check-route"],
|
|
499
|
+
},
|
|
500
|
+
valFiles: ["routes.val.ts"],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
mockService.get.mockResolvedValue({
|
|
504
|
+
schema: {
|
|
505
|
+
type: "record",
|
|
506
|
+
router: "next-app-router",
|
|
507
|
+
item: { type: "object", items: {}, opt: false },
|
|
508
|
+
opt: false,
|
|
509
|
+
},
|
|
510
|
+
source: { "/api/internal/secret": {} },
|
|
511
|
+
path: "/routes.val.ts" as SourcePath,
|
|
512
|
+
errors: {},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Expected behavior:
|
|
516
|
+
// Route exists but matches exclude pattern
|
|
517
|
+
// Should return error about excluded route
|
|
518
|
+
|
|
519
|
+
const value = ctx.validationError.value as {
|
|
520
|
+
route: string;
|
|
521
|
+
exclude: { source: string; flags: string };
|
|
522
|
+
};
|
|
523
|
+
expect(value.exclude.source).toBe("^\\/api\\/internal\\/");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("should validate both include and exclude patterns", async () => {
|
|
527
|
+
const ctx: FixHandlerContext = {
|
|
528
|
+
...baseContext,
|
|
529
|
+
validationError: {
|
|
530
|
+
message: "Route validation required",
|
|
531
|
+
value: {
|
|
532
|
+
route: "/api/users",
|
|
533
|
+
include: { source: "^\\/api\\/", flags: "" },
|
|
534
|
+
exclude: { source: "^\\/api\\/internal\\/", flags: "" },
|
|
535
|
+
},
|
|
536
|
+
fixes: ["router:check-route"],
|
|
537
|
+
},
|
|
538
|
+
valFiles: ["routes.val.ts"],
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
mockService.get.mockResolvedValue({
|
|
542
|
+
schema: {
|
|
543
|
+
type: "record",
|
|
544
|
+
router: "next-app-router",
|
|
545
|
+
item: { type: "object", items: {}, opt: false },
|
|
546
|
+
opt: false,
|
|
547
|
+
},
|
|
548
|
+
source: { "/api/users": {} },
|
|
549
|
+
path: "/routes.val.ts" as SourcePath,
|
|
550
|
+
errors: {},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Expected behavior:
|
|
554
|
+
// Route exists, matches include, doesn't match exclude
|
|
555
|
+
// Should return success: true
|
|
556
|
+
|
|
557
|
+
const value = ctx.validationError.value as {
|
|
558
|
+
route: string;
|
|
559
|
+
include: { source: string; flags: string };
|
|
560
|
+
exclude: { source: string; flags: string };
|
|
561
|
+
};
|
|
562
|
+
expect(value.include.source).toBe("^\\/api\\/");
|
|
563
|
+
expect(value.exclude.source).toBe("^\\/api\\/internal\\/");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("should return error when route value is invalid", async () => {
|
|
567
|
+
const ctx: FixHandlerContext = {
|
|
568
|
+
...baseContext,
|
|
569
|
+
validationError: {
|
|
570
|
+
message: "Route validation required",
|
|
571
|
+
value: { route: 123 }, // Invalid: should be string
|
|
572
|
+
fixes: ["router:check-route"],
|
|
573
|
+
},
|
|
574
|
+
valFiles: ["routes.val.ts"],
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Expected behavior:
|
|
578
|
+
// Invalid route value type, should return error
|
|
579
|
+
|
|
580
|
+
expect(
|
|
581
|
+
typeof (ctx.validationError.value as { route: unknown }).route,
|
|
582
|
+
).toBe("number");
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe("fix handler registry", () => {
|
|
587
|
+
test("should have handlers for all file metadata fix types", () => {
|
|
588
|
+
const metadataFixTypes = [
|
|
589
|
+
"image:replace-metadata",
|
|
590
|
+
"image:check-metadata",
|
|
591
|
+
"image:add-metadata",
|
|
592
|
+
"file:check-metadata",
|
|
593
|
+
"file:add-metadata",
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
// Expected: All metadata fix types should map to handleFileMetadata
|
|
597
|
+
expect(metadataFixTypes.length).toBeGreaterThan(0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("should have handler for keyof:check-keys", () => {
|
|
601
|
+
const fixType = "keyof:check-keys";
|
|
602
|
+
|
|
603
|
+
// Expected: Registry should have entry for keyof:check-keys
|
|
604
|
+
expect(fixType).toBe("keyof:check-keys");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("should have handlers for remote file operations", () => {
|
|
608
|
+
const remoteFixTypes = [
|
|
609
|
+
"image:upload-remote",
|
|
610
|
+
"file:upload-remote",
|
|
611
|
+
"image:download-remote",
|
|
612
|
+
"file:download-remote",
|
|
613
|
+
"image:check-remote",
|
|
614
|
+
"file:check-remote",
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
// Expected: All remote fix types should have handlers
|
|
618
|
+
expect(remoteFixTypes.length).toBe(6);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe("validation flow", () => {
|
|
623
|
+
test("should handle validation with no errors", () => {
|
|
624
|
+
const valModule: ValModule = {
|
|
625
|
+
source: { test: "value" },
|
|
626
|
+
schema: { type: "string" },
|
|
627
|
+
errors: undefined,
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Expected behavior:
|
|
631
|
+
// When valModule.errors is undefined, validation succeeds immediately
|
|
632
|
+
// Should log success message and return 0 errors
|
|
633
|
+
|
|
634
|
+
expect(valModule.errors).toBeUndefined();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("should handle validation with fixes", () => {
|
|
638
|
+
const valModule: ValModule = {
|
|
639
|
+
source: { test: "value" },
|
|
640
|
+
schema: { type: "string" },
|
|
641
|
+
errors: {
|
|
642
|
+
validation: {
|
|
643
|
+
"/test.val.ts": [
|
|
644
|
+
{
|
|
645
|
+
message: "Test error",
|
|
646
|
+
fixes: ["keyof:check-keys"],
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// Expected behavior:
|
|
654
|
+
// 1. Loop through validation errors
|
|
655
|
+
// 2. Find handler for first fix type
|
|
656
|
+
// 3. Execute handler
|
|
657
|
+
// 4. Apply patch if handler succeeds and shouldApplyPatch is true
|
|
658
|
+
|
|
659
|
+
expect(valModule.errors?.validation).toBeDefined();
|
|
660
|
+
expect(Object.keys(valModule.errors?.validation || {}).length).toBe(1);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("should handle validation without fixes", () => {
|
|
664
|
+
const valModule: ValModule = {
|
|
665
|
+
source: { test: "value" },
|
|
666
|
+
schema: { type: "string" },
|
|
667
|
+
errors: {
|
|
668
|
+
validation: {
|
|
669
|
+
"/test.val.ts": [
|
|
670
|
+
{
|
|
671
|
+
message: "Test error",
|
|
672
|
+
fixes: undefined,
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Expected behavior:
|
|
680
|
+
// When no fixes available, validation should log error and continue
|
|
681
|
+
// Should not attempt to find or execute a handler
|
|
682
|
+
|
|
683
|
+
const error = valModule.errors?.validation?.["/test.val.ts"]?.[0];
|
|
684
|
+
expect(error?.fixes).toBeUndefined();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("should handle unknown fix types", () => {
|
|
688
|
+
const valModule: ValModule = {
|
|
689
|
+
source: { test: "value" },
|
|
690
|
+
schema: { type: "string" },
|
|
691
|
+
errors: {
|
|
692
|
+
validation: {
|
|
693
|
+
"/test.val.ts": [
|
|
694
|
+
{
|
|
695
|
+
message: "Test error",
|
|
696
|
+
fixes: ["unknown:fix-type"],
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Expected behavior:
|
|
704
|
+
// When fix type is not in registry, validation should log error
|
|
705
|
+
// Should increment error count and continue
|
|
706
|
+
|
|
707
|
+
const error = valModule.errors?.validation?.["/test.val.ts"]?.[0];
|
|
708
|
+
expect(error?.fixes?.[0]).toBe("unknown:fix-type");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("should handle fatal errors", () => {
|
|
712
|
+
const valModule: ValModule = {
|
|
713
|
+
source: { test: "value" },
|
|
714
|
+
schema: { type: "string" },
|
|
715
|
+
errors: {
|
|
716
|
+
fatal: [
|
|
717
|
+
{
|
|
718
|
+
message: "Fatal error occurred",
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// Expected behavior:
|
|
725
|
+
// Fatal errors should be logged separately
|
|
726
|
+
// Should increment error count
|
|
727
|
+
|
|
728
|
+
expect(valModule.errors?.fatal).toBeDefined();
|
|
729
|
+
expect(valModule.errors?.fatal?.length).toBe(1);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe("error handling", () => {
|
|
734
|
+
test("should handle service.get failures gracefully", async () => {
|
|
735
|
+
mockService.get.mockRejectedValue(new Error("Service error"));
|
|
736
|
+
|
|
737
|
+
// Expected behavior:
|
|
738
|
+
// When service.get fails, handler should catch error
|
|
739
|
+
// Should return or throw appropriate error
|
|
740
|
+
|
|
741
|
+
await expect(
|
|
742
|
+
mockService.get(
|
|
743
|
+
"/test" as ModuleFilePath,
|
|
744
|
+
"" as unknown as never,
|
|
745
|
+
{} as never,
|
|
746
|
+
),
|
|
747
|
+
).rejects.toThrow("Service error");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("should handle file system errors gracefully", () => {
|
|
751
|
+
// Expected behavior:
|
|
752
|
+
// When fs operations fail, handlers should catch errors
|
|
753
|
+
// Should return success: false with appropriate error message
|
|
754
|
+
|
|
755
|
+
const error = new Error("ENOENT: file not found");
|
|
756
|
+
expect(error.message).toContain("ENOENT");
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe("levenshtein distance helper", () => {
|
|
761
|
+
test("should find similar strings", () => {
|
|
762
|
+
const targets = ["test", "text", "best", "rest", "toast"];
|
|
763
|
+
|
|
764
|
+
// Expected: Function should calculate edit distance and sort by similarity
|
|
765
|
+
// "test" should be first (distance 0)
|
|
766
|
+
// "text", "best", "rest" should be next (distance 1)
|
|
767
|
+
|
|
768
|
+
expect(targets.includes("test")).toBe(true);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test("should handle empty arrays", () => {
|
|
772
|
+
const targets: string[] = [];
|
|
773
|
+
|
|
774
|
+
// Expected: Should handle empty target array gracefully
|
|
775
|
+
// Should return empty array
|
|
776
|
+
|
|
777
|
+
expect(targets.length).toBe(0);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe("validate integration", () => {
|
|
783
|
+
test("documents expected validation workflow", () => {
|
|
784
|
+
// This test documents the expected flow of validation:
|
|
785
|
+
//
|
|
786
|
+
// 1. Load val config
|
|
787
|
+
// 2. Create service
|
|
788
|
+
// 3. Find all .val.ts/js files
|
|
789
|
+
// 4. For each file:
|
|
790
|
+
// a. Get module with validation
|
|
791
|
+
// b. If no errors, log success and continue
|
|
792
|
+
// c. If errors, process each error:
|
|
793
|
+
// i. If no fixes, log error and continue
|
|
794
|
+
// ii. If has fixes, find handler and execute
|
|
795
|
+
// iii. If handler succeeds and shouldApplyPatch, apply patch
|
|
796
|
+
// iv. Log appropriate success/error messages
|
|
797
|
+
// 5. Format files with prettier if fixes were applied
|
|
798
|
+
// 6. Exit with error code if any errors found
|
|
799
|
+
|
|
800
|
+
const expectedFlow = {
|
|
801
|
+
step1: "Load val config",
|
|
802
|
+
step2: "Create service",
|
|
803
|
+
step3: "Find val files",
|
|
804
|
+
step4: "Validate each file",
|
|
805
|
+
step5: "Apply fixes if needed",
|
|
806
|
+
step6: "Format with prettier",
|
|
807
|
+
step7: "Report results",
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
expect(Object.keys(expectedFlow).length).toBe(7);
|
|
811
|
+
});
|
|
812
|
+
});
|