@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.
@@ -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
+ });
@@ -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
  },