@codexa/cli 9.0.7 → 9.0.9

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,447 @@
1
+ import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test";
2
+ import { getDb } from "../db/connection";
3
+ import { initSchema, runMigrations } from "../db/schema";
4
+ import { writeFileSync, mkdirSync, rmSync, existsSync } from "fs";
5
+ import { join } from "path";
6
+
7
+ // Mock grepai functions before importing validator
8
+ import * as patterns from "../commands/patterns";
9
+
10
+ import { validateAgainstStandards, printValidationResult } from "./standards-validator";
11
+
12
+ // ═══════════════════════════════════════════════════════════════
13
+ // HELPERS
14
+ // ═══════════════════════════════════════════════════════════════
15
+
16
+ const TMP_DIR = join(process.cwd(), ".codexa", "test-tmp-standards");
17
+
18
+ function ensureTmpDir() {
19
+ if (!existsSync(TMP_DIR)) {
20
+ mkdirSync(TMP_DIR, { recursive: true });
21
+ }
22
+ }
23
+
24
+ function createTmpFile(name: string, content: string): string {
25
+ ensureTmpDir();
26
+ const path = join(TMP_DIR, name);
27
+ writeFileSync(path, content);
28
+ return path;
29
+ }
30
+
31
+ function insertStandard(opts: {
32
+ category: string;
33
+ scope: string;
34
+ rule: string;
35
+ enforcement?: string;
36
+ anti_examples?: string;
37
+ semantic_query?: string | null;
38
+ expect?: string;
39
+ }) {
40
+ const db = getDb();
41
+ db.run(
42
+ `INSERT INTO standards (category, scope, rule, anti_examples, enforcement, semantic_query, expect, source, created_at)
43
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'test', datetime('now'))`,
44
+ [
45
+ opts.category,
46
+ opts.scope,
47
+ opts.rule,
48
+ opts.anti_examples || null,
49
+ opts.enforcement || "required",
50
+ opts.semantic_query || null,
51
+ opts.expect || "no_match",
52
+ ]
53
+ );
54
+ }
55
+
56
+ // ═══════════════════════════════════════════════════════════════
57
+ // TESTS
58
+ // ═══════════════════════════════════════════════════════════════
59
+
60
+ describe("standards-validator", () => {
61
+ beforeEach(() => {
62
+ initSchema();
63
+ runMigrations();
64
+ const db = getDb();
65
+ db.run("DELETE FROM standards");
66
+
67
+ // Clean tmp files
68
+ if (existsSync(TMP_DIR)) {
69
+ rmSync(TMP_DIR, { recursive: true });
70
+ }
71
+ });
72
+
73
+ // ─────────────────────────────────────────────────────────
74
+ // BASE CASES
75
+ // ─────────────────────────────────────────────────────────
76
+
77
+ it("returns passed when no standards exist", () => {
78
+ const file = createTmpFile("test.ts", "export const x = 1;");
79
+ const result = validateAgainstStandards([file], "all");
80
+ expect(result.passed).toBe(true);
81
+ expect(result.violations).toHaveLength(0);
82
+ expect(result.warnings).toHaveLength(0);
83
+ });
84
+
85
+ it("returns passed when files list is empty", () => {
86
+ insertStandard({ category: "code", scope: "all", rule: "No console.log" });
87
+ const result = validateAgainstStandards([], "all");
88
+ expect(result.passed).toBe(true);
89
+ });
90
+
91
+ // ─────────────────────────────────────────────────────────
92
+ // TEXTUAL VALIDATION (anti_examples)
93
+ // ─────────────────────────────────────────────────────────
94
+
95
+ it("detects anti_examples violation in file content", () => {
96
+ const file = createTmpFile("api.ts", 'console.log("debug"); export function handle() {}');
97
+ insertStandard({
98
+ category: "code",
99
+ scope: "all",
100
+ rule: "Nao usar console.log",
101
+ anti_examples: JSON.stringify(["console.log"]),
102
+ });
103
+
104
+ const result = validateAgainstStandards([file], "all");
105
+ expect(result.passed).toBe(false);
106
+ expect(result.violations).toHaveLength(1);
107
+ expect(result.violations[0].detail).toContain("console.log");
108
+ });
109
+
110
+ it("passes when anti_examples not found in file", () => {
111
+ const file = createTmpFile("api.ts", "export function handle() { return 'ok'; }");
112
+ insertStandard({
113
+ category: "code",
114
+ scope: "all",
115
+ rule: "Nao usar console.log",
116
+ anti_examples: JSON.stringify(["console.log"]),
117
+ });
118
+
119
+ const result = validateAgainstStandards([file], "all");
120
+ expect(result.passed).toBe(true);
121
+ });
122
+
123
+ it("treats recommended enforcement as warning not violation", () => {
124
+ const file = createTmpFile("api.ts", 'console.log("test");');
125
+ insertStandard({
126
+ category: "code",
127
+ scope: "all",
128
+ rule: "Evitar console.log",
129
+ anti_examples: JSON.stringify(["console.log"]),
130
+ enforcement: "recommended",
131
+ });
132
+
133
+ const result = validateAgainstStandards([file], "all");
134
+ expect(result.passed).toBe(true); // recommended does NOT block
135
+ expect(result.warnings).toHaveLength(1);
136
+ expect(result.violations).toHaveLength(0);
137
+ });
138
+
139
+ it("filters standards by scope", () => {
140
+ const file = createTmpFile("api.ts", 'console.log("test");');
141
+
142
+ // Standard for frontend scope only
143
+ insertStandard({
144
+ category: "code",
145
+ scope: "frontend",
146
+ rule: "Frontend: no console.log",
147
+ anti_examples: JSON.stringify(["console.log"]),
148
+ });
149
+
150
+ // Validate with backend domain — should NOT match frontend standard
151
+ const result = validateAgainstStandards([file], "backend");
152
+ expect(result.passed).toBe(true);
153
+ });
154
+
155
+ it("matches scope 'all' for any domain", () => {
156
+ const file = createTmpFile("api.ts", 'console.log("test");');
157
+ insertStandard({
158
+ category: "code",
159
+ scope: "all",
160
+ rule: "No console.log",
161
+ anti_examples: JSON.stringify(["console.log"]),
162
+ });
163
+
164
+ const result = validateAgainstStandards([file], "backend");
165
+ expect(result.passed).toBe(false);
166
+ });
167
+
168
+ it("skips nonexistent files gracefully", () => {
169
+ insertStandard({
170
+ category: "code",
171
+ scope: "all",
172
+ rule: "No eval",
173
+ anti_examples: JSON.stringify(["eval("]),
174
+ });
175
+
176
+ const result = validateAgainstStandards(["/nonexistent/file.ts"], "all");
177
+ expect(result.passed).toBe(true);
178
+ });
179
+
180
+ it("handles invalid anti_examples JSON gracefully", () => {
181
+ const file = createTmpFile("api.ts", "export const x = 1;");
182
+ const db = getDb();
183
+ db.run(
184
+ `INSERT INTO standards (category, scope, rule, anti_examples, enforcement, source, created_at)
185
+ VALUES ('code', 'all', 'Bad JSON', 'not-valid-json', 'required', 'test', datetime('now'))`
186
+ );
187
+
188
+ const result = validateAgainstStandards([file], "all");
189
+ expect(result.passed).toBe(true); // Invalid JSON should not crash
190
+ });
191
+
192
+ // ─────────────────────────────────────────────────────────
193
+ // SEMANTIC VALIDATION (grepai)
194
+ // ─────────────────────────────────────────────────────────
195
+
196
+ it("detects no_match violation when grepai finds matching file", () => {
197
+ const file = createTmpFile("handler.ts", "export function handle() {}");
198
+ const resolvedPath = join(TMP_DIR, "handler.ts").replace(/\\/g, "/");
199
+
200
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
201
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
202
+ { path: resolvedPath, score: 0.85 },
203
+ ]);
204
+
205
+ insertStandard({
206
+ category: "code",
207
+ scope: "all",
208
+ rule: "No .then() chains",
209
+ semantic_query: "promise .then() chain instead of async await",
210
+ expect: "no_match",
211
+ });
212
+
213
+ const result = validateAgainstStandards([file], "all");
214
+ expect(result.passed).toBe(false);
215
+ expect(result.violations).toHaveLength(1);
216
+ expect(result.violations[0].detail).toContain("Violacao semantica");
217
+ expect(result.violations[0].detail).toContain("0.85");
218
+
219
+ availSpy.mockRestore();
220
+ searchSpy.mockRestore();
221
+ });
222
+
223
+ it("passes no_match when grepai finds no matches", () => {
224
+ const file = createTmpFile("clean.ts", "export const x = 1;");
225
+
226
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
227
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([]);
228
+
229
+ insertStandard({
230
+ category: "code",
231
+ scope: "all",
232
+ rule: "No .then() chains",
233
+ semantic_query: "promise .then() chain",
234
+ expect: "no_match",
235
+ });
236
+
237
+ const result = validateAgainstStandards([file], "all");
238
+ expect(result.passed).toBe(true);
239
+
240
+ availSpy.mockRestore();
241
+ searchSpy.mockRestore();
242
+ });
243
+
244
+ it("detects must_match violation when grepai finds no match", () => {
245
+ const file = createTmpFile("service.ts", "export function process() {}");
246
+
247
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
248
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([]);
249
+
250
+ insertStandard({
251
+ category: "practice",
252
+ scope: "all",
253
+ rule: "Must have error handling",
254
+ semantic_query: "try catch error handling",
255
+ expect: "must_match",
256
+ });
257
+
258
+ const result = validateAgainstStandards([file], "all");
259
+ expect(result.passed).toBe(false);
260
+ expect(result.violations).toHaveLength(1);
261
+ expect(result.violations[0].detail).toContain("Padrao obrigatorio nao encontrado");
262
+
263
+ availSpy.mockRestore();
264
+ searchSpy.mockRestore();
265
+ });
266
+
267
+ it("passes must_match when grepai finds matching file", () => {
268
+ const file = createTmpFile("service.ts", "try { } catch (e) { }");
269
+ const resolvedPath = join(TMP_DIR, "service.ts").replace(/\\/g, "/");
270
+
271
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
272
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
273
+ { path: resolvedPath, score: 0.9 },
274
+ ]);
275
+
276
+ insertStandard({
277
+ category: "practice",
278
+ scope: "all",
279
+ rule: "Must have error handling",
280
+ semantic_query: "try catch error handling",
281
+ expect: "must_match",
282
+ });
283
+
284
+ const result = validateAgainstStandards([file], "all");
285
+ expect(result.passed).toBe(true);
286
+
287
+ availSpy.mockRestore();
288
+ searchSpy.mockRestore();
289
+ });
290
+
291
+ it("ignores grepai results below score threshold", () => {
292
+ const file = createTmpFile("maybe.ts", "export const x = 1;");
293
+ const resolvedPath = join(TMP_DIR, "maybe.ts").replace(/\\/g, "/");
294
+
295
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
296
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
297
+ { path: resolvedPath, score: 0.5 }, // Below 0.7 threshold
298
+ ]);
299
+
300
+ insertStandard({
301
+ category: "code",
302
+ scope: "all",
303
+ rule: "No console.log",
304
+ semantic_query: "console.log statement",
305
+ expect: "no_match",
306
+ });
307
+
308
+ const result = validateAgainstStandards([file], "all");
309
+ expect(result.passed).toBe(true); // Score too low = no match
310
+
311
+ availSpy.mockRestore();
312
+ searchSpy.mockRestore();
313
+ });
314
+
315
+ it("ignores grepai results for files not in validation list", () => {
316
+ const file = createTmpFile("target.ts", "export const x = 1;");
317
+
318
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
319
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
320
+ { path: "/some/other/file.ts", score: 0.95 }, // High score but wrong file
321
+ ]);
322
+
323
+ insertStandard({
324
+ category: "code",
325
+ scope: "all",
326
+ rule: "No eval",
327
+ semantic_query: "eval() usage",
328
+ expect: "no_match",
329
+ });
330
+
331
+ const result = validateAgainstStandards([file], "all");
332
+ expect(result.passed).toBe(true); // Match is for a different file
333
+
334
+ availSpy.mockRestore();
335
+ searchSpy.mockRestore();
336
+ });
337
+
338
+ // ─────────────────────────────────────────────────────────
339
+ // FALLBACK (grepai unavailable)
340
+ // ─────────────────────────────────────────────────────────
341
+
342
+ it("falls back to anti_examples when grepai not available", () => {
343
+ const file = createTmpFile("api.ts", 'eval("alert(1)");');
344
+
345
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(false);
346
+
347
+ insertStandard({
348
+ category: "code",
349
+ scope: "all",
350
+ rule: "No eval",
351
+ anti_examples: JSON.stringify(["eval("]),
352
+ semantic_query: "eval usage for dynamic code execution",
353
+ expect: "no_match",
354
+ });
355
+
356
+ const result = validateAgainstStandards([file], "all");
357
+ expect(result.passed).toBe(false); // Falls back to anti_examples check
358
+ expect(result.violations).toHaveLength(1);
359
+
360
+ availSpy.mockRestore();
361
+ });
362
+
363
+ it("skips must_match standards when grepai not available", () => {
364
+ const file = createTmpFile("service.ts", "export function process() {}");
365
+
366
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(false);
367
+
368
+ insertStandard({
369
+ category: "practice",
370
+ scope: "all",
371
+ rule: "Must have error handling",
372
+ semantic_query: "try catch error handling",
373
+ expect: "must_match",
374
+ });
375
+
376
+ const result = validateAgainstStandards([file], "all");
377
+ expect(result.passed).toBe(true); // Cannot validate must_match without grepai
378
+
379
+ availSpy.mockRestore();
380
+ });
381
+
382
+ // ─────────────────────────────────────────────────────────
383
+ // CAP & MIXED
384
+ // ─────────────────────────────────────────────────────────
385
+
386
+ it("caps semantic standards at MAX_SEMANTIC_QUERIES (10)", () => {
387
+ const file = createTmpFile("test.ts", "export const x = 1;");
388
+
389
+ let callCount = 0;
390
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
391
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockImplementation(() => {
392
+ callCount++;
393
+ return [];
394
+ });
395
+
396
+ // Insert 15 semantic standards
397
+ for (let i = 0; i < 15; i++) {
398
+ insertStandard({
399
+ category: "code",
400
+ scope: "all",
401
+ rule: `Rule ${i}`,
402
+ semantic_query: `query ${i}`,
403
+ expect: "no_match",
404
+ });
405
+ }
406
+
407
+ validateAgainstStandards([file], "all");
408
+ expect(callCount).toBe(10); // Capped at 10
409
+
410
+ availSpy.mockRestore();
411
+ searchSpy.mockRestore();
412
+ });
413
+
414
+ it("validates both semantic and textual standards in same run", () => {
415
+ const file = createTmpFile("mixed.ts", 'eval("code"); export const x = 1;');
416
+ const resolvedPath = join(TMP_DIR, "mixed.ts").replace(/\\/g, "/");
417
+
418
+ const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
419
+ const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
420
+ { path: resolvedPath, score: 0.9 },
421
+ ]);
422
+
423
+ // Semantic standard
424
+ insertStandard({
425
+ category: "code",
426
+ scope: "all",
427
+ rule: "No .then() chains",
428
+ semantic_query: "promise .then() chain",
429
+ expect: "no_match",
430
+ });
431
+
432
+ // Textual standard (no semantic_query)
433
+ insertStandard({
434
+ category: "code",
435
+ scope: "all",
436
+ rule: "No eval",
437
+ anti_examples: JSON.stringify(["eval("]),
438
+ });
439
+
440
+ const result = validateAgainstStandards([file], "all");
441
+ expect(result.passed).toBe(false);
442
+ expect(result.violations).toHaveLength(2); // One semantic + one textual
443
+
444
+ availSpy.mockRestore();
445
+ searchSpy.mockRestore();
446
+ });
447
+ });