@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
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hallucinated Imports Gate —
|
|
2
|
+
* Hallucinated Imports Gate — Comprehensive Regression Tests
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Coverage: Go, Python, JS/TS, Ruby, C#, Rust, Java, Kotlin
|
|
5
|
+
*
|
|
6
|
+
* Tests the fix for Go stdlib false positives (PicoClaw regression)
|
|
7
|
+
* and validates all 8 language checkers for false positive/negative accuracy.
|
|
8
8
|
*
|
|
9
9
|
* @since v3.0.1
|
|
10
10
|
*/
|
|
11
11
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
12
|
import { HallucinatedImportsGate } from './hallucinated-imports.js';
|
|
13
|
-
// fs-extra is mocked via module-level mock fns above
|
|
14
13
|
// Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
|
|
15
|
-
const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson } = vi.hoisted(() => ({
|
|
14
|
+
const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson, mockReaddirSync } = vi.hoisted(() => ({
|
|
16
15
|
mockPathExists: vi.fn(),
|
|
17
16
|
mockPathExistsSync: vi.fn(),
|
|
18
17
|
mockReadFile: vi.fn(),
|
|
19
18
|
mockReadFileSync: vi.fn(),
|
|
20
19
|
mockReadJson: vi.fn(),
|
|
20
|
+
mockReaddirSync: vi.fn().mockReturnValue([]),
|
|
21
21
|
}));
|
|
22
22
|
vi.mock('fs-extra', () => {
|
|
23
23
|
const mock = {
|
|
@@ -26,6 +26,7 @@ vi.mock('fs-extra', () => {
|
|
|
26
26
|
readFile: mockReadFile,
|
|
27
27
|
readFileSync: mockReadFileSync,
|
|
28
28
|
readJson: mockReadJson,
|
|
29
|
+
readdirSync: mockReaddirSync,
|
|
29
30
|
};
|
|
30
31
|
return {
|
|
31
32
|
...mock,
|
|
@@ -39,21 +40,18 @@ vi.mock('../utils/scanner.js', () => ({
|
|
|
39
40
|
},
|
|
40
41
|
}));
|
|
41
42
|
import { FileScanner } from '../utils/scanner.js';
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════
|
|
44
|
+
// GO
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════
|
|
42
46
|
describe('HallucinatedImportsGate — Go stdlib false positives', () => {
|
|
43
47
|
let gate;
|
|
44
48
|
const testCwd = '/tmp/test-go-project';
|
|
45
|
-
const context = {
|
|
46
|
-
cwd: testCwd,
|
|
47
|
-
ignore: [],
|
|
48
|
-
};
|
|
49
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
49
50
|
beforeEach(() => {
|
|
50
51
|
vi.clearAllMocks();
|
|
52
|
+
mockReaddirSync.mockReturnValue([]);
|
|
51
53
|
gate = new HallucinatedImportsGate({ enabled: true });
|
|
52
54
|
});
|
|
53
|
-
/**
|
|
54
|
-
* This is the exact scenario from PicoClaw — Go stdlib packages with slashes
|
|
55
|
-
* were being flagged as hallucinated. These are ALL real Go stdlib packages.
|
|
56
|
-
*/
|
|
57
55
|
it('should NOT flag Go standard library packages as hallucinated (PicoClaw regression)', async () => {
|
|
58
56
|
const goFileContent = `package main
|
|
59
57
|
|
|
@@ -96,13 +94,11 @@ import (
|
|
|
96
94
|
|
|
97
95
|
func main() {}
|
|
98
96
|
`;
|
|
99
|
-
|
|
100
|
-
FileScanner.findFiles.mockResolvedValue([goFile]);
|
|
97
|
+
FileScanner.findFiles.mockResolvedValue(['main.go']);
|
|
101
98
|
mockReadFile.mockResolvedValue(goFileContent);
|
|
102
99
|
mockPathExists.mockResolvedValue(false);
|
|
103
|
-
mockPathExistsSync.mockReturnValue(false);
|
|
100
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
104
101
|
const failures = await gate.run(context);
|
|
105
|
-
// ZERO failures — every import above is a real Go stdlib package
|
|
106
102
|
expect(failures).toHaveLength(0);
|
|
107
103
|
});
|
|
108
104
|
it('should NOT flag external module imports (github.com, etc.)', async () => {
|
|
@@ -147,17 +143,14 @@ func main() {}
|
|
|
147
143
|
return goFileContent;
|
|
148
144
|
});
|
|
149
145
|
mockPathExists.mockResolvedValue(false);
|
|
150
|
-
mockPathExistsSync.mockReturnValue(true);
|
|
146
|
+
mockPathExistsSync.mockReturnValue(true);
|
|
151
147
|
mockReadFileSync.mockReturnValue(goMod);
|
|
152
148
|
const failures = await gate.run(context);
|
|
153
|
-
// Should flag doesnotexist but NOT realmodule
|
|
154
149
|
expect(failures).toHaveLength(1);
|
|
155
150
|
expect(failures[0].details).toContain('doesnotexist');
|
|
156
151
|
expect(failures[0].details).not.toContain('realmodule');
|
|
157
152
|
});
|
|
158
153
|
it('should NOT flag anything when no go.mod exists and imports have no dots', async () => {
|
|
159
|
-
// Without go.mod, we can't determine the project module path,
|
|
160
|
-
// so we skip project-relative checks to avoid false positives
|
|
161
154
|
const goFileContent = `package main
|
|
162
155
|
|
|
163
156
|
import (
|
|
@@ -211,15 +204,16 @@ func main() {}
|
|
|
211
204
|
expect(failures).toHaveLength(0);
|
|
212
205
|
});
|
|
213
206
|
});
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════
|
|
208
|
+
// PYTHON
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════
|
|
214
210
|
describe('HallucinatedImportsGate — Python stdlib coverage', () => {
|
|
215
211
|
let gate;
|
|
216
212
|
const testCwd = '/tmp/test-py-project';
|
|
217
|
-
const context = {
|
|
218
|
-
cwd: testCwd,
|
|
219
|
-
ignore: [],
|
|
220
|
-
};
|
|
213
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
221
214
|
beforeEach(() => {
|
|
222
215
|
vi.clearAllMocks();
|
|
216
|
+
mockReaddirSync.mockReturnValue([]);
|
|
223
217
|
gate = new HallucinatedImportsGate({ enabled: true });
|
|
224
218
|
});
|
|
225
219
|
it('should NOT flag Python standard library imports', async () => {
|
|
@@ -254,15 +248,16 @@ from urllib.parse import urlparse
|
|
|
254
248
|
expect(failures).toHaveLength(0);
|
|
255
249
|
});
|
|
256
250
|
});
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════
|
|
252
|
+
// JS/TS (Node.js)
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════
|
|
257
254
|
describe('HallucinatedImportsGate — JS/TS Node builtins', () => {
|
|
258
255
|
let gate;
|
|
259
256
|
const testCwd = '/tmp/test-node-project';
|
|
260
|
-
const context = {
|
|
261
|
-
cwd: testCwd,
|
|
262
|
-
ignore: [],
|
|
263
|
-
};
|
|
257
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
264
258
|
beforeEach(() => {
|
|
265
259
|
vi.clearAllMocks();
|
|
260
|
+
mockReaddirSync.mockReturnValue([]);
|
|
266
261
|
gate = new HallucinatedImportsGate({ enabled: true });
|
|
267
262
|
});
|
|
268
263
|
it('should NOT flag Node.js built-in modules', async () => {
|
|
@@ -285,4 +280,685 @@ import { join } from 'node:path';
|
|
|
285
280
|
const failures = await gate.run(context);
|
|
286
281
|
expect(failures).toHaveLength(0);
|
|
287
282
|
});
|
|
283
|
+
it('should NOT flag Node 22.x built-in modules (async_hooks, diagnostics_channel, etc.)', async () => {
|
|
284
|
+
const jsContent = `
|
|
285
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
286
|
+
import dc from 'diagnostics_channel';
|
|
287
|
+
import { readFile } from 'fs/promises';
|
|
288
|
+
import test from 'test';
|
|
289
|
+
import wt from 'worker_threads';
|
|
290
|
+
import timers from 'timers/promises';
|
|
291
|
+
import { ReadableStream } from 'stream/web';
|
|
292
|
+
`;
|
|
293
|
+
FileScanner.findFiles.mockResolvedValue(['server.ts']);
|
|
294
|
+
mockReadFile.mockResolvedValue(jsContent);
|
|
295
|
+
mockPathExists.mockResolvedValue(false);
|
|
296
|
+
const failures = await gate.run(context);
|
|
297
|
+
expect(failures).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════
|
|
301
|
+
// RUBY
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════
|
|
303
|
+
describe('HallucinatedImportsGate — Ruby imports', () => {
|
|
304
|
+
let gate;
|
|
305
|
+
const testCwd = '/tmp/test-ruby-project';
|
|
306
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
307
|
+
beforeEach(() => {
|
|
308
|
+
vi.clearAllMocks();
|
|
309
|
+
mockReaddirSync.mockReturnValue([]);
|
|
310
|
+
gate = new HallucinatedImportsGate({ enabled: true });
|
|
311
|
+
});
|
|
312
|
+
it('should NOT flag Ruby standard library requires', async () => {
|
|
313
|
+
const rbContent = `
|
|
314
|
+
require 'json'
|
|
315
|
+
require 'yaml'
|
|
316
|
+
require 'net/http'
|
|
317
|
+
require 'uri'
|
|
318
|
+
require 'fileutils'
|
|
319
|
+
require 'pathname'
|
|
320
|
+
require 'open3'
|
|
321
|
+
require 'digest'
|
|
322
|
+
require 'openssl'
|
|
323
|
+
require 'csv'
|
|
324
|
+
require 'set'
|
|
325
|
+
require 'date'
|
|
326
|
+
require 'time'
|
|
327
|
+
require 'tempfile'
|
|
328
|
+
require 'securerandom'
|
|
329
|
+
require 'logger'
|
|
330
|
+
require 'socket'
|
|
331
|
+
require 'erb'
|
|
332
|
+
require 'optparse'
|
|
333
|
+
require 'stringio'
|
|
334
|
+
require 'zlib'
|
|
335
|
+
require 'base64'
|
|
336
|
+
require 'benchmark'
|
|
337
|
+
require 'singleton'
|
|
338
|
+
require 'forwardable'
|
|
339
|
+
require 'shellwords'
|
|
340
|
+
require 'bigdecimal'
|
|
341
|
+
`;
|
|
342
|
+
FileScanner.findFiles.mockResolvedValue(['app.rb']);
|
|
343
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
344
|
+
mockPathExists.mockResolvedValue(false);
|
|
345
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
346
|
+
const failures = await gate.run(context);
|
|
347
|
+
expect(failures).toHaveLength(0);
|
|
348
|
+
});
|
|
349
|
+
it('should NOT flag gems listed in Gemfile', async () => {
|
|
350
|
+
const rbContent = `
|
|
351
|
+
require 'rails'
|
|
352
|
+
require 'pg'
|
|
353
|
+
require 'puma'
|
|
354
|
+
require 'sidekiq'
|
|
355
|
+
require 'devise'
|
|
356
|
+
`;
|
|
357
|
+
const gemfile = `
|
|
358
|
+
source 'https://rubygems.org'
|
|
359
|
+
|
|
360
|
+
gem 'rails', '~> 7.0'
|
|
361
|
+
gem 'pg'
|
|
362
|
+
gem 'puma', '~> 6.0'
|
|
363
|
+
gem 'sidekiq'
|
|
364
|
+
gem 'devise'
|
|
365
|
+
`;
|
|
366
|
+
FileScanner.findFiles.mockResolvedValue(['app.rb']);
|
|
367
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
368
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Gemfile'));
|
|
369
|
+
mockReadFileSync.mockReturnValue(gemfile);
|
|
370
|
+
mockPathExists.mockResolvedValue(false);
|
|
371
|
+
const failures = await gate.run(context);
|
|
372
|
+
expect(failures).toHaveLength(0);
|
|
373
|
+
});
|
|
374
|
+
it('should flag unknown requires when Gemfile exists', async () => {
|
|
375
|
+
const rbContent = `
|
|
376
|
+
require 'json'
|
|
377
|
+
require 'nonexistent_gem_abcxyz'
|
|
378
|
+
`;
|
|
379
|
+
const gemfile = `
|
|
380
|
+
source 'https://rubygems.org'
|
|
381
|
+
gem 'rails'
|
|
382
|
+
`;
|
|
383
|
+
FileScanner.findFiles.mockResolvedValue(['app.rb']);
|
|
384
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
385
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Gemfile'));
|
|
386
|
+
mockReadFileSync.mockReturnValue(gemfile);
|
|
387
|
+
mockPathExists.mockResolvedValue(false);
|
|
388
|
+
const failures = await gate.run(context);
|
|
389
|
+
expect(failures).toHaveLength(1);
|
|
390
|
+
expect(failures[0].details).toContain('nonexistent_gem_abcxyz');
|
|
391
|
+
});
|
|
392
|
+
it('should flag broken require_relative paths', async () => {
|
|
393
|
+
const rbContent = `
|
|
394
|
+
require_relative 'lib/helpers'
|
|
395
|
+
require_relative 'models/user'
|
|
396
|
+
`;
|
|
397
|
+
FileScanner.findFiles.mockResolvedValue(['app.rb']);
|
|
398
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
399
|
+
mockPathExists.mockResolvedValue(false);
|
|
400
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
401
|
+
const failures = await gate.run(context);
|
|
402
|
+
// Both require_relative should fail since no matching .rb files exist
|
|
403
|
+
expect(failures).toHaveLength(1); // grouped into 1 failure by file
|
|
404
|
+
expect(failures[0].details).toContain('lib/helpers');
|
|
405
|
+
expect(failures[0].details).toContain('models/user');
|
|
406
|
+
});
|
|
407
|
+
it('should NOT flag require_relative that resolves to project files', async () => {
|
|
408
|
+
const rbContent = `
|
|
409
|
+
require_relative 'lib/helpers'
|
|
410
|
+
`;
|
|
411
|
+
FileScanner.findFiles.mockResolvedValue(['app.rb', 'lib/helpers.rb']);
|
|
412
|
+
mockReadFile.mockImplementation(async (filePath) => {
|
|
413
|
+
if (filePath.includes('helpers.rb'))
|
|
414
|
+
return 'module Helpers; end';
|
|
415
|
+
return rbContent;
|
|
416
|
+
});
|
|
417
|
+
mockPathExists.mockResolvedValue(false);
|
|
418
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
419
|
+
const failures = await gate.run(context);
|
|
420
|
+
expect(failures).toHaveLength(0);
|
|
421
|
+
});
|
|
422
|
+
it('should NOT flag gems from .gemspec add_dependency', async () => {
|
|
423
|
+
const rbContent = `
|
|
424
|
+
require 'thor'
|
|
425
|
+
require 'httparty'
|
|
426
|
+
`;
|
|
427
|
+
const gemspec = `
|
|
428
|
+
Gem::Specification.new do |spec|
|
|
429
|
+
spec.name = "mygem"
|
|
430
|
+
spec.add_dependency "thor", "~> 1.0"
|
|
431
|
+
spec.add_runtime_dependency "httparty"
|
|
432
|
+
end
|
|
433
|
+
`;
|
|
434
|
+
FileScanner.findFiles.mockResolvedValue(['lib/mygem.rb']);
|
|
435
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
436
|
+
mockPathExistsSync.mockImplementation((p) => !p.includes('Gemfile'));
|
|
437
|
+
mockReaddirSync.mockReturnValue(['mygem.gemspec']);
|
|
438
|
+
mockReadFileSync.mockReturnValue(gemspec);
|
|
439
|
+
mockPathExists.mockResolvedValue(false);
|
|
440
|
+
const failures = await gate.run(context);
|
|
441
|
+
expect(failures).toHaveLength(0);
|
|
442
|
+
});
|
|
443
|
+
it('should skip flagging requires when no Gemfile context exists', async () => {
|
|
444
|
+
// Without Gemfile or gemspec, we can't distinguish installed gems from hallucinated ones
|
|
445
|
+
const rbContent = `
|
|
446
|
+
require 'some_unknown_gem'
|
|
447
|
+
`;
|
|
448
|
+
FileScanner.findFiles.mockResolvedValue(['script.rb']);
|
|
449
|
+
mockReadFile.mockResolvedValue(rbContent);
|
|
450
|
+
mockPathExists.mockResolvedValue(false);
|
|
451
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
452
|
+
const failures = await gate.run(context);
|
|
453
|
+
// No Gemfile = gemDeps.size === 0 → skip flagging to avoid false positives
|
|
454
|
+
expect(failures).toHaveLength(0);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
// ═══════════════════════════════════════════════════════════════
|
|
458
|
+
// C# (.NET)
|
|
459
|
+
// ═══════════════════════════════════════════════════════════════
|
|
460
|
+
describe('HallucinatedImportsGate — C# imports', () => {
|
|
461
|
+
let gate;
|
|
462
|
+
const testCwd = '/tmp/test-csharp-project';
|
|
463
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
464
|
+
beforeEach(() => {
|
|
465
|
+
vi.clearAllMocks();
|
|
466
|
+
mockReaddirSync.mockReturnValue([]);
|
|
467
|
+
gate = new HallucinatedImportsGate({ enabled: true });
|
|
468
|
+
});
|
|
469
|
+
it('should NOT flag .NET framework namespaces', async () => {
|
|
470
|
+
const csContent = `
|
|
471
|
+
using System;
|
|
472
|
+
using System.Collections.Generic;
|
|
473
|
+
using System.Linq;
|
|
474
|
+
using System.Threading.Tasks;
|
|
475
|
+
using System.IO;
|
|
476
|
+
using System.Net.Http;
|
|
477
|
+
using System.Text;
|
|
478
|
+
using System.Text.Json;
|
|
479
|
+
using Microsoft.AspNetCore.Mvc;
|
|
480
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
481
|
+
using Microsoft.Extensions.Logging;
|
|
482
|
+
using Microsoft.EntityFrameworkCore;
|
|
483
|
+
`;
|
|
484
|
+
FileScanner.findFiles.mockResolvedValue(['Controllers/HomeController.cs']);
|
|
485
|
+
mockReadFile.mockResolvedValue(csContent);
|
|
486
|
+
mockPathExists.mockResolvedValue(false);
|
|
487
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
488
|
+
const failures = await gate.run(context);
|
|
489
|
+
expect(failures).toHaveLength(0);
|
|
490
|
+
});
|
|
491
|
+
it('should NOT flag NuGet packages from .csproj', async () => {
|
|
492
|
+
const csContent = `
|
|
493
|
+
using Newtonsoft.Json;
|
|
494
|
+
using Serilog;
|
|
495
|
+
using AutoMapper;
|
|
496
|
+
using FluentValidation;
|
|
497
|
+
using MediatR;
|
|
498
|
+
`;
|
|
499
|
+
const csproj = `
|
|
500
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
501
|
+
<ItemGroup>
|
|
502
|
+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
503
|
+
<PackageReference Include="Serilog" Version="3.1.1" />
|
|
504
|
+
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
|
505
|
+
<PackageReference Include="FluentValidation" Version="11.8.0" />
|
|
506
|
+
<PackageReference Include="MediatR" Version="12.2.0" />
|
|
507
|
+
</ItemGroup>
|
|
508
|
+
</Project>
|
|
509
|
+
`;
|
|
510
|
+
FileScanner.findFiles.mockResolvedValue(['Program.cs']);
|
|
511
|
+
mockReadFile.mockResolvedValue(csContent);
|
|
512
|
+
mockPathExists.mockResolvedValue(false);
|
|
513
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
514
|
+
mockReaddirSync.mockReturnValue(['MyProject.csproj']);
|
|
515
|
+
mockReadFileSync.mockReturnValue(csproj);
|
|
516
|
+
const failures = await gate.run(context);
|
|
517
|
+
expect(failures).toHaveLength(0);
|
|
518
|
+
});
|
|
519
|
+
it('should NOT flag using static directives', async () => {
|
|
520
|
+
const csContent = `
|
|
521
|
+
using System;
|
|
522
|
+
using static System.Math;
|
|
523
|
+
using static System.Console;
|
|
524
|
+
`;
|
|
525
|
+
FileScanner.findFiles.mockResolvedValue(['Helper.cs']);
|
|
526
|
+
mockReadFile.mockResolvedValue(csContent);
|
|
527
|
+
mockPathExists.mockResolvedValue(false);
|
|
528
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
529
|
+
const failures = await gate.run(context);
|
|
530
|
+
expect(failures).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
it('should NOT flag using disposable pattern (using var = ...)', async () => {
|
|
533
|
+
const csContent = `
|
|
534
|
+
using System;
|
|
535
|
+
using (var stream = new FileStream("test.txt", FileMode.Open))
|
|
536
|
+
{
|
|
537
|
+
// Should not be parsed as a namespace import
|
|
538
|
+
}
|
|
539
|
+
`;
|
|
540
|
+
FileScanner.findFiles.mockResolvedValue(['Program.cs']);
|
|
541
|
+
mockReadFile.mockResolvedValue(csContent);
|
|
542
|
+
mockPathExists.mockResolvedValue(false);
|
|
543
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
544
|
+
const failures = await gate.run(context);
|
|
545
|
+
expect(failures).toHaveLength(0);
|
|
546
|
+
});
|
|
547
|
+
it('should flag project-relative namespaces that do not resolve (with .csproj)', async () => {
|
|
548
|
+
const csContent = `
|
|
549
|
+
using System;
|
|
550
|
+
using MyProject.Services.UserService;
|
|
551
|
+
using MyProject.Models.DoesNotExist;
|
|
552
|
+
`;
|
|
553
|
+
const csContent2 = `
|
|
554
|
+
namespace MyProject.Services.UserService
|
|
555
|
+
{
|
|
556
|
+
public class UserService { }
|
|
557
|
+
}
|
|
558
|
+
`;
|
|
559
|
+
const csproj = `<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>`;
|
|
560
|
+
FileScanner.findFiles.mockResolvedValue([
|
|
561
|
+
'Controllers/HomeController.cs',
|
|
562
|
+
'Services/UserService/UserService.cs',
|
|
563
|
+
]);
|
|
564
|
+
mockReadFile.mockImplementation(async (filePath) => {
|
|
565
|
+
if (filePath.includes('UserService'))
|
|
566
|
+
return csContent2;
|
|
567
|
+
return csContent;
|
|
568
|
+
});
|
|
569
|
+
mockPathExists.mockResolvedValue(false);
|
|
570
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
571
|
+
mockReaddirSync.mockReturnValue(['MyProject.csproj']);
|
|
572
|
+
mockReadFileSync.mockReturnValue(csproj);
|
|
573
|
+
const failures = await gate.run(context);
|
|
574
|
+
// Should flag DoesNotExist but not UserService
|
|
575
|
+
expect(failures).toHaveLength(1);
|
|
576
|
+
expect(failures[0].details).toContain('DoesNotExist');
|
|
577
|
+
expect(failures[0].details).not.toContain('UserService');
|
|
578
|
+
});
|
|
579
|
+
it('should NOT flag common ecosystem NuGet packages', async () => {
|
|
580
|
+
const csContent = `
|
|
581
|
+
using Xunit;
|
|
582
|
+
using Moq;
|
|
583
|
+
using FluentAssertions;
|
|
584
|
+
using NUnit.Framework;
|
|
585
|
+
using Dapper;
|
|
586
|
+
using Polly;
|
|
587
|
+
using StackExchange.Redis;
|
|
588
|
+
using Npgsql;
|
|
589
|
+
using Grpc.Core;
|
|
590
|
+
`;
|
|
591
|
+
FileScanner.findFiles.mockResolvedValue(['Tests.cs']);
|
|
592
|
+
mockReadFile.mockResolvedValue(csContent);
|
|
593
|
+
mockPathExists.mockResolvedValue(false);
|
|
594
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
595
|
+
const failures = await gate.run(context);
|
|
596
|
+
expect(failures).toHaveLength(0);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
// ═══════════════════════════════════════════════════════════════
|
|
600
|
+
// RUST
|
|
601
|
+
// ═══════════════════════════════════════════════════════════════
|
|
602
|
+
describe('HallucinatedImportsGate — Rust imports', () => {
|
|
603
|
+
let gate;
|
|
604
|
+
const testCwd = '/tmp/test-rust-project';
|
|
605
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
606
|
+
beforeEach(() => {
|
|
607
|
+
vi.clearAllMocks();
|
|
608
|
+
mockReaddirSync.mockReturnValue([]);
|
|
609
|
+
gate = new HallucinatedImportsGate({ enabled: true });
|
|
610
|
+
});
|
|
611
|
+
it('should NOT flag Rust std library crates', async () => {
|
|
612
|
+
const rsContent = `
|
|
613
|
+
use std::collections::HashMap;
|
|
614
|
+
use std::io::{self, Read, Write};
|
|
615
|
+
use std::fs;
|
|
616
|
+
use std::path::PathBuf;
|
|
617
|
+
use std::sync::Arc;
|
|
618
|
+
use core::fmt;
|
|
619
|
+
use alloc::vec::Vec;
|
|
620
|
+
`;
|
|
621
|
+
FileScanner.findFiles.mockResolvedValue(['src/main.rs']);
|
|
622
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
623
|
+
mockPathExists.mockResolvedValue(false);
|
|
624
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
625
|
+
const failures = await gate.run(context);
|
|
626
|
+
expect(failures).toHaveLength(0);
|
|
627
|
+
});
|
|
628
|
+
it('should NOT flag crates listed in Cargo.toml', async () => {
|
|
629
|
+
const rsContent = `
|
|
630
|
+
use serde::{Serialize, Deserialize};
|
|
631
|
+
use tokio::runtime::Runtime;
|
|
632
|
+
use reqwest::Client;
|
|
633
|
+
use clap::Parser;
|
|
634
|
+
`;
|
|
635
|
+
const cargoToml = `
|
|
636
|
+
[package]
|
|
637
|
+
name = "my-project"
|
|
638
|
+
version = "0.1.0"
|
|
639
|
+
|
|
640
|
+
[dependencies]
|
|
641
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
642
|
+
tokio = { version = "1.0", features = ["full"] }
|
|
643
|
+
reqwest = "0.11"
|
|
644
|
+
clap = { version = "4.0", features = ["derive"] }
|
|
645
|
+
`;
|
|
646
|
+
FileScanner.findFiles.mockResolvedValue(['src/main.rs']);
|
|
647
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
648
|
+
mockPathExists.mockResolvedValue(false);
|
|
649
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
650
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
651
|
+
const failures = await gate.run(context);
|
|
652
|
+
expect(failures).toHaveLength(0);
|
|
653
|
+
});
|
|
654
|
+
it('should handle Cargo.toml dash-to-underscore conversion', async () => {
|
|
655
|
+
const rsContent = `
|
|
656
|
+
use my_crate::something;
|
|
657
|
+
use another_lib::util;
|
|
658
|
+
`;
|
|
659
|
+
const cargoToml = `
|
|
660
|
+
[dependencies]
|
|
661
|
+
my-crate = "1.0"
|
|
662
|
+
another-lib = "2.0"
|
|
663
|
+
`;
|
|
664
|
+
FileScanner.findFiles.mockResolvedValue(['src/main.rs']);
|
|
665
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
666
|
+
mockPathExists.mockResolvedValue(false);
|
|
667
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
668
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
669
|
+
const failures = await gate.run(context);
|
|
670
|
+
expect(failures).toHaveLength(0);
|
|
671
|
+
});
|
|
672
|
+
it('should NOT flag crate/self/super keywords', async () => {
|
|
673
|
+
const rsContent = `
|
|
674
|
+
use crate::config::Settings;
|
|
675
|
+
use self::helpers::format;
|
|
676
|
+
use super::parent_module;
|
|
677
|
+
`;
|
|
678
|
+
FileScanner.findFiles.mockResolvedValue(['src/lib.rs']);
|
|
679
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
680
|
+
mockPathExists.mockResolvedValue(false);
|
|
681
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
682
|
+
const failures = await gate.run(context);
|
|
683
|
+
expect(failures).toHaveLength(0);
|
|
684
|
+
});
|
|
685
|
+
it('should flag unknown extern crate not in Cargo.toml', async () => {
|
|
686
|
+
const rsContent = `
|
|
687
|
+
extern crate serde;
|
|
688
|
+
extern crate nonexistent_crate;
|
|
689
|
+
`;
|
|
690
|
+
const cargoToml = `
|
|
691
|
+
[dependencies]
|
|
692
|
+
serde = "1.0"
|
|
693
|
+
`;
|
|
694
|
+
FileScanner.findFiles.mockResolvedValue(['src/main.rs']);
|
|
695
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
696
|
+
mockPathExists.mockResolvedValue(false);
|
|
697
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
698
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
699
|
+
const failures = await gate.run(context);
|
|
700
|
+
expect(failures).toHaveLength(1);
|
|
701
|
+
expect(failures[0].details).toContain('nonexistent_crate');
|
|
702
|
+
expect(failures[0].details).not.toContain('serde');
|
|
703
|
+
});
|
|
704
|
+
it('should flag unknown use crate not in Cargo.toml', async () => {
|
|
705
|
+
const rsContent = `
|
|
706
|
+
use serde::Serialize;
|
|
707
|
+
use fake_crate::FakeStruct;
|
|
708
|
+
`;
|
|
709
|
+
const cargoToml = `
|
|
710
|
+
[dependencies]
|
|
711
|
+
serde = "1.0"
|
|
712
|
+
`;
|
|
713
|
+
FileScanner.findFiles.mockResolvedValue(['src/main.rs']);
|
|
714
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
715
|
+
mockPathExists.mockResolvedValue(false);
|
|
716
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
717
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
718
|
+
const failures = await gate.run(context);
|
|
719
|
+
expect(failures).toHaveLength(1);
|
|
720
|
+
expect(failures[0].details).toContain('fake_crate');
|
|
721
|
+
});
|
|
722
|
+
it('should NOT flag pub use re-exports of known crates', async () => {
|
|
723
|
+
const rsContent = `
|
|
724
|
+
pub use serde::Serialize;
|
|
725
|
+
pub use std::collections::HashMap;
|
|
726
|
+
`;
|
|
727
|
+
const cargoToml = `
|
|
728
|
+
[dependencies]
|
|
729
|
+
serde = "1.0"
|
|
730
|
+
`;
|
|
731
|
+
FileScanner.findFiles.mockResolvedValue(['src/lib.rs']);
|
|
732
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
733
|
+
mockPathExists.mockResolvedValue(false);
|
|
734
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
735
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
736
|
+
const failures = await gate.run(context);
|
|
737
|
+
expect(failures).toHaveLength(0);
|
|
738
|
+
});
|
|
739
|
+
it('should handle [dev-dependencies] and [build-dependencies]', async () => {
|
|
740
|
+
const rsContent = `
|
|
741
|
+
use criterion::Criterion;
|
|
742
|
+
use cc::Build;
|
|
743
|
+
`;
|
|
744
|
+
const cargoToml = `
|
|
745
|
+
[dependencies]
|
|
746
|
+
serde = "1.0"
|
|
747
|
+
|
|
748
|
+
[dev-dependencies]
|
|
749
|
+
criterion = "0.5"
|
|
750
|
+
|
|
751
|
+
[build-dependencies]
|
|
752
|
+
cc = "1.0"
|
|
753
|
+
`;
|
|
754
|
+
FileScanner.findFiles.mockResolvedValue(['benches/bench.rs']);
|
|
755
|
+
mockReadFile.mockResolvedValue(rsContent);
|
|
756
|
+
mockPathExists.mockResolvedValue(false);
|
|
757
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('Cargo.toml'));
|
|
758
|
+
mockReadFileSync.mockReturnValue(cargoToml);
|
|
759
|
+
const failures = await gate.run(context);
|
|
760
|
+
expect(failures).toHaveLength(0);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
// ═══════════════════════════════════════════════════════════════
|
|
764
|
+
// JAVA
|
|
765
|
+
// ═══════════════════════════════════════════════════════════════
|
|
766
|
+
describe('HallucinatedImportsGate — Java imports', () => {
|
|
767
|
+
let gate;
|
|
768
|
+
const testCwd = '/tmp/test-java-project';
|
|
769
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
770
|
+
beforeEach(() => {
|
|
771
|
+
vi.clearAllMocks();
|
|
772
|
+
mockReaddirSync.mockReturnValue([]);
|
|
773
|
+
gate = new HallucinatedImportsGate({ enabled: true });
|
|
774
|
+
});
|
|
775
|
+
it('should NOT flag Java standard library imports', async () => {
|
|
776
|
+
const javaContent = `
|
|
777
|
+
import java.util.List;
|
|
778
|
+
import java.util.Map;
|
|
779
|
+
import java.util.HashMap;
|
|
780
|
+
import java.io.File;
|
|
781
|
+
import java.io.IOException;
|
|
782
|
+
import java.net.URL;
|
|
783
|
+
import java.time.LocalDateTime;
|
|
784
|
+
import java.util.stream.Collectors;
|
|
785
|
+
import javax.net.ssl.SSLContext;
|
|
786
|
+
import jakarta.servlet.http.HttpServletRequest;
|
|
787
|
+
`;
|
|
788
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/java/App.java']);
|
|
789
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
790
|
+
mockPathExists.mockResolvedValue(false);
|
|
791
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
792
|
+
const failures = await gate.run(context);
|
|
793
|
+
expect(failures).toHaveLength(0);
|
|
794
|
+
});
|
|
795
|
+
it('should NOT flag import static for Java stdlib', async () => {
|
|
796
|
+
const javaContent = `
|
|
797
|
+
import static java.lang.Math.max;
|
|
798
|
+
import static java.util.Collections.emptyList;
|
|
799
|
+
`;
|
|
800
|
+
FileScanner.findFiles.mockResolvedValue(['Helper.java']);
|
|
801
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
802
|
+
mockPathExists.mockResolvedValue(false);
|
|
803
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
804
|
+
const failures = await gate.run(context);
|
|
805
|
+
expect(failures).toHaveLength(0);
|
|
806
|
+
});
|
|
807
|
+
it('should NOT flag build.gradle dependencies', async () => {
|
|
808
|
+
const javaContent = `
|
|
809
|
+
import com.google.guava.collect.ImmutableList;
|
|
810
|
+
import org.springframework.boot.SpringApplication;
|
|
811
|
+
import io.netty.channel.Channel;
|
|
812
|
+
`;
|
|
813
|
+
const buildGradle = `
|
|
814
|
+
plugins {
|
|
815
|
+
id 'java'
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
dependencies {
|
|
819
|
+
implementation 'com.google.guava:guava:32.0.0-jre'
|
|
820
|
+
implementation 'org.springframework.boot:spring-boot-starter:3.1.0'
|
|
821
|
+
implementation 'io.netty:netty-all:4.1.100'
|
|
822
|
+
}
|
|
823
|
+
`;
|
|
824
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/java/App.java']);
|
|
825
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
826
|
+
mockPathExists.mockResolvedValue(false);
|
|
827
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('build.gradle'));
|
|
828
|
+
mockReadFileSync.mockReturnValue(buildGradle);
|
|
829
|
+
const failures = await gate.run(context);
|
|
830
|
+
expect(failures).toHaveLength(0);
|
|
831
|
+
});
|
|
832
|
+
it('should NOT flag pom.xml dependencies', async () => {
|
|
833
|
+
const javaContent = `
|
|
834
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
835
|
+
import org.apache.commons.lang3.StringUtils;
|
|
836
|
+
`;
|
|
837
|
+
const pomXml = `
|
|
838
|
+
<project>
|
|
839
|
+
<dependencies>
|
|
840
|
+
<dependency>
|
|
841
|
+
<groupId>com.fasterxml.jackson</groupId>
|
|
842
|
+
<artifactId>jackson-databind</artifactId>
|
|
843
|
+
</dependency>
|
|
844
|
+
<dependency>
|
|
845
|
+
<groupId>org.apache.commons</groupId>
|
|
846
|
+
<artifactId>commons-lang3</artifactId>
|
|
847
|
+
</dependency>
|
|
848
|
+
</dependencies>
|
|
849
|
+
</project>
|
|
850
|
+
`;
|
|
851
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/java/App.java']);
|
|
852
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
853
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('pom.xml'));
|
|
854
|
+
mockReadFileSync.mockReturnValue(pomXml);
|
|
855
|
+
mockPathExists.mockResolvedValue(false);
|
|
856
|
+
const failures = await gate.run(context);
|
|
857
|
+
expect(failures).toHaveLength(0);
|
|
858
|
+
});
|
|
859
|
+
it('should flag unknown imports when build deps context exists', async () => {
|
|
860
|
+
const javaContent = `
|
|
861
|
+
import java.util.List;
|
|
862
|
+
import com.nonexistent.hallucinated.FakeClass;
|
|
863
|
+
`;
|
|
864
|
+
const buildGradle = `
|
|
865
|
+
dependencies {
|
|
866
|
+
implementation 'org.springframework:spring-core:6.0.0'
|
|
867
|
+
}
|
|
868
|
+
`;
|
|
869
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/java/App.java']);
|
|
870
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
871
|
+
mockPathExists.mockResolvedValue(false);
|
|
872
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('build.gradle'));
|
|
873
|
+
mockReadFileSync.mockReturnValue(buildGradle);
|
|
874
|
+
const failures = await gate.run(context);
|
|
875
|
+
expect(failures).toHaveLength(1);
|
|
876
|
+
expect(failures[0].details).toContain('com.nonexistent.hallucinated.FakeClass');
|
|
877
|
+
});
|
|
878
|
+
it('should NOT flag when no build context exists (avoid false positives)', async () => {
|
|
879
|
+
const javaContent = `
|
|
880
|
+
import com.example.whatever.SomeClass;
|
|
881
|
+
`;
|
|
882
|
+
FileScanner.findFiles.mockResolvedValue(['App.java']);
|
|
883
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
884
|
+
mockPathExists.mockResolvedValue(false);
|
|
885
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
886
|
+
const failures = await gate.run(context);
|
|
887
|
+
expect(failures).toHaveLength(0);
|
|
888
|
+
});
|
|
889
|
+
it('should NOT flag common test framework imports', async () => {
|
|
890
|
+
const javaContent = `
|
|
891
|
+
import org.junit.jupiter.api.Test;
|
|
892
|
+
import org.junit.jupiter.api.Assertions;
|
|
893
|
+
import org.slf4j.Logger;
|
|
894
|
+
import org.slf4j.LoggerFactory;
|
|
895
|
+
`;
|
|
896
|
+
FileScanner.findFiles.mockResolvedValue(['Test.java']);
|
|
897
|
+
mockReadFile.mockResolvedValue(javaContent);
|
|
898
|
+
mockPathExists.mockResolvedValue(false);
|
|
899
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
900
|
+
const failures = await gate.run(context);
|
|
901
|
+
expect(failures).toHaveLength(0);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════
|
|
905
|
+
// KOTLIN
|
|
906
|
+
// ═══════════════════════════════════════════════════════════════
|
|
907
|
+
describe('HallucinatedImportsGate — Kotlin imports', () => {
|
|
908
|
+
let gate;
|
|
909
|
+
const testCwd = '/tmp/test-kotlin-project';
|
|
910
|
+
const context = { cwd: testCwd, ignore: [] };
|
|
911
|
+
beforeEach(() => {
|
|
912
|
+
vi.clearAllMocks();
|
|
913
|
+
mockReaddirSync.mockReturnValue([]);
|
|
914
|
+
gate = new HallucinatedImportsGate({ enabled: true });
|
|
915
|
+
});
|
|
916
|
+
it('should NOT flag Kotlin standard library imports', async () => {
|
|
917
|
+
const ktContent = `
|
|
918
|
+
import kotlin.collections.mutableListOf
|
|
919
|
+
import kotlin.coroutines.coroutineContext
|
|
920
|
+
import kotlinx.coroutines.launch
|
|
921
|
+
import kotlinx.coroutines.flow.Flow
|
|
922
|
+
import kotlinx.serialization.Serializable
|
|
923
|
+
`;
|
|
924
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/kotlin/App.kt']);
|
|
925
|
+
mockReadFile.mockResolvedValue(ktContent);
|
|
926
|
+
mockPathExists.mockResolvedValue(false);
|
|
927
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
928
|
+
const failures = await gate.run(context);
|
|
929
|
+
expect(failures).toHaveLength(0);
|
|
930
|
+
});
|
|
931
|
+
it('should NOT flag Java stdlib imports from Kotlin (interop)', async () => {
|
|
932
|
+
const ktContent = `
|
|
933
|
+
import java.util.UUID
|
|
934
|
+
import java.io.File
|
|
935
|
+
import java.time.Instant
|
|
936
|
+
import javax.crypto.Cipher
|
|
937
|
+
`;
|
|
938
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/kotlin/App.kt']);
|
|
939
|
+
mockReadFile.mockResolvedValue(ktContent);
|
|
940
|
+
mockPathExists.mockResolvedValue(false);
|
|
941
|
+
mockPathExistsSync.mockReturnValue(false);
|
|
942
|
+
const failures = await gate.run(context);
|
|
943
|
+
expect(failures).toHaveLength(0);
|
|
944
|
+
});
|
|
945
|
+
it('should flag unknown Kotlin imports when Gradle context exists', async () => {
|
|
946
|
+
const ktContent = `
|
|
947
|
+
import kotlin.collections.mutableListOf
|
|
948
|
+
import com.hallucinated.fake.Module
|
|
949
|
+
`;
|
|
950
|
+
const buildGradle = `
|
|
951
|
+
dependencies {
|
|
952
|
+
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0'
|
|
953
|
+
}
|
|
954
|
+
`;
|
|
955
|
+
FileScanner.findFiles.mockResolvedValue(['src/main/kotlin/App.kt']);
|
|
956
|
+
mockReadFile.mockResolvedValue(ktContent);
|
|
957
|
+
mockPathExists.mockResolvedValue(false);
|
|
958
|
+
mockPathExistsSync.mockImplementation((p) => p.includes('build.gradle') || p.includes('build.gradle.kts'));
|
|
959
|
+
mockReadFileSync.mockReturnValue(buildGradle);
|
|
960
|
+
const failures = await gate.run(context);
|
|
961
|
+
expect(failures).toHaveLength(1);
|
|
962
|
+
expect(failures[0].details).toContain('com.hallucinated.fake.Module');
|
|
963
|
+
});
|
|
288
964
|
});
|