@rigour-labs/core 3.0.1 → 3.0.3

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,964 @@
1
+ /**
2
+ * Hallucinated Imports Gate — Comprehensive Regression Tests
3
+ *
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
+ *
9
+ * @since v3.0.1
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import { HallucinatedImportsGate } from './hallucinated-imports.js';
13
+ // Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
14
+ const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson, mockReaddirSync } = vi.hoisted(() => ({
15
+ mockPathExists: vi.fn(),
16
+ mockPathExistsSync: vi.fn(),
17
+ mockReadFile: vi.fn(),
18
+ mockReadFileSync: vi.fn(),
19
+ mockReadJson: vi.fn(),
20
+ mockReaddirSync: vi.fn().mockReturnValue([]),
21
+ }));
22
+ vi.mock('fs-extra', () => {
23
+ const mock = {
24
+ pathExists: mockPathExists,
25
+ pathExistsSync: mockPathExistsSync,
26
+ readFile: mockReadFile,
27
+ readFileSync: mockReadFileSync,
28
+ readJson: mockReadJson,
29
+ readdirSync: mockReaddirSync,
30
+ };
31
+ return {
32
+ ...mock,
33
+ default: mock,
34
+ };
35
+ });
36
+ // Mock FileScanner
37
+ vi.mock('../utils/scanner.js', () => ({
38
+ FileScanner: {
39
+ findFiles: vi.fn().mockResolvedValue([]),
40
+ },
41
+ }));
42
+ import { FileScanner } from '../utils/scanner.js';
43
+ // ═══════════════════════════════════════════════════════════════
44
+ // GO
45
+ // ═══════════════════════════════════════════════════════════════
46
+ describe('HallucinatedImportsGate — Go stdlib false positives', () => {
47
+ let gate;
48
+ const testCwd = '/tmp/test-go-project';
49
+ const context = { cwd: testCwd, ignore: [] };
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ mockReaddirSync.mockReturnValue([]);
53
+ gate = new HallucinatedImportsGate({ enabled: true });
54
+ });
55
+ it('should NOT flag Go standard library packages as hallucinated (PicoClaw regression)', async () => {
56
+ const goFileContent = `package main
57
+
58
+ import (
59
+ "encoding/json"
60
+ "path/filepath"
61
+ "net/http"
62
+ "crypto/rand"
63
+ "crypto/sha256"
64
+ "encoding/base64"
65
+ "os/exec"
66
+ "os/signal"
67
+ "net/url"
68
+ "fmt"
69
+ "io"
70
+ "os"
71
+ "strings"
72
+ "context"
73
+ "sync"
74
+ "time"
75
+ "log"
76
+ "errors"
77
+ "io/ioutil"
78
+ "io/fs"
79
+ "math/rand"
80
+ "regexp"
81
+ "strconv"
82
+ "bytes"
83
+ "bufio"
84
+ "sort"
85
+ "testing"
86
+ "net/http/httptest"
87
+ "database/sql"
88
+ "html/template"
89
+ "text/template"
90
+ "archive/zip"
91
+ "compress/gzip"
92
+ "runtime/debug"
93
+ )
94
+
95
+ func main() {}
96
+ `;
97
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
98
+ mockReadFile.mockResolvedValue(goFileContent);
99
+ mockPathExists.mockResolvedValue(false);
100
+ mockPathExistsSync.mockReturnValue(false);
101
+ const failures = await gate.run(context);
102
+ expect(failures).toHaveLength(0);
103
+ });
104
+ it('should NOT flag external module imports (github.com, etc.)', async () => {
105
+ const goFileContent = `package main
106
+
107
+ import (
108
+ "github.com/gin-gonic/gin"
109
+ "github.com/stretchr/testify/assert"
110
+ "google.golang.org/grpc"
111
+ "go.uber.org/zap"
112
+ "golang.org/x/crypto/bcrypt"
113
+ )
114
+
115
+ func main() {}
116
+ `;
117
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
118
+ mockReadFile.mockResolvedValue(goFileContent);
119
+ mockPathExists.mockResolvedValue(false);
120
+ mockPathExistsSync.mockReturnValue(false);
121
+ const failures = await gate.run(context);
122
+ expect(failures).toHaveLength(0);
123
+ });
124
+ it('should flag project-relative imports that do not resolve (with go.mod)', async () => {
125
+ const goMod = `module github.com/myorg/myproject
126
+
127
+ go 1.22
128
+ `;
129
+ const goFileContent = `package main
130
+
131
+ import (
132
+ "fmt"
133
+ "github.com/myorg/myproject/pkg/realmodule"
134
+ "github.com/myorg/myproject/pkg/doesnotexist"
135
+ )
136
+
137
+ func main() {}
138
+ `;
139
+ FileScanner.findFiles.mockResolvedValue(['cmd/main.go', 'pkg/realmodule/handler.go']);
140
+ mockReadFile.mockImplementation(async (filePath) => {
141
+ if (filePath.includes('handler.go'))
142
+ return 'package realmodule\n\nimport "fmt"\n\nfunc Handler() {}\n';
143
+ return goFileContent;
144
+ });
145
+ mockPathExists.mockResolvedValue(false);
146
+ mockPathExistsSync.mockReturnValue(true);
147
+ mockReadFileSync.mockReturnValue(goMod);
148
+ const failures = await gate.run(context);
149
+ expect(failures).toHaveLength(1);
150
+ expect(failures[0].details).toContain('doesnotexist');
151
+ expect(failures[0].details).not.toContain('realmodule');
152
+ });
153
+ it('should NOT flag anything when no go.mod exists and imports have no dots', async () => {
154
+ const goFileContent = `package main
155
+
156
+ import (
157
+ "fmt"
158
+ "net/http"
159
+ "internal/custom"
160
+ )
161
+
162
+ func main() {}
163
+ `;
164
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
165
+ mockReadFile.mockResolvedValue(goFileContent);
166
+ mockPathExists.mockResolvedValue(false);
167
+ mockPathExistsSync.mockReturnValue(false);
168
+ const failures = await gate.run(context);
169
+ expect(failures).toHaveLength(0);
170
+ });
171
+ it('should handle single-line imports', async () => {
172
+ const goFileContent = `package main
173
+
174
+ import "fmt"
175
+ import "encoding/json"
176
+ import "net/http"
177
+
178
+ func main() {}
179
+ `;
180
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
181
+ mockReadFile.mockResolvedValue(goFileContent);
182
+ mockPathExists.mockResolvedValue(false);
183
+ mockPathExistsSync.mockReturnValue(false);
184
+ const failures = await gate.run(context);
185
+ expect(failures).toHaveLength(0);
186
+ });
187
+ it('should handle aliased imports', async () => {
188
+ const goFileContent = `package main
189
+
190
+ import (
191
+ "fmt"
192
+ mrand "math/rand"
193
+ _ "net/http/pprof"
194
+ . "os"
195
+ )
196
+
197
+ func main() {}
198
+ `;
199
+ FileScanner.findFiles.mockResolvedValue(['main.go']);
200
+ mockReadFile.mockResolvedValue(goFileContent);
201
+ mockPathExists.mockResolvedValue(false);
202
+ mockPathExistsSync.mockReturnValue(false);
203
+ const failures = await gate.run(context);
204
+ expect(failures).toHaveLength(0);
205
+ });
206
+ });
207
+ // ═══════════════════════════════════════════════════════════════
208
+ // PYTHON
209
+ // ═══════════════════════════════════════════════════════════════
210
+ describe('HallucinatedImportsGate — Python stdlib coverage', () => {
211
+ let gate;
212
+ const testCwd = '/tmp/test-py-project';
213
+ const context = { cwd: testCwd, ignore: [] };
214
+ beforeEach(() => {
215
+ vi.clearAllMocks();
216
+ mockReaddirSync.mockReturnValue([]);
217
+ gate = new HallucinatedImportsGate({ enabled: true });
218
+ });
219
+ it('should NOT flag Python standard library imports', async () => {
220
+ const pyContent = `
221
+ import os
222
+ import sys
223
+ import json
224
+ import hashlib
225
+ import pathlib
226
+ import subprocess
227
+ import argparse
228
+ import typing
229
+ import dataclasses
230
+ import functools
231
+ import itertools
232
+ import collections
233
+ import datetime
234
+ import re
235
+ import math
236
+ import random
237
+ import threading
238
+ import asyncio
239
+ from os.path import join, exists
240
+ from collections import defaultdict
241
+ from typing import List, Optional
242
+ from urllib.parse import urlparse
243
+ `;
244
+ FileScanner.findFiles.mockResolvedValue(['main.py']);
245
+ mockReadFile.mockResolvedValue(pyContent);
246
+ mockPathExists.mockResolvedValue(false);
247
+ const failures = await gate.run(context);
248
+ expect(failures).toHaveLength(0);
249
+ });
250
+ });
251
+ // ═══════════════════════════════════════════════════════════════
252
+ // JS/TS (Node.js)
253
+ // ═══════════════════════════════════════════════════════════════
254
+ describe('HallucinatedImportsGate — JS/TS Node builtins', () => {
255
+ let gate;
256
+ const testCwd = '/tmp/test-node-project';
257
+ const context = { cwd: testCwd, ignore: [] };
258
+ beforeEach(() => {
259
+ vi.clearAllMocks();
260
+ mockReaddirSync.mockReturnValue([]);
261
+ gate = new HallucinatedImportsGate({ enabled: true });
262
+ });
263
+ it('should NOT flag Node.js built-in modules', async () => {
264
+ const jsContent = `
265
+ import fs from 'fs';
266
+ import path from 'path';
267
+ import crypto from 'crypto';
268
+ import http from 'http';
269
+ import https from 'https';
270
+ import url from 'url';
271
+ import os from 'os';
272
+ import stream from 'stream';
273
+ import util from 'util';
274
+ import { readFile } from 'node:fs';
275
+ import { join } from 'node:path';
276
+ `;
277
+ FileScanner.findFiles.mockResolvedValue(['index.ts']);
278
+ mockReadFile.mockResolvedValue(jsContent);
279
+ mockPathExists.mockResolvedValue(false);
280
+ const failures = await gate.run(context);
281
+ expect(failures).toHaveLength(0);
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
+ });
964
+ });