openmatrix 0.2.19 → 0.2.20

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,778 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectTestFrameworks = detectTestFrameworks;
37
+ exports.detectProjectType = detectProjectType;
38
+ exports.scanTestFiles = scanTestFiles;
39
+ exports.scanSourceFiles = scanSourceFiles;
40
+ exports.scanDirectory = scanDirectory;
41
+ exports.inferSourceFile = inferSourceFile;
42
+ exports.inferFileType = inferFileType;
43
+ exports.extractExports = extractExports;
44
+ exports.hasCorrespondingTest = hasCorrespondingTest;
45
+ exports.inferTestTypes = inferTestTypes;
46
+ exports.detectTestStyle = detectTestStyle;
47
+ exports.detectFrontend = detectFrontend;
48
+ exports.detectCoverageReport = detectCoverageReport;
49
+ exports.performFullScan = performFullScan;
50
+ // src/test/context-analyzer.ts
51
+ /**
52
+ * 测试上下文分析器 - 项目代码扫描和框架检测
53
+ *
54
+ * 提供 CLI 和测试生成器共用的分析功能
55
+ */
56
+ const fs = __importStar(require("fs"));
57
+ const path = __importStar(require("path"));
58
+ /**
59
+ * 检测测试框架
60
+ */
61
+ function detectTestFrameworks(projectRoot) {
62
+ const frameworks = [];
63
+ // 检测 package.json (Node.js/TypeScript)
64
+ const packageJsonPath = path.join(projectRoot, 'package.json');
65
+ if (fs.existsSync(packageJsonPath)) {
66
+ try {
67
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
68
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
69
+ detectNodeTestFrameworks(projectRoot, deps, frameworks);
70
+ }
71
+ catch {
72
+ // 忽略 JSON 解析错误
73
+ }
74
+ }
75
+ // 检测 Python 测试框架
76
+ detectPythonTestFrameworks(projectRoot, frameworks);
77
+ // 检测 Go 测试
78
+ const goModPath = path.join(projectRoot, 'go.mod');
79
+ if (fs.existsSync(goModPath)) {
80
+ frameworks.push({
81
+ framework: 'gotest',
82
+ isPrimary: frameworks.length === 0,
83
+ supportedTypes: ['unit', 'integration'],
84
+ commands: {
85
+ test: 'go test ./...',
86
+ testFile: 'go test <file>',
87
+ testCoverage: 'go test -cover ./...'
88
+ }
89
+ });
90
+ }
91
+ // 检测 Rust 测试
92
+ const cargoPath = path.join(projectRoot, 'Cargo.toml');
93
+ if (fs.existsSync(cargoPath)) {
94
+ frameworks.push({
95
+ framework: 'cargo-test',
96
+ isPrimary: frameworks.length === 0,
97
+ supportedTypes: ['unit', 'integration'],
98
+ commands: {
99
+ test: 'cargo test',
100
+ testCoverage: 'cargo tarpaulin'
101
+ }
102
+ });
103
+ }
104
+ // 检测 Java 测试框架
105
+ detectJavaTestFrameworks(projectRoot, frameworks);
106
+ // 如果没有检测到框架,标记为 unknown
107
+ if (frameworks.length === 0) {
108
+ frameworks.push({
109
+ framework: 'unknown',
110
+ isPrimary: true,
111
+ supportedTypes: [],
112
+ commands: { test: '' }
113
+ });
114
+ }
115
+ return frameworks;
116
+ }
117
+ /**
118
+ * 检测 Node.js 测试框架
119
+ */
120
+ function detectNodeTestFrameworks(projectRoot, deps, frameworks) {
121
+ // Vitest
122
+ if (deps.vitest) {
123
+ const configFiles = ['vitest.config.ts', 'vitest.config.js', 'vite.config.ts', 'vite.config.js'];
124
+ const configFile = configFiles.find(f => fs.existsSync(path.join(projectRoot, f)));
125
+ frameworks.push({
126
+ framework: 'vitest',
127
+ version: deps.vitest,
128
+ configFile,
129
+ isPrimary: true,
130
+ supportedTypes: ['unit', 'integration'],
131
+ commands: {
132
+ test: 'vitest run',
133
+ testFile: 'vitest run <file>',
134
+ testCoverage: 'vitest run --coverage',
135
+ watch: 'vitest watch',
136
+ updateSnapshot: 'vitest run --update'
137
+ }
138
+ });
139
+ }
140
+ // Jest
141
+ if (deps.jest || deps['@types/jest']) {
142
+ const configFiles = ['jest.config.ts', 'jest.config.js', 'jest.config.json'];
143
+ const configFile = configFiles.find(f => fs.existsSync(path.join(projectRoot, f)));
144
+ frameworks.push({
145
+ framework: 'jest',
146
+ version: deps.jest || deps['@types/jest'],
147
+ configFile,
148
+ isPrimary: !frameworks.some(f => f.framework === 'vitest'),
149
+ supportedTypes: ['unit', 'integration'],
150
+ commands: {
151
+ test: 'jest',
152
+ testFile: 'jest <file>',
153
+ testCoverage: 'jest --coverage',
154
+ watch: 'jest --watch',
155
+ updateSnapshot: 'jest --updateSnapshot'
156
+ }
157
+ });
158
+ }
159
+ // Mocha
160
+ if (deps.mocha) {
161
+ const configFiles = ['.mocharc.json', '.mocharc.js', '.mocharc.yml'];
162
+ const configFile = configFiles.find(f => fs.existsSync(path.join(projectRoot, f)));
163
+ frameworks.push({
164
+ framework: 'mocha',
165
+ version: deps.mocha,
166
+ configFile,
167
+ isPrimary: !frameworks.some(f => f.isPrimary),
168
+ supportedTypes: ['unit', 'integration'],
169
+ commands: {
170
+ test: 'mocha',
171
+ testFile: 'mocha <file>',
172
+ testCoverage: 'nyc mocha'
173
+ }
174
+ });
175
+ }
176
+ // Playwright (E2E)
177
+ if (deps['@playwright/test']) {
178
+ const configFile = fs.existsSync(path.join(projectRoot, 'playwright.config.ts'))
179
+ ? 'playwright.config.ts'
180
+ : fs.existsSync(path.join(projectRoot, 'playwright.config.js'))
181
+ ? 'playwright.config.js'
182
+ : undefined;
183
+ frameworks.push({
184
+ framework: 'playwright',
185
+ version: deps['@playwright/test'],
186
+ configFile,
187
+ isPrimary: false,
188
+ supportedTypes: ['e2e', 'ui', 'visual', 'accessibility'],
189
+ commands: {
190
+ test: 'playwright test',
191
+ testFile: 'playwright test <file>',
192
+ watch: 'playwright test --ui'
193
+ }
194
+ });
195
+ }
196
+ // Cypress (E2E)
197
+ if (deps.cypress) {
198
+ const configFile = fs.existsSync(path.join(projectRoot, 'cypress.config.ts'))
199
+ ? 'cypress.config.ts'
200
+ : fs.existsSync(path.join(projectRoot, 'cypress.config.js'))
201
+ ? 'cypress.config.js'
202
+ : undefined;
203
+ frameworks.push({
204
+ framework: 'cypress',
205
+ version: deps.cypress,
206
+ configFile,
207
+ isPrimary: false,
208
+ supportedTypes: ['e2e', 'ui', 'integration'],
209
+ commands: {
210
+ test: 'cypress run',
211
+ testFile: 'cypress run --spec <file>',
212
+ watch: 'cypress open'
213
+ }
214
+ });
215
+ }
216
+ }
217
+ /**
218
+ * 检测 Python 测试框架
219
+ */
220
+ function detectPythonTestFrameworks(projectRoot, frameworks) {
221
+ const requirementsPath = path.join(projectRoot, 'requirements.txt');
222
+ if (fs.existsSync(requirementsPath)) {
223
+ const requirements = fs.readFileSync(requirementsPath, 'utf-8');
224
+ if (requirements.includes('pytest')) {
225
+ frameworks.push({
226
+ framework: 'pytest',
227
+ isPrimary: frameworks.length === 0,
228
+ supportedTypes: ['unit', 'integration', 'api'],
229
+ commands: {
230
+ test: 'pytest',
231
+ testFile: 'pytest <file>',
232
+ testCoverage: 'pytest --cov'
233
+ }
234
+ });
235
+ }
236
+ if (requirements.includes('unittest')) {
237
+ frameworks.push({
238
+ framework: 'unittest',
239
+ isPrimary: frameworks.length === 0 && !requirements.includes('pytest'),
240
+ supportedTypes: ['unit', 'integration'],
241
+ commands: {
242
+ test: 'python -m unittest discover',
243
+ testFile: 'python -m unittest <file>'
244
+ }
245
+ });
246
+ }
247
+ }
248
+ // 检测 pyproject.toml (Python)
249
+ const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
250
+ if (fs.existsSync(pyprojectPath)) {
251
+ const pyproject = fs.readFileSync(pyprojectPath, 'utf-8');
252
+ if (pyproject.includes('pytest')) {
253
+ frameworks.push({
254
+ framework: 'pytest',
255
+ isPrimary: !frameworks.some(f => f.framework === 'pytest'),
256
+ supportedTypes: ['unit', 'integration', 'api'],
257
+ commands: {
258
+ test: 'pytest',
259
+ testFile: 'pytest <file>',
260
+ testCoverage: 'pytest --cov'
261
+ }
262
+ });
263
+ }
264
+ }
265
+ }
266
+ /**
267
+ * 检测 Java 测试框架
268
+ */
269
+ function detectJavaTestFrameworks(projectRoot, frameworks) {
270
+ const pomPath = path.join(projectRoot, 'pom.xml');
271
+ if (fs.existsSync(pomPath)) {
272
+ const pom = fs.readFileSync(pomPath, 'utf-8');
273
+ if (pom.includes('junit')) {
274
+ frameworks.push({
275
+ framework: 'junit',
276
+ isPrimary: frameworks.length === 0,
277
+ supportedTypes: ['unit', 'integration'],
278
+ commands: {
279
+ test: 'mvn test',
280
+ testFile: 'mvn test -Dtest=<class>'
281
+ }
282
+ });
283
+ }
284
+ if (pom.includes('testng')) {
285
+ frameworks.push({
286
+ framework: 'testng',
287
+ isPrimary: frameworks.length === 0 && !pom.includes('junit'),
288
+ supportedTypes: ['unit', 'integration'],
289
+ commands: {
290
+ test: 'mvn test',
291
+ testFile: 'mvn test -Dtest=<class>'
292
+ }
293
+ });
294
+ }
295
+ }
296
+ // 检测 Gradle (Java/Kotlin)
297
+ const gradlePath = path.join(projectRoot, 'build.gradle');
298
+ const gradleKtsPath = path.join(projectRoot, 'build.gradle.kts');
299
+ if (fs.existsSync(gradlePath) || fs.existsSync(gradleKtsPath)) {
300
+ const gradleFile = fs.existsSync(gradlePath) ? gradlePath : gradleKtsPath;
301
+ const gradle = fs.readFileSync(gradleFile, 'utf-8');
302
+ if (gradle.includes('junit') || gradle.includes('test')) {
303
+ frameworks.push({
304
+ framework: 'junit',
305
+ isPrimary: frameworks.length === 0,
306
+ supportedTypes: ['unit', 'integration'],
307
+ commands: {
308
+ test: 'gradle test',
309
+ testFile: 'gradle test --tests <class>'
310
+ }
311
+ });
312
+ }
313
+ }
314
+ }
315
+ /**
316
+ * 检测项目类型
317
+ */
318
+ function detectProjectType(projectRoot) {
319
+ const packageJsonPath = path.join(projectRoot, 'package.json');
320
+ // Node.js/TypeScript 项目
321
+ if (fs.existsSync(packageJsonPath)) {
322
+ try {
323
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
324
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
325
+ // 前端框架检测
326
+ if (deps.react || deps['react-dom'])
327
+ return 'react';
328
+ if (deps.vue)
329
+ return 'vue';
330
+ if (deps['@angular/core'])
331
+ return 'angular';
332
+ if (deps.next)
333
+ return 'nextjs';
334
+ if (deps.nuxt)
335
+ return 'nuxt';
336
+ if (deps.svelte)
337
+ return 'svelte';
338
+ // TypeScript 检测
339
+ if (deps.typescript || fs.existsSync(path.join(projectRoot, 'tsconfig.json'))) {
340
+ return 'typescript';
341
+ }
342
+ return 'nodejs';
343
+ }
344
+ catch {
345
+ return 'nodejs';
346
+ }
347
+ }
348
+ // Python 项目
349
+ if (fs.existsSync(path.join(projectRoot, 'requirements.txt')) ||
350
+ fs.existsSync(path.join(projectRoot, 'pyproject.toml')) ||
351
+ fs.existsSync(path.join(projectRoot, 'setup.py'))) {
352
+ return 'python';
353
+ }
354
+ // Go 项目
355
+ if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
356
+ return 'go';
357
+ }
358
+ // Rust 项目
359
+ if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
360
+ return 'rust';
361
+ }
362
+ // Java 项目
363
+ if (fs.existsSync(path.join(projectRoot, 'pom.xml')) ||
364
+ fs.existsSync(path.join(projectRoot, 'build.gradle')) ||
365
+ fs.existsSync(path.join(projectRoot, 'build.gradle.kts'))) {
366
+ return 'java';
367
+ }
368
+ // Flutter 项目
369
+ if (fs.existsSync(path.join(projectRoot, 'pubspec.yaml'))) {
370
+ const pubspec = fs.readFileSync(path.join(projectRoot, 'pubspec.yaml'), 'utf-8');
371
+ if (pubspec.includes('flutter'))
372
+ return 'flutter';
373
+ return 'dart';
374
+ }
375
+ return 'unknown';
376
+ }
377
+ /**
378
+ * 扫描测试文件
379
+ */
380
+ function scanTestFiles(projectRoot, testDirs) {
381
+ const testFiles = [];
382
+ const testPatterns = [
383
+ /\.test\.(ts|tsx|js|jsx)$/,
384
+ /\.spec\.(ts|tsx|js|jsx)$/,
385
+ /_test\.(py)$/,
386
+ /test_.*\.py$/,
387
+ /_test\.go$/,
388
+ /Test.*\.java$/,
389
+ /.*Test\.java$/,
390
+ /.*Tests\.cs$/
391
+ ];
392
+ for (const testDir of testDirs) {
393
+ const fullPath = path.join(projectRoot, testDir);
394
+ if (!fs.existsSync(fullPath))
395
+ continue;
396
+ scanDirectory(fullPath, (filePath) => {
397
+ const relativePath = path.relative(projectRoot, filePath);
398
+ const fileName = path.basename(filePath);
399
+ // 检查是否匹配测试文件模式
400
+ const isTestFile = testPatterns.some(p => p.test(fileName));
401
+ if (!isTestFile)
402
+ return;
403
+ // 推断测试类型
404
+ let testType = 'unit';
405
+ if (relativePath.includes('e2e') || relativePath.includes('end-to-end')) {
406
+ testType = 'e2e';
407
+ }
408
+ else if (relativePath.includes('integration')) {
409
+ testType = 'integration';
410
+ }
411
+ else if (relativePath.includes('api')) {
412
+ testType = 'api';
413
+ }
414
+ else if (relativePath.includes('ui') || relativePath.includes('component')) {
415
+ testType = 'ui';
416
+ }
417
+ // 尝试推断关联的源文件
418
+ const sourceFile = inferSourceFile(filePath, projectRoot);
419
+ testFiles.push({
420
+ path: relativePath,
421
+ type: testType,
422
+ sourceFile,
423
+ lastModified: fs.statSync(filePath).mtime.toISOString()
424
+ });
425
+ });
426
+ }
427
+ return testFiles;
428
+ }
429
+ /**
430
+ * 扫描源文件
431
+ */
432
+ function scanSourceFiles(projectRoot, sourceDirs, existingTests) {
433
+ const sourceFiles = [];
434
+ const sourcePatterns = [
435
+ /\.(ts|tsx)$/,
436
+ /\.(js|jsx)$/,
437
+ /\.py$/,
438
+ /\.go$/,
439
+ /\.java$/,
440
+ /\.rs$/,
441
+ /\.cs$/
442
+ ];
443
+ // 排除测试文件
444
+ const excludePatterns = [
445
+ /\.test\.(ts|tsx|js|jsx)$/,
446
+ /\.spec\.(ts|tsx|js|jsx)$/,
447
+ /_test\.(py)$/,
448
+ /test_.*\.py$/,
449
+ /_test\.go$/,
450
+ /Test.*\.java$/,
451
+ /__tests__/,
452
+ /tests?\//
453
+ ];
454
+ // 已有测试覆盖的源文件集合
455
+ const coveredSources = new Set();
456
+ for (const test of existingTests) {
457
+ if (test.sourceFile) {
458
+ coveredSources.add(test.sourceFile);
459
+ }
460
+ }
461
+ for (const sourceDir of sourceDirs) {
462
+ const fullPath = path.join(projectRoot, sourceDir);
463
+ if (!fs.existsSync(fullPath))
464
+ continue;
465
+ scanDirectory(fullPath, (filePath) => {
466
+ const relativePath = path.relative(projectRoot, filePath);
467
+ const fileName = path.basename(filePath);
468
+ // 排除测试文件
469
+ if (excludePatterns.some(p => p.test(relativePath) || p.test(fileName)))
470
+ return;
471
+ // 检查是否匹配源文件模式
472
+ const isSourceFile = sourcePatterns.some(p => p.test(fileName));
473
+ if (!isSourceFile)
474
+ return;
475
+ // 排除类型声明文件
476
+ if (fileName.endsWith('.d.ts'))
477
+ return;
478
+ // 推断文件类型
479
+ const fileType = inferFileType(relativePath);
480
+ // 提取导出
481
+ const exports = extractExports(filePath);
482
+ // 检查是否有对应的测试文件
483
+ const hasTest = coveredSources.has(relativePath) || hasCorrespondingTest(relativePath, existingTests);
484
+ // 推断建议的测试类型
485
+ const suggestedTestTypes = inferTestTypes(fileType, relativePath);
486
+ sourceFiles.push({
487
+ path: relativePath,
488
+ fileType,
489
+ exports,
490
+ hasTest,
491
+ suggestedTestTypes
492
+ });
493
+ });
494
+ }
495
+ return sourceFiles.filter(f => !f.hasTest);
496
+ }
497
+ /**
498
+ * 递归扫描目录
499
+ */
500
+ function scanDirectory(dir, callback) {
501
+ const excludeDirs = ['node_modules', 'dist', 'build', '.git', '__pycache__', 'vendor', 'target'];
502
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
503
+ for (const entry of entries) {
504
+ const fullPath = path.join(dir, entry.name);
505
+ if (entry.isDirectory()) {
506
+ // 排除特定目录
507
+ if (excludeDirs.includes(entry.name))
508
+ continue;
509
+ scanDirectory(fullPath, callback);
510
+ }
511
+ else if (entry.isFile()) {
512
+ callback(fullPath);
513
+ }
514
+ }
515
+ }
516
+ /**
517
+ * 推断关联的源文件
518
+ */
519
+ function inferSourceFile(testPath, projectRoot) {
520
+ const testFileName = path.basename(testPath);
521
+ const testDir = path.dirname(testPath);
522
+ // 移除测试后缀
523
+ let sourceName = testFileName
524
+ .replace(/\.test\.(ts|tsx|js|jsx)$/, '.$1')
525
+ .replace(/\.spec\.(ts|tsx|js|jsx)$/, '$1')
526
+ .replace(/_test\.py$/, '.py')
527
+ .replace(/test_(.*)\.py$/, '$1.py')
528
+ .replace(/_test\.go$/, '.go');
529
+ if (sourceName === testFileName)
530
+ return undefined;
531
+ // 在同目录或父目录寻找源文件
532
+ const possibleDirs = [
533
+ testDir,
534
+ path.join(testDir, '..'),
535
+ path.join(projectRoot, 'src'),
536
+ path.join(projectRoot, 'lib')
537
+ ];
538
+ for (const dir of possibleDirs) {
539
+ const sourcePath = path.join(dir, sourceName);
540
+ if (fs.existsSync(sourcePath)) {
541
+ return path.relative(projectRoot, sourcePath);
542
+ }
543
+ }
544
+ return undefined;
545
+ }
546
+ /**
547
+ * 推断文件类型
548
+ */
549
+ function inferFileType(filePath) {
550
+ const fileName = path.basename(filePath);
551
+ const dirPath = path.dirname(filePath);
552
+ if (dirPath.includes('component') || fileName.includes('.tsx') || fileName.includes('.jsx')) {
553
+ return 'component';
554
+ }
555
+ if (dirPath.includes('service') || fileName.includes('service')) {
556
+ return 'service';
557
+ }
558
+ if (dirPath.includes('util') || dirPath.includes('utils') || fileName.includes('util')) {
559
+ return 'util';
560
+ }
561
+ if (fileName.includes('.class.') || fileName.match(/[A-Z][a-z]+\.ts$/)) {
562
+ return 'class';
563
+ }
564
+ if (dirPath.includes('api') || dirPath.includes('route')) {
565
+ return 'module';
566
+ }
567
+ return 'module';
568
+ }
569
+ /**
570
+ * 提取导出(简化版)
571
+ */
572
+ function extractExports(filePath) {
573
+ try {
574
+ const content = fs.readFileSync(filePath, 'utf-8');
575
+ const exports = [];
576
+ // 提取 export function/class/const
577
+ const exportPatterns = [
578
+ /export\s+function\s+(\w+)/g,
579
+ /export\s+class\s+(\w+)/g,
580
+ /export\s+const\s+(\w+)/g,
581
+ /export\s+async\s+function\s+(\w+)/g,
582
+ /export\s+\{[^}]*\}/g
583
+ ];
584
+ for (const pattern of exportPatterns) {
585
+ let match;
586
+ while ((match = pattern.exec(content)) !== null) {
587
+ if (match[1]) {
588
+ exports.push(match[1]);
589
+ }
590
+ else {
591
+ // 解析 export { ... }
592
+ const items = match[0].match(/\w+/g);
593
+ if (items) {
594
+ exports.push(...items.filter(i => i !== 'export'));
595
+ }
596
+ }
597
+ }
598
+ }
599
+ // Python def/class
600
+ if (filePath.endsWith('.py')) {
601
+ const pyPatterns = [
602
+ /^def\s+(\w+)/gm,
603
+ /^class\s+(\w+)/gm
604
+ ];
605
+ for (const pattern of pyPatterns) {
606
+ let match;
607
+ while ((match = pattern.exec(content)) !== null) {
608
+ if (match[1] && !match[1].startsWith('_')) {
609
+ exports.push(match[1]);
610
+ }
611
+ }
612
+ }
613
+ }
614
+ return exports.slice(0, 10);
615
+ }
616
+ catch {
617
+ return [];
618
+ }
619
+ }
620
+ /**
621
+ * 检查是否有对应的测试文件
622
+ */
623
+ function hasCorrespondingTest(sourcePath, existingTests) {
624
+ const sourceFileName = path.basename(sourcePath);
625
+ // 生成可能的测试文件名
626
+ const possibleTestNames = [
627
+ sourceFileName.replace(/\.(ts|tsx|js|jsx)$/, '.test.$1'),
628
+ sourceFileName.replace(/\.(ts|tsx|js|jsx)$/, '.spec.$1'),
629
+ sourceFileName.replace(/\.py$/, '_test.py'),
630
+ sourceFileName.replace(/\.py$/, 'test_' + sourceFileName.replace('.py', '') + '.py'),
631
+ sourceFileName.replace(/\.go$/, '_test.go')
632
+ ];
633
+ return existingTests.some(t => possibleTestNames.includes(path.basename(t.path)));
634
+ }
635
+ /**
636
+ * 推断建议的测试类型
637
+ */
638
+ function inferTestTypes(fileType, filePath) {
639
+ const types = ['unit'];
640
+ if (fileType === 'component') {
641
+ types.push('ui');
642
+ }
643
+ if (fileType === 'service') {
644
+ types.push('integration');
645
+ }
646
+ if (filePath.includes('api') || filePath.includes('route')) {
647
+ types.push('api');
648
+ }
649
+ return types;
650
+ }
651
+ /**
652
+ * 检测测试风格
653
+ */
654
+ function detectTestStyle(projectRoot, existingTests) {
655
+ if (existingTests.length === 0)
656
+ return undefined;
657
+ // 读取第一个测试文件分析风格
658
+ const sampleTest = existingTests[0];
659
+ const testPath = path.join(projectRoot, sampleTest.path);
660
+ try {
661
+ const content = fs.readFileSync(testPath, 'utf-8');
662
+ // 命名约定
663
+ const hasDescribeIt = content.includes('describe(') && content.includes('it(');
664
+ const hasTest = content.includes('test(');
665
+ const namingConvention = hasDescribeIt && hasTest ? 'mixed' :
666
+ hasDescribeIt ? 'describe-it' : 'test';
667
+ let assertionLibrary = 'expect';
668
+ if (content.includes('assert('))
669
+ assertionLibrary = 'assert';
670
+ if (content.includes('should('))
671
+ assertionLibrary = 'should';
672
+ if (content.includes('chai'))
673
+ assertionLibrary = 'chai';
674
+ // TypeScript/JSX
675
+ const usesTypeScript = testPath.endsWith('.ts') || testPath.endsWith('.tsx');
676
+ const usesJSX = testPath.endsWith('.tsx') || testPath.endsWith('.jsx');
677
+ // 文件后缀
678
+ const fileSuffix = path.extname(testPath);
679
+ // 文件位置
680
+ const testDir = path.dirname(sampleTest.path);
681
+ const sourceDir = sampleTest.sourceFile ? path.dirname(sampleTest.sourceFile) : '';
682
+ const fileLocation = testDir === sourceDir || testDir === '__tests__' ? 'adjacent' : 'separate';
683
+ return {
684
+ namingConvention,
685
+ assertionLibrary,
686
+ usesTypeScript,
687
+ usesJSX,
688
+ fileSuffix,
689
+ fileLocation
690
+ };
691
+ }
692
+ catch {
693
+ return undefined;
694
+ }
695
+ }
696
+ /**
697
+ * 检测是否有前端项目
698
+ */
699
+ function detectFrontend(projectRoot, projectType) {
700
+ const frontendTypes = ['react', 'vue', 'angular', 'nextjs', 'nuxt', 'svelte', 'flutter'];
701
+ const isFrontend = frontendTypes.includes(projectType);
702
+ // 检查是否有 UI 组件目录
703
+ const componentDirs = ['components', 'src/components', 'pages', 'src/pages', 'views', 'src/views'];
704
+ const hasUIComponents = componentDirs.some(d => fs.existsSync(path.join(projectRoot, d)));
705
+ return { isFrontend, hasUIComponents };
706
+ }
707
+ /**
708
+ * 检测覆盖率报告
709
+ */
710
+ function detectCoverageReport(projectRoot) {
711
+ const coveragePaths = [
712
+ 'coverage/coverage-summary.json',
713
+ 'coverage/lcov-report/lcov.info',
714
+ '.nyc_output/out.json',
715
+ 'coverage.json'
716
+ ];
717
+ for (const coveragePath of coveragePaths) {
718
+ const fullPath = path.join(projectRoot, coveragePath);
719
+ if (fs.existsSync(fullPath)) {
720
+ try {
721
+ const content = fs.readFileSync(fullPath, 'utf-8');
722
+ const data = JSON.parse(content);
723
+ if (data.total) {
724
+ return {
725
+ total: data.total.lines?.pct || data.total,
726
+ files: []
727
+ };
728
+ }
729
+ }
730
+ catch {
731
+ // 忽略解析错误
732
+ }
733
+ }
734
+ }
735
+ return undefined;
736
+ }
737
+ /**
738
+ * 执行完整扫描并返回结果
739
+ */
740
+ function performFullScan(projectRoot, target) {
741
+ // 确定扫描目录
742
+ const testDirs = ['tests', 'test', '__tests__', 'spec', 'src/__tests__', 'cypress/e2e', 'e2e'];
743
+ const sourceDirs = target ? [target] : ['src', 'lib', 'app', 'packages'];
744
+ // 检测测试框架
745
+ const frameworks = detectTestFrameworks(projectRoot);
746
+ // 检测项目类型
747
+ const projectType = detectProjectType(projectRoot);
748
+ // 扫描测试文件
749
+ const existingTests = scanTestFiles(projectRoot, testDirs);
750
+ // 扫描源文件
751
+ const uncoveredSources = scanSourceFiles(projectRoot, sourceDirs, existingTests);
752
+ // 检测前端信息
753
+ const frontendInfo = detectFrontend(projectRoot, projectType);
754
+ // 检测测试风格
755
+ const testStyle = detectTestStyle(projectRoot, existingTests);
756
+ // 检测覆盖率报告
757
+ const coverageReport = detectCoverageReport(projectRoot);
758
+ return {
759
+ timestamp: new Date().toISOString(),
760
+ projectRoot,
761
+ target: target || 'all',
762
+ frameworks,
763
+ existingTests,
764
+ uncoveredSources,
765
+ projectType,
766
+ isFrontend: frontendInfo.isFrontend,
767
+ hasUIComponents: frontendInfo.hasUIComponents,
768
+ coverageReport,
769
+ testStyle,
770
+ summary: {
771
+ frameworkCount: frameworks.length,
772
+ existingTestCount: existingTests.length,
773
+ uncoveredSourceCount: uncoveredSources.length,
774
+ hasTestConfig: frameworks.some(f => f.configFile),
775
+ hasCoverageConfig: frameworks.some(f => f.commands.testCoverage)
776
+ }
777
+ };
778
+ }