@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.
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Hallucinated Imports Gate — Go Standard Library False Positive Regression Tests
2
+ * Hallucinated Imports Gate — Comprehensive Regression Tests
3
3
  *
4
- * Tests the fix for https://github.com/rigour-labs/rigour/issues/XXX
5
- * Previously, Go stdlib packages with slashes (encoding/json, net/http, etc.)
6
- * were flagged as hallucinated imports because the gate only recognized
7
- * single-word stdlib packages (fmt, os, io).
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
- const goFile = 'main.go';
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); // no go.mod
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); // go.mod exists
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
  });