ai-metrics-mcp-server 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +59 -0
- package/src/collector.js +1296 -0
- package/src/git-collector.js +957 -0
- package/src/git-collector.test.js +625 -0
- package/src/index.js +75 -0
- package/src/test.js +220 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI-Metrics Git Collector 单元测试
|
|
5
|
+
* 测试所有核心功能模块
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
// 导入被测试的函数(模拟导出)
|
|
14
|
+
// 注意:这些函数在 git-collector.js 中需要被导出
|
|
15
|
+
|
|
16
|
+
console.log('╔══════════════════════════════════════════════════════════════════════╗');
|
|
17
|
+
console.log('║ AI-Metrics Git Collector - Unit Test Suite ║');
|
|
18
|
+
console.log('╚══════════════════════════════════════════════════════════════════════╝\n');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 测试用例 1: 时间格式化函数
|
|
22
|
+
*/
|
|
23
|
+
function testGetLocalTime() {
|
|
24
|
+
console.log('Test 1: getLocalTime() - 时间格式化函数');
|
|
25
|
+
|
|
26
|
+
// 模拟 getLocalTime 函数
|
|
27
|
+
function getLocalTime() {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const offsetMinutes = now.getTimezoneOffset();
|
|
30
|
+
const offsetMs = offsetMinutes * 60 * 1000;
|
|
31
|
+
const localDate = new Date(now.getTime() - offsetMs);
|
|
32
|
+
return localDate.toISOString()
|
|
33
|
+
.replace('T', ' ')
|
|
34
|
+
.substring(0, 19);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = getLocalTime();
|
|
38
|
+
console.log(` Result: ${result}`);
|
|
39
|
+
|
|
40
|
+
// 验证格式 YYYY-MM-DD HH:mm:ss
|
|
41
|
+
const regex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
42
|
+
assert(regex.test(result), 'Time format should be YYYY-MM-DD HH:mm:ss');
|
|
43
|
+
|
|
44
|
+
console.log(' ✓ Passed: Time format is correct\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 测试用例 2: 文件操作类型判断
|
|
49
|
+
*/
|
|
50
|
+
function testDetermineOperation() {
|
|
51
|
+
console.log('Test 2: determineOperation() - 文件操作类型判断');
|
|
52
|
+
|
|
53
|
+
function determineOperation(added, deleted) {
|
|
54
|
+
if (added > 0 && deleted === 0) return 'add';
|
|
55
|
+
if (added === 0 && deleted > 0) return 'delete';
|
|
56
|
+
if (added > 0 && deleted > 0) return 'modify';
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const testCases = [
|
|
61
|
+
{ added: 10, deleted: 0, expected: 'add', desc: '新增文件' },
|
|
62
|
+
{ added: 0, deleted: 5, expected: 'delete', desc: '删除文件' },
|
|
63
|
+
{ added: 10, deleted: 5, expected: 'modify', desc: '修改文件' },
|
|
64
|
+
{ added: 0, deleted: 0, expected: 'unknown', desc: '未知操作' }
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
for (const tc of testCases) {
|
|
68
|
+
const result = determineOperation(tc.added, tc.deleted);
|
|
69
|
+
assert.strictEqual(result, tc.expected, `${tc.desc} failed`);
|
|
70
|
+
console.log(` ✓ ${tc.desc}: ${result}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(' ✓ Passed: All operation types are correct\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 测试用例 3: 文件过滤规则
|
|
78
|
+
*/
|
|
79
|
+
function testShouldFilterFile() {
|
|
80
|
+
console.log('Test 3: shouldFilterFile() - 文件过滤规则');
|
|
81
|
+
|
|
82
|
+
function shouldFilterFile(filePath) {
|
|
83
|
+
const filterPatterns = [
|
|
84
|
+
/node_modules\//,
|
|
85
|
+
/dist\//,
|
|
86
|
+
/build\//,
|
|
87
|
+
/vendor\//,
|
|
88
|
+
/\.min\./,
|
|
89
|
+
/package-lock\.json$/,
|
|
90
|
+
/yarn\.lock$/,
|
|
91
|
+
/pnpm-lock\.yaml$/,
|
|
92
|
+
/\.log$/,
|
|
93
|
+
/\.jpg$/,
|
|
94
|
+
/\.png$/,
|
|
95
|
+
/\.gif$/,
|
|
96
|
+
/\.pdf$/,
|
|
97
|
+
/\.zip$/
|
|
98
|
+
];
|
|
99
|
+
return filterPatterns.some(pattern => pattern.test(filePath));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const testCases = [
|
|
103
|
+
{ path: 'node_modules/package/index.js', shouldFilter: true, desc: 'node_modules' },
|
|
104
|
+
{ path: 'dist/bundle.js', shouldFilter: true, desc: 'dist' },
|
|
105
|
+
{ path: 'build/output.js', shouldFilter: true, desc: 'build' },
|
|
106
|
+
{ path: 'package-lock.json', shouldFilter: true, desc: 'package-lock.json' },
|
|
107
|
+
{ path: 'src/app.js', shouldFilter: false, desc: 'source file' },
|
|
108
|
+
{ path: 'README.md', shouldFilter: false, desc: 'markdown' },
|
|
109
|
+
{ path: 'image.png', shouldFilter: true, desc: 'image file' },
|
|
110
|
+
{ path: 'style.min.css', shouldFilter: true, desc: 'minified file' }
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const tc of testCases) {
|
|
114
|
+
const result = shouldFilterFile(tc.path);
|
|
115
|
+
assert.strictEqual(result, tc.shouldFilter, `Filter ${tc.desc} failed`);
|
|
116
|
+
console.log(` ✓ ${tc.desc}: ${result ? 'filtered' : 'included'}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(' ✓ Passed: File filtering works correctly\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 测试用例 4: AI 代码识别初始化
|
|
124
|
+
*/
|
|
125
|
+
function testIdentifyAICodeInitialization() {
|
|
126
|
+
console.log('Test 4: identifyAICode() - AI 代码识别初始化');
|
|
127
|
+
|
|
128
|
+
const commitInfo = {
|
|
129
|
+
commitId: 'abc123',
|
|
130
|
+
branch: 'main',
|
|
131
|
+
gitStorePath: 'git@github.com:user/repo.git'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const commitFiles = [
|
|
135
|
+
{ filePath: 'src/app.js', operation: 'add', addedLines: 10, deletedLines: 0 },
|
|
136
|
+
{ filePath: 'src/utils.js', operation: 'modify', addedLines: 5, deletedLines: 2 }
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// 模拟没有会话文件的情况
|
|
140
|
+
const result = {
|
|
141
|
+
aiAddedLines: 0,
|
|
142
|
+
aiModifiedLines: 0,
|
|
143
|
+
aiDeletedLines: 0,
|
|
144
|
+
aiFiles: [],
|
|
145
|
+
ideType: 'unknown',
|
|
146
|
+
sessionIdeType: 'unknown'
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// 验证初始状态
|
|
150
|
+
assert.strictEqual(result.aiAddedLines, 0, 'aiAddedLines should be 0');
|
|
151
|
+
assert.strictEqual(result.ideType, 'unknown', 'ideType should be unknown by default');
|
|
152
|
+
assert.deepStrictEqual(result.aiFiles, [], 'aiFiles should be empty');
|
|
153
|
+
|
|
154
|
+
console.log(' ✓ AI metrics initialized correctly');
|
|
155
|
+
console.log(` ✓ Default ideType: ${result.ideType}`);
|
|
156
|
+
console.log(' ✓ Passed: AI code identification is ready\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 测试用例 5: 数据聚合计算
|
|
161
|
+
*/
|
|
162
|
+
function testReportDataCalculation() {
|
|
163
|
+
console.log('Test 5: Report Data Calculation - 数据聚合计算');
|
|
164
|
+
|
|
165
|
+
// 模拟文件变更数据
|
|
166
|
+
const filesWithDetails = [
|
|
167
|
+
{
|
|
168
|
+
filePath: 'src/app.js',
|
|
169
|
+
operation: 'add',
|
|
170
|
+
addedLines: 50,
|
|
171
|
+
deletedLines: 0,
|
|
172
|
+
totalLines: 50
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
filePath: 'src/utils.js',
|
|
176
|
+
operation: 'modify',
|
|
177
|
+
addedLines: 30,
|
|
178
|
+
deletedLines: 10,
|
|
179
|
+
totalLines: 40
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
filePath: 'src/config.js',
|
|
183
|
+
operation: 'delete',
|
|
184
|
+
addedLines: 0,
|
|
185
|
+
deletedLines: 20,
|
|
186
|
+
totalLines: 20
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// AI 指标
|
|
191
|
+
const aiMetrics = {
|
|
192
|
+
aiAddedLines: 40,
|
|
193
|
+
aiModifiedLines: 15,
|
|
194
|
+
aiDeletedLines: 5
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 计算统计
|
|
198
|
+
const totalAddedLines = filesWithDetails.reduce((sum, f) => sum + f.addedLines, 0);
|
|
199
|
+
const totalDeletedLines = filesWithDetails.reduce((sum, f) => sum + f.deletedLines, 0);
|
|
200
|
+
const totalModifiedLines = filesWithDetails.filter(f => f.operation === 'modify')
|
|
201
|
+
.reduce((sum, f) => sum + (f.addedLines + f.deletedLines), 0);
|
|
202
|
+
|
|
203
|
+
const totalAILines = aiMetrics.aiAddedLines + aiMetrics.aiModifiedLines + aiMetrics.aiDeletedLines;
|
|
204
|
+
// 总代码行数 = 新增行 + 修改行 + 删除行
|
|
205
|
+
// 修改行 = 30+10 = 40
|
|
206
|
+
// 总代码行数 = 50 + 40 + 30 = 120
|
|
207
|
+
const totalCodeLines = totalAddedLines + totalModifiedLines + totalDeletedLines;
|
|
208
|
+
const aiCodeRatio = totalCodeLines > 0 ? (totalAILines / totalCodeLines * 100).toFixed(2) : 0;
|
|
209
|
+
|
|
210
|
+
// 验证计算结果
|
|
211
|
+
assert.strictEqual(totalAddedLines, 80, 'Total added lines');
|
|
212
|
+
assert.strictEqual(totalDeletedLines, 30, 'Total deleted lines');
|
|
213
|
+
assert.strictEqual(totalModifiedLines, 40, 'Total modified lines');
|
|
214
|
+
assert.strictEqual(totalAILines, 60, 'Total AI lines');
|
|
215
|
+
// 60 / 150 = 0.4 = 40%
|
|
216
|
+
assert.strictEqual(totalCodeLines, 150, 'Total code lines');
|
|
217
|
+
assert.strictEqual(aiCodeRatio, '40.00', 'AI code ratio');
|
|
218
|
+
|
|
219
|
+
console.log(` ✓ Total Added Lines: ${totalAddedLines}`);
|
|
220
|
+
console.log(` ✓ Total Deleted Lines: ${totalDeletedLines}`);
|
|
221
|
+
console.log(` ✓ Total Modified Lines: ${totalModifiedLines}`);
|
|
222
|
+
console.log(` ✓ Total Code Lines: ${totalCodeLines}`);
|
|
223
|
+
console.log(` ✓ Total AI Lines: ${totalAILines}`);
|
|
224
|
+
console.log(` ✓ AI Code Ratio: ${aiCodeRatio}%`);
|
|
225
|
+
console.log(' ✓ Passed: All calculations are correct\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 测试用例 6: ideType 验证
|
|
230
|
+
*/
|
|
231
|
+
function testValidateIdeType() {
|
|
232
|
+
console.log('Test 6: validateIdeType() - ideType 字段验证');
|
|
233
|
+
|
|
234
|
+
function validateIdeType(ideType) {
|
|
235
|
+
const validTypes = ['cursor', 'vscode', 'sublime', 'jetbrains', 'vim', 'emacs', 'unknown'];
|
|
236
|
+
return validTypes.includes(ideType) ? ideType : 'unknown';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const testCases = [
|
|
240
|
+
{ input: 'cursor', expected: 'cursor', desc: 'Cursor IDE' },
|
|
241
|
+
{ input: 'vscode', expected: 'vscode', desc: 'VS Code' },
|
|
242
|
+
{ input: 'jetbrains', expected: 'jetbrains', desc: 'JetBrains' },
|
|
243
|
+
{ input: 'invalid', expected: 'unknown', desc: 'Invalid type' },
|
|
244
|
+
{ input: '', expected: 'unknown', desc: 'Empty string' },
|
|
245
|
+
{ input: null, expected: 'unknown', desc: 'Null value' }
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const tc of testCases) {
|
|
249
|
+
const result = validateIdeType(tc.input);
|
|
250
|
+
assert.strictEqual(result, tc.expected, `ideType validation for ${tc.desc} failed`);
|
|
251
|
+
console.log(` ✓ ${tc.desc}: ${result}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(' ✓ Passed: ideType validation works correctly\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 测试用例 7: createTime 验证
|
|
259
|
+
*/
|
|
260
|
+
function testValidateCreateTime() {
|
|
261
|
+
console.log('Test 7: validateCreateTime() - createTime 字段验证');
|
|
262
|
+
|
|
263
|
+
function validateCreateTime(createTime) {
|
|
264
|
+
const regex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
265
|
+
return regex.test(createTime) ? createTime : 'INVALID';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const validTime = '2026-01-13 20:11:30';
|
|
269
|
+
const invalidTime = '2026-01-13T20:11:30Z';
|
|
270
|
+
|
|
271
|
+
const validResult = validateCreateTime(validTime);
|
|
272
|
+
const invalidResult = validateCreateTime(invalidTime);
|
|
273
|
+
|
|
274
|
+
assert.strictEqual(validResult, validTime, 'Valid time format');
|
|
275
|
+
assert.strictEqual(invalidResult, 'INVALID', 'Invalid time format');
|
|
276
|
+
|
|
277
|
+
console.log(` ✓ Valid time format: ${validResult}`);
|
|
278
|
+
console.log(` ✓ Invalid time format detected: ${invalidResult}`);
|
|
279
|
+
console.log(' ✓ Passed: createTime validation works correctly\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 测试用例 8: 本地缓存目录结构
|
|
284
|
+
*/
|
|
285
|
+
function testCacheDirectoryStructure() {
|
|
286
|
+
console.log('Test 8: Cache Directory Structure - 缓存目录结构验证');
|
|
287
|
+
|
|
288
|
+
const cacheDir = path.join(os.homedir(), '.ai-metrics-cache');
|
|
289
|
+
|
|
290
|
+
// 验证缓存目录结构
|
|
291
|
+
console.log(` Cache root: ${cacheDir}`);
|
|
292
|
+
console.log(` Fail cache: ${path.join(cacheDir, 'reportToServerFail/')}`);
|
|
293
|
+
console.log(` Session cache pattern: ${path.join(cacheDir, '{hash}', '{branch}', 'session-*.json')}`);
|
|
294
|
+
|
|
295
|
+
console.log(' ✓ Passed: Cache directory structure is correct\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 测试用例 9: 错误处理
|
|
300
|
+
*/
|
|
301
|
+
function testErrorHandling() {
|
|
302
|
+
console.log('Test 9: Error Handling - 错误处理');
|
|
303
|
+
|
|
304
|
+
function safeGetGitInfo(filePath) {
|
|
305
|
+
try {
|
|
306
|
+
if (!filePath) {
|
|
307
|
+
throw new Error('filePath is required');
|
|
308
|
+
}
|
|
309
|
+
return { gitStorePath: 'git@github.com:user/repo.git', branch: 'main' };
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return { gitStorePath: '', branch: '' };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const validResult = safeGetGitInfo('/path/to/file.js');
|
|
316
|
+
const invalidResult = safeGetGitInfo(null);
|
|
317
|
+
|
|
318
|
+
assert.strictEqual(validResult.gitStorePath, 'git@github.com:user/repo.git', 'Valid result');
|
|
319
|
+
assert.strictEqual(invalidResult.gitStorePath, '', 'Error handling fallback');
|
|
320
|
+
|
|
321
|
+
console.log(' ✓ Valid case handled correctly');
|
|
322
|
+
console.log(' ✓ Error case handled with fallback');
|
|
323
|
+
console.log(' ✓ Passed: Error handling works correctly\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 测试用例 10: 内容对比验证
|
|
328
|
+
*/
|
|
329
|
+
function testCompareLineContent() {
|
|
330
|
+
console.log('Test 10: compareLineContent() - 内容对比验证');
|
|
331
|
+
|
|
332
|
+
// 模拟 AI 生成的行
|
|
333
|
+
const aiGeneratedLines = [
|
|
334
|
+
{ lineNumber: 1, content: 'const app = express();' },
|
|
335
|
+
{ lineNumber: 2, content: 'app.use(cors());' },
|
|
336
|
+
{ lineNumber: 3, content: 'console.log("Server started");' }
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
// 模拟最终文件内容
|
|
340
|
+
const finalFileContent = `const app = express();
|
|
341
|
+
app.use(cors());
|
|
342
|
+
app.listen(3000);
|
|
343
|
+
console.log("Server started");
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
// 模拟 compareLineContent 函数
|
|
347
|
+
function compareLineContent(aiLines, finalContent) {
|
|
348
|
+
const finalLines = finalContent.split('\n');
|
|
349
|
+
const matchedLines = [];
|
|
350
|
+
|
|
351
|
+
for (const aiLine of aiLines) {
|
|
352
|
+
const aiLineContent = (aiLine.content || '').trim();
|
|
353
|
+
if (!aiLineContent) continue;
|
|
354
|
+
|
|
355
|
+
const foundIndex = finalLines.findIndex(line => line.trim() === aiLineContent);
|
|
356
|
+
if (foundIndex !== -1) {
|
|
357
|
+
matchedLines.push({
|
|
358
|
+
...aiLine,
|
|
359
|
+
finalLineNumber: foundIndex + 1,
|
|
360
|
+
found: true
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return matchedLines;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const matchedLines = compareLineContent(aiGeneratedLines, finalFileContent);
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(matchedLines.length, 3, 'All three lines should match');
|
|
370
|
+
assert.strictEqual(matchedLines[0].content, 'const app = express();', 'First line matches');
|
|
371
|
+
assert.strictEqual(matchedLines[1].content, 'app.use(cors());', 'Second line matches');
|
|
372
|
+
assert.strictEqual(matchedLines[2].content, 'console.log("Server started");', 'Third line matches');
|
|
373
|
+
|
|
374
|
+
console.log(` ✓ AI Generated Lines: ${aiGeneratedLines.length}`);
|
|
375
|
+
console.log(` ✓ Matched in Final Content: ${matchedLines.length}`);
|
|
376
|
+
console.log(' ✓ Passed: Content comparison works correctly\n');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 测试用例 11: 部分内容匹配
|
|
381
|
+
*/
|
|
382
|
+
function testPartialContentMatch() {
|
|
383
|
+
console.log('Test 11: Partial Content Match - 部分内容匹配验证');
|
|
384
|
+
|
|
385
|
+
// 模拟 AI 生成的行
|
|
386
|
+
const aiGeneratedLines = [
|
|
387
|
+
{ lineNumber: 1, content: 'const server = app.listen(3000);' },
|
|
388
|
+
{ lineNumber: 2, content: 'deleted line that no longer exists' },
|
|
389
|
+
{ lineNumber: 3, content: 'console.log("Port: 3000");' }
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
// 模拟最终文件内容(第二行被删除)
|
|
393
|
+
const finalFileContent = `const server = app.listen(3000);
|
|
394
|
+
console.log("Port: 3000");
|
|
395
|
+
console.log("Server is running");
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
// 模拟 compareLineContent 函数
|
|
399
|
+
function compareLineContent(aiLines, finalContent) {
|
|
400
|
+
const finalLines = finalContent.split('\n');
|
|
401
|
+
const matchedLines = [];
|
|
402
|
+
|
|
403
|
+
for (const aiLine of aiLines) {
|
|
404
|
+
const aiLineContent = (aiLine.content || '').trim();
|
|
405
|
+
if (!aiLineContent) continue;
|
|
406
|
+
|
|
407
|
+
const foundIndex = finalLines.findIndex(line => line.trim() === aiLineContent);
|
|
408
|
+
if (foundIndex !== -1) {
|
|
409
|
+
matchedLines.push({
|
|
410
|
+
...aiLine,
|
|
411
|
+
finalLineNumber: foundIndex + 1,
|
|
412
|
+
found: true
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return matchedLines;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const matchedLines = compareLineContent(aiGeneratedLines, finalFileContent);
|
|
420
|
+
|
|
421
|
+
// 只有两行在最终文件中存在
|
|
422
|
+
assert.strictEqual(matchedLines.length, 2, 'Only two lines should match');
|
|
423
|
+
assert(matchedLines.find(l => l.content === 'const server = app.listen(3000);'), 'First line exists');
|
|
424
|
+
assert(matchedLines.find(l => l.content === 'console.log("Port: 3000");'), 'Third line exists');
|
|
425
|
+
assert(!matchedLines.find(l => l.content === 'deleted line that no longer exists'), 'Deleted line not matched');
|
|
426
|
+
|
|
427
|
+
console.log(` ✓ AI Generated Lines: ${aiGeneratedLines.length}`);
|
|
428
|
+
console.log(` ✓ Matched in Final Content: ${matchedLines.length}`);
|
|
429
|
+
console.log(' ✓ Deleted Lines Filtered Out: 1');
|
|
430
|
+
console.log(' ✓ Passed: Partial content matching works correctly\n');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 测试用例 12: 完整流程验证
|
|
435
|
+
*/
|
|
436
|
+
function testCompletePipeline() {
|
|
437
|
+
console.log('Test 12: Complete Pipeline - 完整流程验证');
|
|
438
|
+
|
|
439
|
+
// 模拟完整的数据流
|
|
440
|
+
const reportData = {
|
|
441
|
+
userId: 'user123',
|
|
442
|
+
commitId: 'abc123',
|
|
443
|
+
commitTime: new Date().toISOString(),
|
|
444
|
+
gitStorePath: 'git@github.com:user/repo.git',
|
|
445
|
+
branchName: 'main',
|
|
446
|
+
message: 'Feature: Add AI detection',
|
|
447
|
+
ideType: 'cursor',
|
|
448
|
+
createTime: '2026-01-13 20:11:30',
|
|
449
|
+
totalAddedLines: 80,
|
|
450
|
+
totalModifiedLines: 40,
|
|
451
|
+
totalDeletedLines: 30,
|
|
452
|
+
totalFiles: 3,
|
|
453
|
+
aiAddedLines: 40, // 已通过内容对比验证
|
|
454
|
+
aiModifiedLines: 15, // 已通过内容对比验证
|
|
455
|
+
aiDeletedLines: 5, // 已通过内容对比验证
|
|
456
|
+
aiCodeRatio: 40.00,
|
|
457
|
+
aiFiles: ['src/app.js', 'src/utils.js'],
|
|
458
|
+
commitFiles: ['src/app.js', 'src/utils.js', 'src/config.js'],
|
|
459
|
+
metadata: {
|
|
460
|
+
hostname: 'localhost',
|
|
461
|
+
platform: 'darwin',
|
|
462
|
+
collectorVersion: '1.0.0',
|
|
463
|
+
timestamp: new Date().toISOString(),
|
|
464
|
+
aiVerified: true, // 标记为已通过内容验证
|
|
465
|
+
matchDetails: []
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// 验证报告数据的完整性
|
|
470
|
+
assert(reportData.userId, 'userId required');
|
|
471
|
+
assert(reportData.commitId, 'commitId required');
|
|
472
|
+
assert(reportData.gitStorePath, 'gitStorePath required');
|
|
473
|
+
assert(reportData.ideType, 'ideType required');
|
|
474
|
+
assert(reportData.createTime, 'createTime required');
|
|
475
|
+
assert(reportData.metadata.aiVerified === true, 'aiVerified flag must be true');
|
|
476
|
+
assert(reportData.totalAddedLines >= 0, 'totalAddedLines must be non-negative');
|
|
477
|
+
assert(reportData.aiCodeRatio >= 0 && reportData.aiCodeRatio <= 100, 'aiCodeRatio must be between 0-100');
|
|
478
|
+
assert(Array.isArray(reportData.aiFiles), 'aiFiles must be an array');
|
|
479
|
+
assert(reportData.metadata, 'metadata required');
|
|
480
|
+
assert(reportData.aiAddedLines <= reportData.totalAddedLines, 'aiAddedLines should not exceed totalAddedLines');
|
|
481
|
+
assert(reportData.aiModifiedLines <= reportData.totalModifiedLines, 'aiModifiedLines should not exceed totalModifiedLines');
|
|
482
|
+
assert(reportData.aiDeletedLines <= reportData.totalDeletedLines, 'aiDeletedLines should not exceed totalDeletedLines');
|
|
483
|
+
|
|
484
|
+
console.log(' ✓ All required fields present');
|
|
485
|
+
console.log(' ✓ AI verification flag enabled');
|
|
486
|
+
console.log(' ✓ Data types are correct');
|
|
487
|
+
console.log(' ✓ Values are within valid ranges');
|
|
488
|
+
console.log(' ✓ AI metrics do not exceed total metrics');
|
|
489
|
+
console.log(' ✓ Passed: Complete pipeline validation successful\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 执行所有测试
|
|
494
|
+
*/
|
|
495
|
+
function runAllTests() {
|
|
496
|
+
console.log('Running all unit tests...\n');
|
|
497
|
+
|
|
498
|
+
const testFunctions = [
|
|
499
|
+
testGetLocalTime,
|
|
500
|
+
testDetermineOperation,
|
|
501
|
+
testShouldFilterFile,
|
|
502
|
+
testIdentifyAICodeInitialization,
|
|
503
|
+
testReportDataCalculation,
|
|
504
|
+
testValidateIdeType,
|
|
505
|
+
testValidateCreateTime,
|
|
506
|
+
testCacheDirectoryStructure,
|
|
507
|
+
testErrorHandling,
|
|
508
|
+
testCompareLineContent,
|
|
509
|
+
testPartialContentMatch,
|
|
510
|
+
testCompletePipeline
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
let passedCount = 0;
|
|
514
|
+
let failedCount = 0;
|
|
515
|
+
|
|
516
|
+
for (const testFunc of testFunctions) {
|
|
517
|
+
try {
|
|
518
|
+
testFunc();
|
|
519
|
+
passedCount++;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error(`✗ ${testFunc.name} failed:`);
|
|
522
|
+
console.error(` ${error.message}\n`);
|
|
523
|
+
failedCount++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 打印测试总结
|
|
528
|
+
console.log('╔══════════════════════════════════════════════════════════════════════╗');
|
|
529
|
+
console.log('║ Test Summary ║');
|
|
530
|
+
console.log('╚══════════════════════════════════════════════════════════════════════╝');
|
|
531
|
+
console.log(`Total Tests: ${testFunctions.length}`);
|
|
532
|
+
console.log(`✓ Passed: ${passedCount}`);
|
|
533
|
+
console.log(`✗ Failed: ${failedCount}\n`);
|
|
534
|
+
|
|
535
|
+
if (failedCount === 0) {
|
|
536
|
+
console.log('✓ All tests passed!\n');
|
|
537
|
+
process.exit(0);
|
|
538
|
+
} else {
|
|
539
|
+
console.log('✗ Some tests failed!\n');
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 测试用例:验证 Hook Content 内容
|
|
546
|
+
*/
|
|
547
|
+
function testHookContent() {
|
|
548
|
+
console.log('\n\n╔══════════════════════════════════════════════════════════════════════╗');
|
|
549
|
+
console.log('║ 测试: 验证 generateHookContent() 输出 ║');
|
|
550
|
+
console.log('╚══════════════════════════════════════════════════════════════════════╝\n');
|
|
551
|
+
|
|
552
|
+
// 模拟 generateHookContent 函数
|
|
553
|
+
const HOOK_VERSION = 2;
|
|
554
|
+
const HOOK_MAGIC_STRING = '# AI-METRICS-HOOK-MARKER';
|
|
555
|
+
|
|
556
|
+
function generateHookContent() {
|
|
557
|
+
const content = `#!/bin/bash
|
|
558
|
+
${HOOK_MAGIC_STRING}
|
|
559
|
+
# AI_METRICS_HOOK_VERSION=${HOOK_VERSION}
|
|
560
|
+
|
|
561
|
+
# ====== AI METRICS GIT COLLECTOR HOOK ======
|
|
562
|
+
# 此 hook 由 AI Metrics 系统自动安装和管理
|
|
563
|
+
# 请勿手动修改此文件内容
|
|
564
|
+
# 如需禁用,请执行: rm .git/hooks/post-commit
|
|
565
|
+
# =============================================
|
|
566
|
+
|
|
567
|
+
set -e
|
|
568
|
+
|
|
569
|
+
# 获取当前提交的 hash
|
|
570
|
+
COMMIT_HASH=\\$(git rev-parse HEAD)
|
|
571
|
+
|
|
572
|
+
# 执行 AI Metrics 数据收集
|
|
573
|
+
if [ -f ~/.ai-metrics-cache/git-collector.js ]; then
|
|
574
|
+
AI_METRICS_USER_ID="\${AI_METRICS_USER_ID}" \\
|
|
575
|
+
node ~/.ai-metrics-cache/git-collector.js "\\$COMMIT_HASH" 2>/dev/null || true
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
# Hook 结束
|
|
579
|
+
`;
|
|
580
|
+
return content;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const hookContent = generateHookContent();
|
|
584
|
+
|
|
585
|
+
console.log('📋 生成的 Hook Content:');
|
|
586
|
+
console.log('━'.repeat(70));
|
|
587
|
+
console.log(hookContent);
|
|
588
|
+
console.log('━'.repeat(70));
|
|
589
|
+
|
|
590
|
+
// 分析问题
|
|
591
|
+
console.log('\n🔍 问题分析:\n');
|
|
592
|
+
|
|
593
|
+
// 检查 1: 检查反斜杠是否正确
|
|
594
|
+
console.log('1️⃣ 反斜杠转义问题:');
|
|
595
|
+
if (hookContent.includes('\\$')) {
|
|
596
|
+
console.log(' ❌ 发现 \\$ 这是错的!');
|
|
597
|
+
console.log(' 在 JavaScript 模板字符串中,\\\\ 会被转义为单个 \\');
|
|
598
|
+
console.log(' 最后生成的 shell 脚本会有 \\$ 而不是 $');
|
|
599
|
+
} else {
|
|
600
|
+
console.log(' ✓ 反斜杠正确');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 检查 2: 检查行尾反斜杠
|
|
604
|
+
console.log('\n2️⃣ 行尾反斜杠问题:');
|
|
605
|
+
if (hookContent.includes('AI_METRICS_USER_ID=')) {
|
|
606
|
+
const nodeLineStart = hookContent.indexOf('node ~/.ai-metrics-cache');
|
|
607
|
+
const nodeLinePrev = hookContent.substring(nodeLineStart - 50, nodeLineStart + 100);
|
|
608
|
+
console.log(' ❌ 发现行尾有多余的反斜杠!');
|
|
609
|
+
console.log(` 这会导致 shell 脚本行继续(line continuation)`);
|
|
610
|
+
console.log(` 导致 node 命令不会执行`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 检查 3: 打印实际的 shell 脚本
|
|
614
|
+
console.log('\n3️⃣ 实际的 shell 脚本会是这样:');
|
|
615
|
+
console.log('━'.repeat(70));
|
|
616
|
+
console.log(hookContent.split('\n').slice(9, 14).join('\n'));
|
|
617
|
+
console.log('━'.repeat(70));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 运行测试
|
|
621
|
+
runAllTests();
|
|
622
|
+
|
|
623
|
+
// 额外运行 Hook Content 测试
|
|
624
|
+
testHookContent();
|
|
625
|
+
|
package/src/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI代码统计MCP Server
|
|
5
|
+
* 用于自动采集AI生成的代码统计数据
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import {
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import CodeStatsCollector from './collector.js';
|
|
16
|
+
|
|
17
|
+
dotenv.config();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 主函数
|
|
21
|
+
*/
|
|
22
|
+
async function main() {
|
|
23
|
+
// 初始化收集器
|
|
24
|
+
const collector = new CodeStatsCollector();
|
|
25
|
+
|
|
26
|
+
// 创建MCP Server
|
|
27
|
+
const server = new Server(
|
|
28
|
+
{
|
|
29
|
+
name: 'ai-metrics-mcp-server',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
capabilities: {
|
|
34
|
+
tools: {},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// 注册工具列表处理器
|
|
40
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
41
|
+
tools: collector.getTools()
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// 注册工具调用处理器
|
|
45
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
46
|
+
const { name, arguments: args } = request.params;
|
|
47
|
+
return await collector.handleToolCall(name, args);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 启动服务
|
|
51
|
+
const transport = new StdioServerTransport();
|
|
52
|
+
await server.connect(transport);
|
|
53
|
+
|
|
54
|
+
// 输出到stderr(stdout用于MCP通信)
|
|
55
|
+
console.error('AI Metrics MCP Server started successfully');
|
|
56
|
+
console.error(`User ID: ${process.env.USER_ID || 'Not configured'}`);
|
|
57
|
+
console.error(`Cache Dir: ${process.env.CACHE_DIR || '~/.ai-metrics-cache'}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 错误处理
|
|
61
|
+
process.on('uncaughtException', (error) => {
|
|
62
|
+
console.error('Uncaught Exception:', error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
67
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 启动
|
|
72
|
+
main().catch((error) => {
|
|
73
|
+
console.error('Failed to start server:', error);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|