@rigour-labs/core 3.0.2 → 3.0.4
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/dist/gates/deprecated-apis.d.ts +55 -0
- package/dist/gates/deprecated-apis.js +724 -0
- package/dist/gates/deprecated-apis.test.d.ts +1 -0
- package/dist/gates/deprecated-apis.test.js +288 -0
- package/dist/gates/hallucinated-imports.d.ts +79 -13
- package/dist/gates/hallucinated-imports.js +434 -50
- package/dist/gates/hallucinated-imports.test.js +707 -31
- package/dist/gates/phantom-apis.d.ts +77 -0
- package/dist/gates/phantom-apis.js +675 -0
- package/dist/gates/phantom-apis.test.d.ts +1 -0
- package/dist/gates/phantom-apis.test.js +320 -0
- package/dist/gates/runner.js +37 -15
- package/dist/gates/test-quality.d.ts +67 -0
- package/dist/gates/test-quality.js +512 -0
- package/dist/gates/test-quality.test.d.ts +1 -0
- package/dist/gates/test-quality.test.js +312 -0
- package/dist/templates/index.js +31 -1
- package/dist/types/index.d.ts +348 -0
- package/dist/types/index.js +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const mockFindFiles = vi.hoisted(() => vi.fn());
|
|
3
|
+
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
4
|
+
const mockPathExists = vi.hoisted(() => vi.fn());
|
|
5
|
+
vi.mock('../utils/scanner.js', () => ({
|
|
6
|
+
FileScanner: { findFiles: mockFindFiles },
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('fs-extra', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
readFile: mockReadFile,
|
|
11
|
+
pathExists: mockPathExists,
|
|
12
|
+
pathExistsSync: vi.fn().mockReturnValue(false),
|
|
13
|
+
readFileSync: vi.fn().mockReturnValue(''),
|
|
14
|
+
readJson: vi.fn().mockResolvedValue(null),
|
|
15
|
+
readdirSync: vi.fn().mockReturnValue([]),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
import { PhantomApisGate } from './phantom-apis.js';
|
|
19
|
+
describe('PhantomApisGate — Node.js', () => {
|
|
20
|
+
let gate;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
gate = new PhantomApisGate();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
it('should flag non-existent fs methods', async () => {
|
|
26
|
+
mockFindFiles.mockResolvedValue(['src/utils.ts']);
|
|
27
|
+
mockReadFile.mockResolvedValue(`
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
const data = fs.readFileAsync('test.txt');
|
|
30
|
+
const result = fs.writeFilePromise('out.txt', data);
|
|
31
|
+
`);
|
|
32
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
33
|
+
expect(failures).toHaveLength(1);
|
|
34
|
+
expect(failures[0].details).toContain('readFileAsync');
|
|
35
|
+
expect(failures[0].details).toContain('writeFilePromise');
|
|
36
|
+
});
|
|
37
|
+
it('should NOT flag real fs methods', async () => {
|
|
38
|
+
mockFindFiles.mockResolvedValue(['src/utils.ts']);
|
|
39
|
+
mockReadFile.mockResolvedValue(`
|
|
40
|
+
import fs from 'fs';
|
|
41
|
+
const data = fs.readFileSync('test.txt', 'utf-8');
|
|
42
|
+
fs.writeFileSync('out.txt', data);
|
|
43
|
+
fs.existsSync('/tmp');
|
|
44
|
+
const stream = fs.createReadStream('big.txt');
|
|
45
|
+
`);
|
|
46
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
47
|
+
expect(failures).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
it('should flag non-existent path methods', async () => {
|
|
50
|
+
mockFindFiles.mockResolvedValue(['src/paths.ts']);
|
|
51
|
+
mockReadFile.mockResolvedValue(`
|
|
52
|
+
import path from 'path';
|
|
53
|
+
const combined = path.combine('a', 'b');
|
|
54
|
+
const exists = path.exists('/tmp');
|
|
55
|
+
`);
|
|
56
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
57
|
+
expect(failures).toHaveLength(1);
|
|
58
|
+
expect(failures[0].details).toContain('combine');
|
|
59
|
+
expect(failures[0].details).toContain('exists');
|
|
60
|
+
});
|
|
61
|
+
it('should NOT flag real path methods', async () => {
|
|
62
|
+
mockFindFiles.mockResolvedValue(['src/paths.ts']);
|
|
63
|
+
mockReadFile.mockResolvedValue(`
|
|
64
|
+
import path from 'path';
|
|
65
|
+
const p = path.join('a', 'b');
|
|
66
|
+
const ext = path.extname('file.txt');
|
|
67
|
+
const abs = path.resolve('.');
|
|
68
|
+
`);
|
|
69
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
70
|
+
expect(failures).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
it('should flag non-existent crypto methods', async () => {
|
|
73
|
+
mockFindFiles.mockResolvedValue(['src/crypto.ts']);
|
|
74
|
+
mockReadFile.mockResolvedValue(`
|
|
75
|
+
import crypto from 'crypto';
|
|
76
|
+
const hash = crypto.generateHash('sha256', 'data');
|
|
77
|
+
const key = crypto.createKey('aes-256');
|
|
78
|
+
`);
|
|
79
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
80
|
+
expect(failures).toHaveLength(1);
|
|
81
|
+
expect(failures[0].details).toContain('generateHash');
|
|
82
|
+
expect(failures[0].details).toContain('createKey');
|
|
83
|
+
});
|
|
84
|
+
it('should NOT flag real crypto methods', async () => {
|
|
85
|
+
mockFindFiles.mockResolvedValue(['src/crypto.ts']);
|
|
86
|
+
mockReadFile.mockResolvedValue(`
|
|
87
|
+
import crypto from 'crypto';
|
|
88
|
+
const hash = crypto.createHash('sha256');
|
|
89
|
+
const uuid = crypto.randomUUID();
|
|
90
|
+
`);
|
|
91
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
92
|
+
expect(failures).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
it('should handle require() imports', async () => {
|
|
95
|
+
mockFindFiles.mockResolvedValue(['src/legacy.js']);
|
|
96
|
+
mockReadFile.mockResolvedValue(`
|
|
97
|
+
const fs = require('fs');
|
|
98
|
+
fs.readFileAsync('data.txt');
|
|
99
|
+
`);
|
|
100
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
101
|
+
expect(failures).toHaveLength(1);
|
|
102
|
+
expect(failures[0].details).toContain('readFileAsync');
|
|
103
|
+
});
|
|
104
|
+
it('should handle node: protocol imports', async () => {
|
|
105
|
+
mockFindFiles.mockResolvedValue(['src/modern.ts']);
|
|
106
|
+
mockReadFile.mockResolvedValue(`
|
|
107
|
+
import os from 'node:os';
|
|
108
|
+
const info = os.cpuInfo();
|
|
109
|
+
`);
|
|
110
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
111
|
+
expect(failures).toHaveLength(1);
|
|
112
|
+
expect(failures[0].details).toContain('cpuInfo');
|
|
113
|
+
});
|
|
114
|
+
it('should suggest closest real method', async () => {
|
|
115
|
+
mockFindFiles.mockResolvedValue(['src/typo.ts']);
|
|
116
|
+
mockReadFile.mockResolvedValue(`
|
|
117
|
+
import fs from 'fs';
|
|
118
|
+
fs.readFileSyn('data.txt');
|
|
119
|
+
`);
|
|
120
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
121
|
+
expect(failures).toHaveLength(1);
|
|
122
|
+
expect(failures[0].details).toContain('readFileSync');
|
|
123
|
+
});
|
|
124
|
+
it('should not scan when disabled', async () => {
|
|
125
|
+
const disabled = new PhantomApisGate({ enabled: false });
|
|
126
|
+
const failures = await disabled.run({ cwd: '/project' });
|
|
127
|
+
expect(failures).toHaveLength(0);
|
|
128
|
+
expect(mockFindFiles).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('PhantomApisGate — Python', () => {
|
|
132
|
+
let gate;
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
gate = new PhantomApisGate();
|
|
135
|
+
vi.clearAllMocks();
|
|
136
|
+
});
|
|
137
|
+
it('should flag non-existent os methods', async () => {
|
|
138
|
+
mockFindFiles.mockResolvedValue(['utils.py']);
|
|
139
|
+
mockReadFile.mockResolvedValue(`
|
|
140
|
+
import os
|
|
141
|
+
current = os.getCurrentDirectory()
|
|
142
|
+
files = os.listFiles('.')
|
|
143
|
+
`);
|
|
144
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
145
|
+
expect(failures).toHaveLength(1);
|
|
146
|
+
expect(failures[0].details).toContain('getCurrentDirectory');
|
|
147
|
+
expect(failures[0].details).toContain('listFiles');
|
|
148
|
+
});
|
|
149
|
+
it('should NOT flag real os methods', async () => {
|
|
150
|
+
mockFindFiles.mockResolvedValue(['utils.py']);
|
|
151
|
+
mockReadFile.mockResolvedValue(`
|
|
152
|
+
import os
|
|
153
|
+
current = os.getcwd()
|
|
154
|
+
files = os.listdir('.')
|
|
155
|
+
exists = os.path
|
|
156
|
+
`);
|
|
157
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
158
|
+
expect(failures).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
it('should flag non-existent json methods', async () => {
|
|
161
|
+
mockFindFiles.mockResolvedValue(['parser.py']);
|
|
162
|
+
mockReadFile.mockResolvedValue(`
|
|
163
|
+
import json
|
|
164
|
+
data = json.parse('{"key": "value"}')
|
|
165
|
+
result = json.stringify(data)
|
|
166
|
+
`);
|
|
167
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
168
|
+
expect(failures).toHaveLength(1);
|
|
169
|
+
expect(failures[0].details).toContain('parse');
|
|
170
|
+
expect(failures[0].details).toContain('stringify');
|
|
171
|
+
});
|
|
172
|
+
it('should NOT flag real json methods', async () => {
|
|
173
|
+
mockFindFiles.mockResolvedValue(['parser.py']);
|
|
174
|
+
mockReadFile.mockResolvedValue(`
|
|
175
|
+
import json
|
|
176
|
+
data = json.loads('{"key": "value"}')
|
|
177
|
+
output = json.dumps(data)
|
|
178
|
+
`);
|
|
179
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
180
|
+
expect(failures).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
it('should handle aliased imports', async () => {
|
|
183
|
+
mockFindFiles.mockResolvedValue(['utils.py']);
|
|
184
|
+
mockReadFile.mockResolvedValue(`
|
|
185
|
+
import os as operating_system
|
|
186
|
+
result = operating_system.getCurrentDirectory()
|
|
187
|
+
`);
|
|
188
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
189
|
+
expect(failures).toHaveLength(1);
|
|
190
|
+
expect(failures[0].details).toContain('getCurrentDirectory');
|
|
191
|
+
});
|
|
192
|
+
it('should flag non-existent subprocess methods', async () => {
|
|
193
|
+
mockFindFiles.mockResolvedValue(['runner.py']);
|
|
194
|
+
mockReadFile.mockResolvedValue(`
|
|
195
|
+
import subprocess
|
|
196
|
+
result = subprocess.execute(['ls', '-la'])
|
|
197
|
+
`);
|
|
198
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
199
|
+
expect(failures).toHaveLength(1);
|
|
200
|
+
expect(failures[0].details).toContain('execute');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('PhantomApisGate — Go', () => {
|
|
204
|
+
let gate;
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
gate = new PhantomApisGate();
|
|
207
|
+
vi.clearAllMocks();
|
|
208
|
+
});
|
|
209
|
+
it('should flag Python-style Go method names', async () => {
|
|
210
|
+
mockFindFiles.mockResolvedValue(['main.go']);
|
|
211
|
+
mockReadFile.mockResolvedValue(`
|
|
212
|
+
package main
|
|
213
|
+
import "strings"
|
|
214
|
+
func main() {
|
|
215
|
+
result := strings.includes("hello", "ell")
|
|
216
|
+
}
|
|
217
|
+
`);
|
|
218
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
219
|
+
expect(failures).toHaveLength(1);
|
|
220
|
+
expect(failures[0].details).toContain('includes');
|
|
221
|
+
expect(failures[0].details).toContain('strings.Contains');
|
|
222
|
+
});
|
|
223
|
+
it('should flag JS-style JSON methods in Go', async () => {
|
|
224
|
+
mockFindFiles.mockResolvedValue(['parser.go']);
|
|
225
|
+
mockReadFile.mockResolvedValue(`
|
|
226
|
+
package main
|
|
227
|
+
import "encoding/json"
|
|
228
|
+
func parse() {
|
|
229
|
+
data := json.parse(raw)
|
|
230
|
+
out := json.stringify(data)
|
|
231
|
+
}
|
|
232
|
+
`);
|
|
233
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
234
|
+
expect(failures).toHaveLength(1);
|
|
235
|
+
expect(failures[0].details).toContain('parse');
|
|
236
|
+
});
|
|
237
|
+
it('should flag os.Exists in Go', async () => {
|
|
238
|
+
mockFindFiles.mockResolvedValue(['files.go']);
|
|
239
|
+
mockReadFile.mockResolvedValue(`
|
|
240
|
+
package main
|
|
241
|
+
import "os"
|
|
242
|
+
func check() {
|
|
243
|
+
exists := os.Exists("/tmp/file")
|
|
244
|
+
}
|
|
245
|
+
`);
|
|
246
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
247
|
+
expect(failures).toHaveLength(1);
|
|
248
|
+
expect(failures[0].details).toContain('Exists');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
describe('PhantomApisGate — C#', () => {
|
|
252
|
+
let gate;
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
gate = new PhantomApisGate();
|
|
255
|
+
vi.clearAllMocks();
|
|
256
|
+
});
|
|
257
|
+
it('should flag Java-style method casing in C#', async () => {
|
|
258
|
+
mockFindFiles.mockResolvedValue(['Program.cs']);
|
|
259
|
+
mockReadFile.mockResolvedValue(`
|
|
260
|
+
using System;
|
|
261
|
+
class Program {
|
|
262
|
+
void Main() {
|
|
263
|
+
string s = "hello";
|
|
264
|
+
int len = s.length;
|
|
265
|
+
bool eq = s.equals("world");
|
|
266
|
+
string str = s.toString();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`);
|
|
270
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
271
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
272
|
+
expect(failures[0].details).toMatch(/length|equals|toString/);
|
|
273
|
+
});
|
|
274
|
+
it('should flag Java collections in C#', async () => {
|
|
275
|
+
mockFindFiles.mockResolvedValue(['Service.cs']);
|
|
276
|
+
mockReadFile.mockResolvedValue(`
|
|
277
|
+
using System.Collections.Generic;
|
|
278
|
+
List<string> items = new ArrayList<string>();
|
|
279
|
+
HashMap<string, int> map = new HashMap<string, int>();
|
|
280
|
+
System.out.println("hello");
|
|
281
|
+
`);
|
|
282
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
283
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
describe('PhantomApisGate — Java', () => {
|
|
287
|
+
let gate;
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
gate = new PhantomApisGate();
|
|
290
|
+
vi.clearAllMocks();
|
|
291
|
+
});
|
|
292
|
+
it('should flag JS/Python-style method names in Java', async () => {
|
|
293
|
+
mockFindFiles.mockResolvedValue(['Main.java']);
|
|
294
|
+
mockReadFile.mockResolvedValue(`
|
|
295
|
+
import java.util.List;
|
|
296
|
+
class Main {
|
|
297
|
+
void test() {
|
|
298
|
+
List<String> items = new ArrayList<>();
|
|
299
|
+
items.push("hello");
|
|
300
|
+
items.includes("test");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
`);
|
|
304
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
305
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
306
|
+
expect(failures[0].details).toMatch(/push|includes/);
|
|
307
|
+
});
|
|
308
|
+
it('should flag new List() in Java', async () => {
|
|
309
|
+
mockFindFiles.mockResolvedValue(['Service.java']);
|
|
310
|
+
mockReadFile.mockResolvedValue(`
|
|
311
|
+
import java.util.*;
|
|
312
|
+
class Service {
|
|
313
|
+
List<String> items = new List<String>();
|
|
314
|
+
Map<String, Integer> map = new Map<String, Integer>();
|
|
315
|
+
}
|
|
316
|
+
`);
|
|
317
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
318
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
319
|
+
});
|
|
320
|
+
});
|
package/dist/gates/runner.js
CHANGED
|
@@ -18,6 +18,9 @@ import { HallucinatedImportsGate } from './hallucinated-imports.js';
|
|
|
18
18
|
import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
|
|
19
19
|
import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
|
|
20
20
|
import { PromiseSafetyGate } from './promise-safety.js';
|
|
21
|
+
import { PhantomApisGate } from './phantom-apis.js';
|
|
22
|
+
import { DeprecatedApisGate } from './deprecated-apis.js';
|
|
23
|
+
import { TestQualityGate } from './test-quality.js';
|
|
21
24
|
import { execa } from 'execa';
|
|
22
25
|
import { Logger } from '../utils/logger.js';
|
|
23
26
|
export class GateRunner {
|
|
@@ -78,6 +81,16 @@ export class GateRunner {
|
|
|
78
81
|
if (this.config.gates.promise_safety?.enabled !== false) {
|
|
79
82
|
this.gates.push(new PromiseSafetyGate(this.config.gates.promise_safety));
|
|
80
83
|
}
|
|
84
|
+
// v3.1+ Extended Hallucination Detection
|
|
85
|
+
if (this.config.gates.phantom_apis?.enabled !== false) {
|
|
86
|
+
this.gates.push(new PhantomApisGate(this.config.gates.phantom_apis));
|
|
87
|
+
}
|
|
88
|
+
if (this.config.gates.deprecated_apis?.enabled !== false) {
|
|
89
|
+
this.gates.push(new DeprecatedApisGate(this.config.gates.deprecated_apis));
|
|
90
|
+
}
|
|
91
|
+
if (this.config.gates.test_quality?.enabled !== false) {
|
|
92
|
+
this.gates.push(new TestQualityGate(this.config.gates.test_quality));
|
|
93
|
+
}
|
|
81
94
|
// Environment Alignment Gate (Should be prioritized)
|
|
82
95
|
if (this.config.gates.environment?.enabled) {
|
|
83
96
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
@@ -162,20 +175,34 @@ export class GateRunner {
|
|
|
162
175
|
}
|
|
163
176
|
const score = Math.max(0, 100 - totalDeduction);
|
|
164
177
|
// Two-score system: separate AI health from structural quality
|
|
178
|
+
// IMPORTANT: Only ai-drift affects ai_health_score, only traditional affects structural_score.
|
|
179
|
+
// Security and governance affect the overall score but NOT the sub-scores,
|
|
180
|
+
// preventing security criticals from incorrectly zeroing structural_score.
|
|
165
181
|
let aiDeduction = 0;
|
|
166
|
-
let aiCount = 0;
|
|
167
182
|
let structuralDeduction = 0;
|
|
168
|
-
|
|
183
|
+
const provenanceCounts = {
|
|
184
|
+
'ai-drift': 0,
|
|
185
|
+
'traditional': 0,
|
|
186
|
+
'security': 0,
|
|
187
|
+
'governance': 0,
|
|
188
|
+
};
|
|
169
189
|
for (const f of failures) {
|
|
170
190
|
const sev = (f.severity || 'medium');
|
|
171
191
|
const weight = SEVERITY_WEIGHTS[sev] ?? 5;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
const prov = f.provenance || 'traditional';
|
|
193
|
+
provenanceCounts[prov] = (provenanceCounts[prov] || 0) + 1;
|
|
194
|
+
switch (prov) {
|
|
195
|
+
case 'ai-drift':
|
|
196
|
+
aiDeduction += weight;
|
|
197
|
+
break;
|
|
198
|
+
case 'traditional':
|
|
199
|
+
structuralDeduction += weight;
|
|
200
|
+
break;
|
|
201
|
+
// security and governance contribute to overall score (totalDeduction)
|
|
202
|
+
// but do NOT pollute the sub-scores
|
|
203
|
+
case 'security':
|
|
204
|
+
case 'governance':
|
|
205
|
+
break;
|
|
179
206
|
}
|
|
180
207
|
}
|
|
181
208
|
return {
|
|
@@ -188,12 +215,7 @@ export class GateRunner {
|
|
|
188
215
|
ai_health_score: Math.max(0, 100 - aiDeduction),
|
|
189
216
|
structural_score: Math.max(0, 100 - structuralDeduction),
|
|
190
217
|
severity_breakdown: severityBreakdown,
|
|
191
|
-
provenance_breakdown:
|
|
192
|
-
'ai-drift': aiCount,
|
|
193
|
-
traditional: structuralCount - failures.filter(f => f.provenance === 'security' || f.provenance === 'governance').length,
|
|
194
|
-
security: failures.filter(f => f.provenance === 'security').length,
|
|
195
|
-
governance: failures.filter(f => f.provenance === 'governance').length,
|
|
196
|
-
},
|
|
218
|
+
provenance_breakdown: provenanceCounts,
|
|
197
219
|
},
|
|
198
220
|
};
|
|
199
221
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Test Quality Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects AI-generated test anti-patterns that create false confidence.
|
|
5
|
+
* AI models generate tests that look comprehensive but actually test
|
|
6
|
+
* the AI's own assumptions rather than the developer's intent.
|
|
7
|
+
*
|
|
8
|
+
* Detected anti-patterns:
|
|
9
|
+
* 1. Empty test bodies — tests with no assertions
|
|
10
|
+
* 2. Tautological assertions — expect(true).toBe(true), assert True
|
|
11
|
+
* 3. Mock-everything — tests that mock every dependency (test nothing real)
|
|
12
|
+
* 4. Missing error path tests — only happy path tested
|
|
13
|
+
* 5. Shallow snapshot abuse — snapshot tests with no semantic assertions
|
|
14
|
+
* 6. Assertion-free async — async tests that never await/assert
|
|
15
|
+
*
|
|
16
|
+
* Supported test frameworks:
|
|
17
|
+
* JS/TS — Jest, Vitest, Mocha, Jasmine, Node test runner
|
|
18
|
+
* Python — pytest, unittest
|
|
19
|
+
* Go — testing package (t.Run, table-driven tests)
|
|
20
|
+
* Java — JUnit 4/5, TestNG
|
|
21
|
+
* Kotlin — JUnit 5, kotlin.test
|
|
22
|
+
*
|
|
23
|
+
* @since v3.0.0
|
|
24
|
+
* @since v3.0.3 — Go, Java, Kotlin support added
|
|
25
|
+
*/
|
|
26
|
+
import { Gate, GateContext } from './base.js';
|
|
27
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
28
|
+
export interface TestQualityIssue {
|
|
29
|
+
file: string;
|
|
30
|
+
line: number;
|
|
31
|
+
pattern: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
export interface TestQualityConfig {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
check_empty_tests?: boolean;
|
|
37
|
+
check_tautological?: boolean;
|
|
38
|
+
check_mock_heavy?: boolean;
|
|
39
|
+
check_snapshot_abuse?: boolean;
|
|
40
|
+
check_assertion_free_async?: boolean;
|
|
41
|
+
max_mocks_per_test?: number;
|
|
42
|
+
ignore_patterns?: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare class TestQualityGate extends Gate {
|
|
45
|
+
private config;
|
|
46
|
+
constructor(config?: TestQualityConfig);
|
|
47
|
+
protected get provenance(): Provenance;
|
|
48
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
49
|
+
private checkJSTestQuality;
|
|
50
|
+
private analyzeJSTestBlock;
|
|
51
|
+
private checkPythonTestQuality;
|
|
52
|
+
private analyzePythonTestBlock;
|
|
53
|
+
/**
|
|
54
|
+
* Go test quality checks.
|
|
55
|
+
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
56
|
+
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
57
|
+
* Also checks for t.Run subtests and table-driven patterns.
|
|
58
|
+
*/
|
|
59
|
+
private checkGoTestQuality;
|
|
60
|
+
/**
|
|
61
|
+
* Java/Kotlin test quality checks.
|
|
62
|
+
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
63
|
+
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
64
|
+
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
65
|
+
*/
|
|
66
|
+
private checkJavaKotlinTestQuality;
|
|
67
|
+
}
|