@rigour-labs/core 3.0.3 → 3.0.5
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/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 +13 -0
- 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,312 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const mockFindFiles = vi.hoisted(() => vi.fn());
|
|
3
|
+
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock('../utils/scanner.js', () => ({
|
|
5
|
+
FileScanner: { findFiles: mockFindFiles },
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('fs-extra', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
readFile: mockReadFile,
|
|
10
|
+
pathExists: vi.fn().mockResolvedValue(false),
|
|
11
|
+
pathExistsSync: vi.fn().mockReturnValue(false),
|
|
12
|
+
readFileSync: vi.fn().mockReturnValue(''),
|
|
13
|
+
readJson: vi.fn().mockResolvedValue(null),
|
|
14
|
+
readdirSync: vi.fn().mockReturnValue([]),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
import { TestQualityGate } from './test-quality.js';
|
|
18
|
+
describe('TestQualityGate — JS/TS tests', () => {
|
|
19
|
+
let gate;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
gate = new TestQualityGate();
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it('should flag empty test bodies', async () => {
|
|
25
|
+
mockFindFiles.mockResolvedValue(['src/utils.test.ts']);
|
|
26
|
+
mockReadFile.mockResolvedValue(`
|
|
27
|
+
describe('Utils', () => {
|
|
28
|
+
it('should do something', () => {
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
`);
|
|
32
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
33
|
+
expect(failures).toHaveLength(1);
|
|
34
|
+
expect(failures[0].details).toContain('empty-test');
|
|
35
|
+
});
|
|
36
|
+
it('should flag tests with no assertions', async () => {
|
|
37
|
+
mockFindFiles.mockResolvedValue(['src/api.test.ts']);
|
|
38
|
+
mockReadFile.mockResolvedValue(`
|
|
39
|
+
describe('API', () => {
|
|
40
|
+
it('should fetch data', () => {
|
|
41
|
+
const data = fetchData();
|
|
42
|
+
console.log(data);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
`);
|
|
46
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
47
|
+
expect(failures).toHaveLength(1);
|
|
48
|
+
expect(failures[0].details).toContain('no-assertion');
|
|
49
|
+
});
|
|
50
|
+
it('should flag tautological assertions', async () => {
|
|
51
|
+
mockFindFiles.mockResolvedValue(['src/basic.test.ts']);
|
|
52
|
+
mockReadFile.mockResolvedValue(`
|
|
53
|
+
describe('Basic', () => {
|
|
54
|
+
it('should pass', () => {
|
|
55
|
+
expect(true).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
`);
|
|
59
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
60
|
+
expect(failures).toHaveLength(1);
|
|
61
|
+
expect(failures[0].details).toContain('tautological');
|
|
62
|
+
});
|
|
63
|
+
it('should flag mock-heavy tests', async () => {
|
|
64
|
+
mockFindFiles.mockResolvedValue(['src/service.test.ts']);
|
|
65
|
+
mockReadFile.mockResolvedValue(`
|
|
66
|
+
describe('Service', () => {
|
|
67
|
+
it('should process', () => {
|
|
68
|
+
const mock1 = vi.fn();
|
|
69
|
+
const mock2 = vi.fn();
|
|
70
|
+
const mock3 = vi.fn();
|
|
71
|
+
const mock4 = vi.fn();
|
|
72
|
+
const mock5 = vi.fn();
|
|
73
|
+
const mock6 = vi.fn();
|
|
74
|
+
expect(mock1).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
`);
|
|
78
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
79
|
+
expect(failures).toHaveLength(1);
|
|
80
|
+
expect(failures[0].details).toContain('mock-heavy');
|
|
81
|
+
});
|
|
82
|
+
it('should NOT flag well-written tests', async () => {
|
|
83
|
+
mockFindFiles.mockResolvedValue(['src/good.test.ts']);
|
|
84
|
+
mockReadFile.mockResolvedValue(`
|
|
85
|
+
describe('Calculator', () => {
|
|
86
|
+
it('should add two numbers', () => {
|
|
87
|
+
const result = add(2, 3);
|
|
88
|
+
expect(result).toBe(5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle negative numbers', () => {
|
|
92
|
+
const result = add(-1, -2);
|
|
93
|
+
expect(result).toBe(-3);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
`);
|
|
97
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
98
|
+
expect(failures).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
it('should NOT flag tests with expect().toEqual()', async () => {
|
|
101
|
+
mockFindFiles.mockResolvedValue(['src/valid.test.ts']);
|
|
102
|
+
mockReadFile.mockResolvedValue(`
|
|
103
|
+
describe('Parser', () => {
|
|
104
|
+
it('should parse JSON', () => {
|
|
105
|
+
const result = parseConfig('{"key": "value"}');
|
|
106
|
+
expect(result).toEqual({ key: 'value' });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
`);
|
|
110
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
111
|
+
expect(failures).toHaveLength(0);
|
|
112
|
+
});
|
|
113
|
+
it('should not scan when disabled', async () => {
|
|
114
|
+
const disabled = new TestQualityGate({ enabled: false });
|
|
115
|
+
const failures = await disabled.run({ cwd: '/project' });
|
|
116
|
+
expect(failures).toHaveLength(0);
|
|
117
|
+
expect(mockFindFiles).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('TestQualityGate — Python tests', () => {
|
|
121
|
+
let gate;
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
gate = new TestQualityGate();
|
|
124
|
+
vi.clearAllMocks();
|
|
125
|
+
});
|
|
126
|
+
it('should flag empty test functions', async () => {
|
|
127
|
+
mockFindFiles.mockResolvedValue(['test_utils.py']);
|
|
128
|
+
mockReadFile.mockResolvedValue(`
|
|
129
|
+
def test_something():
|
|
130
|
+
pass
|
|
131
|
+
`);
|
|
132
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
133
|
+
expect(failures).toHaveLength(1);
|
|
134
|
+
expect(failures[0].details).toContain('empty-test');
|
|
135
|
+
});
|
|
136
|
+
it('should flag tests with no assertions', async () => {
|
|
137
|
+
mockFindFiles.mockResolvedValue(['test_api.py']);
|
|
138
|
+
mockReadFile.mockResolvedValue(`
|
|
139
|
+
def test_fetch_data():
|
|
140
|
+
data = fetch_data()
|
|
141
|
+
print(data)
|
|
142
|
+
result = process(data)
|
|
143
|
+
`);
|
|
144
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
145
|
+
expect(failures).toHaveLength(1);
|
|
146
|
+
expect(failures[0].details).toContain('no-assertion');
|
|
147
|
+
});
|
|
148
|
+
it('should flag tautological Python assertions', async () => {
|
|
149
|
+
mockFindFiles.mockResolvedValue(['test_basic.py']);
|
|
150
|
+
mockReadFile.mockResolvedValue(`
|
|
151
|
+
def test_always_passes():
|
|
152
|
+
assert True
|
|
153
|
+
`);
|
|
154
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
155
|
+
expect(failures).toHaveLength(1);
|
|
156
|
+
expect(failures[0].details).toContain('tautological');
|
|
157
|
+
});
|
|
158
|
+
it('should NOT flag Python tests with real assertions', async () => {
|
|
159
|
+
mockFindFiles.mockResolvedValue(['test_calc.py']);
|
|
160
|
+
mockReadFile.mockResolvedValue(`
|
|
161
|
+
def test_addition():
|
|
162
|
+
result = add(2, 3)
|
|
163
|
+
assert result == 5
|
|
164
|
+
|
|
165
|
+
def test_subtraction():
|
|
166
|
+
result = subtract(5, 3)
|
|
167
|
+
assert result == 2
|
|
168
|
+
`);
|
|
169
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
170
|
+
expect(failures).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
it('should NOT flag tests using pytest.raises', async () => {
|
|
173
|
+
mockFindFiles.mockResolvedValue(['test_errors.py']);
|
|
174
|
+
mockReadFile.mockResolvedValue(`
|
|
175
|
+
def test_invalid_input():
|
|
176
|
+
with pytest.raises(ValueError):
|
|
177
|
+
parse_int("not a number")
|
|
178
|
+
`);
|
|
179
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
180
|
+
expect(failures).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('TestQualityGate — Go tests', () => {
|
|
184
|
+
let gate;
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
gate = new TestQualityGate();
|
|
187
|
+
vi.clearAllMocks();
|
|
188
|
+
});
|
|
189
|
+
it('should flag empty Go test functions', async () => {
|
|
190
|
+
mockFindFiles.mockResolvedValue(['utils_test.go']);
|
|
191
|
+
mockReadFile.mockResolvedValue(`
|
|
192
|
+
package utils
|
|
193
|
+
|
|
194
|
+
import "testing"
|
|
195
|
+
|
|
196
|
+
func TestSomething(t *testing.T) {
|
|
197
|
+
}
|
|
198
|
+
`);
|
|
199
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
200
|
+
expect(failures).toHaveLength(1);
|
|
201
|
+
expect(failures[0].details).toContain('empty-test');
|
|
202
|
+
});
|
|
203
|
+
it('should flag Go tests without assertions', async () => {
|
|
204
|
+
mockFindFiles.mockResolvedValue(['calc_test.go']);
|
|
205
|
+
mockReadFile.mockResolvedValue(`
|
|
206
|
+
package calc
|
|
207
|
+
|
|
208
|
+
import "testing"
|
|
209
|
+
|
|
210
|
+
func TestAdd(t *testing.T) {
|
|
211
|
+
result := Add(2, 3)
|
|
212
|
+
_ = result
|
|
213
|
+
}
|
|
214
|
+
`);
|
|
215
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
216
|
+
expect(failures).toHaveLength(1);
|
|
217
|
+
expect(failures[0].details).toContain('no-assertion');
|
|
218
|
+
});
|
|
219
|
+
it('should NOT flag Go tests with t.Error assertions', async () => {
|
|
220
|
+
mockFindFiles.mockResolvedValue(['good_test.go']);
|
|
221
|
+
mockReadFile.mockResolvedValue(`
|
|
222
|
+
package calc
|
|
223
|
+
|
|
224
|
+
import "testing"
|
|
225
|
+
|
|
226
|
+
func TestAdd(t *testing.T) {
|
|
227
|
+
result := Add(2, 3)
|
|
228
|
+
if result != 5 {
|
|
229
|
+
t.Errorf("expected 5, got %d", result)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
`);
|
|
233
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
234
|
+
expect(failures).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
it('should NOT flag Go tests with testify assertions', async () => {
|
|
237
|
+
mockFindFiles.mockResolvedValue(['testify_test.go']);
|
|
238
|
+
mockReadFile.mockResolvedValue(`
|
|
239
|
+
package main
|
|
240
|
+
|
|
241
|
+
import (
|
|
242
|
+
"testing"
|
|
243
|
+
"github.com/stretchr/testify/assert"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
func TestAdd(t *testing.T) {
|
|
247
|
+
result := Add(2, 3)
|
|
248
|
+
assert.Equal(t, 5, result)
|
|
249
|
+
}
|
|
250
|
+
`);
|
|
251
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
252
|
+
expect(failures).toHaveLength(0);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
describe('TestQualityGate — Java tests', () => {
|
|
256
|
+
let gate;
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
gate = new TestQualityGate();
|
|
259
|
+
vi.clearAllMocks();
|
|
260
|
+
});
|
|
261
|
+
it('should flag Java tests without assertions', async () => {
|
|
262
|
+
mockFindFiles.mockResolvedValue(['CalcTest.java']);
|
|
263
|
+
mockReadFile.mockResolvedValue(`
|
|
264
|
+
import org.junit.jupiter.api.Test;
|
|
265
|
+
|
|
266
|
+
class CalcTest {
|
|
267
|
+
@Test
|
|
268
|
+
void testAdd() {
|
|
269
|
+
int result = Calculator.add(2, 3);
|
|
270
|
+
System.out.println(result);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
`);
|
|
274
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
275
|
+
expect(failures).toHaveLength(1);
|
|
276
|
+
expect(failures[0].details).toContain('no-assertion');
|
|
277
|
+
});
|
|
278
|
+
it('should NOT flag Java tests with assertions', async () => {
|
|
279
|
+
mockFindFiles.mockResolvedValue(['GoodTest.java']);
|
|
280
|
+
mockReadFile.mockResolvedValue(`
|
|
281
|
+
import org.junit.jupiter.api.Test;
|
|
282
|
+
import static org.junit.jupiter.api.Assertions.*;
|
|
283
|
+
|
|
284
|
+
class GoodTest {
|
|
285
|
+
@Test
|
|
286
|
+
void testAdd() {
|
|
287
|
+
int result = Calculator.add(2, 3);
|
|
288
|
+
assertEquals(5, result);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`);
|
|
292
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
293
|
+
expect(failures).toHaveLength(0);
|
|
294
|
+
});
|
|
295
|
+
it('should flag tautological Java assertions', async () => {
|
|
296
|
+
mockFindFiles.mockResolvedValue(['TautTest.java']);
|
|
297
|
+
mockReadFile.mockResolvedValue(`
|
|
298
|
+
import org.junit.jupiter.api.Test;
|
|
299
|
+
import static org.junit.jupiter.api.Assertions.*;
|
|
300
|
+
|
|
301
|
+
class TautTest {
|
|
302
|
+
@Test
|
|
303
|
+
void testAlwaysPasses() {
|
|
304
|
+
assertTrue(true);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`);
|
|
308
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
309
|
+
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
310
|
+
expect(failures[0].details).toContain('tautological');
|
|
311
|
+
});
|
|
312
|
+
});
|
package/dist/templates/index.js
CHANGED
|
@@ -481,11 +481,41 @@ export const UNIVERSAL_CONFIG = {
|
|
|
481
481
|
check_unsafe_fetch: true,
|
|
482
482
|
ignore_patterns: [],
|
|
483
483
|
},
|
|
484
|
+
phantom_apis: {
|
|
485
|
+
enabled: true,
|
|
486
|
+
check_node: true,
|
|
487
|
+
check_python: true,
|
|
488
|
+
check_go: true,
|
|
489
|
+
check_csharp: true,
|
|
490
|
+
check_java: true,
|
|
491
|
+
ignore_patterns: [],
|
|
492
|
+
},
|
|
493
|
+
deprecated_apis: {
|
|
494
|
+
enabled: true,
|
|
495
|
+
check_node: true,
|
|
496
|
+
check_python: true,
|
|
497
|
+
check_web: true,
|
|
498
|
+
check_go: true,
|
|
499
|
+
check_csharp: true,
|
|
500
|
+
check_java: true,
|
|
501
|
+
block_security_deprecated: true,
|
|
502
|
+
ignore_patterns: [],
|
|
503
|
+
},
|
|
504
|
+
test_quality: {
|
|
505
|
+
enabled: true,
|
|
506
|
+
check_empty_tests: true,
|
|
507
|
+
check_tautological: true,
|
|
508
|
+
check_mock_heavy: true,
|
|
509
|
+
check_snapshot_abuse: true,
|
|
510
|
+
check_assertion_free_async: true,
|
|
511
|
+
max_mocks_per_test: 5,
|
|
512
|
+
ignore_patterns: [],
|
|
513
|
+
},
|
|
484
514
|
},
|
|
485
515
|
hooks: {
|
|
486
516
|
enabled: false,
|
|
487
517
|
tools: [],
|
|
488
|
-
fast_gates: ['hallucinated-imports', 'promise-safety', 'security-patterns', 'file-size'],
|
|
518
|
+
fast_gates: ['hallucinated-imports', 'phantom-apis', 'deprecated-apis', 'promise-safety', 'security-patterns', 'file-size'],
|
|
489
519
|
timeout_ms: 5000,
|
|
490
520
|
block_on_failure: false,
|
|
491
521
|
},
|