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