openmatrix 0.2.19 → 0.2.21
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/dist/cli/commands/test.d.ts +20 -0
- package/dist/cli/commands/test.js +216 -0
- package/dist/cli/index.js +2 -0
- package/dist/test/context-analyzer.d.ts +76 -0
- package/dist/test/context-analyzer.js +778 -0
- package/dist/test/generator.d.ts +17 -0
- package/dist/test/generator.js +403 -0
- package/dist/types/index.d.ts +309 -0
- package/package.json +1 -1
- package/skills/approve.md +32 -1
- package/skills/auto.md +32 -1
- package/skills/brainstorm.md +94 -23
- package/skills/debug.md +56 -1
- package/skills/deploy.md +155 -9
- package/skills/feature.md +32 -1
- package/skills/meeting.md +32 -1
- package/skills/om.md +80 -26
- package/skills/report.md +32 -1
- package/skills/research.md +32 -1
- package/skills/resume.md +31 -1
- package/skills/retry.md +31 -1
- package/skills/start.md +32 -1
- package/skills/status.md +32 -1
- package/skills/test.md +750 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TestScanResult, UncoveredSourceFile, GeneratedTestFile, GeneratedMockFile, TestGenerationResult, TestConfig } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 生成测试文件
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateTestFiles(scanResult: TestScanResult, config: TestConfig, selectedSources: UncoveredSourceFile[]): TestGenerationResult;
|
|
6
|
+
/**
|
|
7
|
+
* 生成 E2E 测试文件
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateE2ETestFiles(scanResult: TestScanResult, config: TestConfig, uiComponents: UncoveredSourceFile[]): GeneratedTestFile[];
|
|
10
|
+
/**
|
|
11
|
+
* 写入生成的测试文件
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeGeneratedFiles(files: GeneratedTestFile[], mockFiles: GeneratedMockFile[], projectRoot: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* 生成测试文件(简化接口)
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateTests(projectRoot: string, scanResult: TestScanResult, targetFiles?: string[]): TestGenerationResult;
|
|
@@ -0,0 +1,403 @@
|
|
|
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.generateTestFiles = generateTestFiles;
|
|
37
|
+
exports.generateE2ETestFiles = generateE2ETestFiles;
|
|
38
|
+
exports.writeGeneratedFiles = writeGeneratedFiles;
|
|
39
|
+
exports.generateTests = generateTests;
|
|
40
|
+
// src/test/generator.ts
|
|
41
|
+
/**
|
|
42
|
+
* 测试用例生成器 - 基于上下文生成测试用例模板
|
|
43
|
+
*
|
|
44
|
+
* 功能:
|
|
45
|
+
* - 生成测试文件模板
|
|
46
|
+
* - 根据现有测试风格保持一致性
|
|
47
|
+
* - 支持多种测试框架
|
|
48
|
+
* - 生成 Mock 文件
|
|
49
|
+
*
|
|
50
|
+
* 模块依赖: test/context-analyzer → test/generator → skills/test
|
|
51
|
+
*/
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
/**
|
|
55
|
+
* 生成测试文件
|
|
56
|
+
*/
|
|
57
|
+
function generateTestFiles(scanResult, config, selectedSources) {
|
|
58
|
+
const templateConfig = {
|
|
59
|
+
framework: config.framework,
|
|
60
|
+
testStyle: scanResult.testStyle,
|
|
61
|
+
outputDir: config.outputDir || 'tests',
|
|
62
|
+
fileSuffix: scanResult.testStyle?.fileSuffix || '.test.ts'
|
|
63
|
+
};
|
|
64
|
+
const files = [];
|
|
65
|
+
const testCases = [];
|
|
66
|
+
const mockFiles = [];
|
|
67
|
+
for (const source of selectedSources) {
|
|
68
|
+
const testFile = generateSingleTestFile(source, templateConfig, scanResult.projectRoot);
|
|
69
|
+
files.push(testFile);
|
|
70
|
+
const cases = generateTestCaseDescriptions(source, config.testTypes);
|
|
71
|
+
testCases.push(...cases);
|
|
72
|
+
}
|
|
73
|
+
// 如果需要 Mock
|
|
74
|
+
if (config.includeMocks) {
|
|
75
|
+
const mocks = generateMockFiles(selectedSources, templateConfig, scanResult.projectRoot);
|
|
76
|
+
mockFiles.push(...mocks);
|
|
77
|
+
}
|
|
78
|
+
// 获取运行命令
|
|
79
|
+
const primaryFramework = scanResult.frameworks.find(f => f.isPrimary);
|
|
80
|
+
const runCommand = primaryFramework?.commands.test || 'npm test';
|
|
81
|
+
return {
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
config,
|
|
84
|
+
files,
|
|
85
|
+
testCases,
|
|
86
|
+
mockFiles,
|
|
87
|
+
statistics: {
|
|
88
|
+
fileCount: files.length,
|
|
89
|
+
testCaseCount: testCases.length,
|
|
90
|
+
unitTestCount: testCases.filter(c => c.type === 'unit').length,
|
|
91
|
+
integrationTestCount: testCases.filter(c => c.type === 'integration').length,
|
|
92
|
+
e2eTestCount: testCases.filter(c => c.type === 'e2e').length,
|
|
93
|
+
mockFileCount: mockFiles.length
|
|
94
|
+
},
|
|
95
|
+
runCommand,
|
|
96
|
+
notes: [
|
|
97
|
+
'测试文件已生成,请检查内容是否符合预期',
|
|
98
|
+
'运行测试前请确保安装了必要的依赖'
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 生成单个测试文件
|
|
104
|
+
*/
|
|
105
|
+
function generateSingleTestFile(source, config, projectRoot) {
|
|
106
|
+
const testPath = determineTestPath(source.path, config);
|
|
107
|
+
const content = generateTestContent(source, config);
|
|
108
|
+
return {
|
|
109
|
+
path: testPath,
|
|
110
|
+
content,
|
|
111
|
+
type: config.testStyle?.usesTypeScript ? 'unit' : 'unit',
|
|
112
|
+
sourceFile: source.path,
|
|
113
|
+
testCaseIds: source.exports.map((exp, i) => `${source.path}-${exp}-${i}`),
|
|
114
|
+
overwrites: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 确定测试文件路径
|
|
119
|
+
*/
|
|
120
|
+
function determineTestPath(sourcePath, config) {
|
|
121
|
+
const sourceFileName = path.basename(sourcePath);
|
|
122
|
+
const sourceDir = path.dirname(sourcePath);
|
|
123
|
+
const ext = path.extname(sourceFileName);
|
|
124
|
+
let testFileName;
|
|
125
|
+
if (config.testStyle?.fileLocation === 'adjacent') {
|
|
126
|
+
// 同目录
|
|
127
|
+
testFileName = sourceFileName.replace(ext, config.fileSuffix.replace('.test', '.test').replace('.spec', '.spec'));
|
|
128
|
+
return path.join(sourceDir, testFileName);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// 独立目录
|
|
132
|
+
testFileName = sourceFileName.replace(ext, config.fileSuffix);
|
|
133
|
+
return path.join(config.outputDir, sourceDir, testFileName);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 生成测试内容
|
|
138
|
+
*/
|
|
139
|
+
function generateTestContent(source, config) {
|
|
140
|
+
const framework = config.framework;
|
|
141
|
+
const usesDescribeIt = config.testStyle?.namingConvention === 'describe-it';
|
|
142
|
+
const usesTypeScript = config.testStyle?.usesTypeScript ?? true;
|
|
143
|
+
// 导入语句
|
|
144
|
+
const importPath = source.path.replace(/\.(ts|tsx)$/, '').replace(/\.(js|jsx)$/, '');
|
|
145
|
+
const imports = generateImports(source.exports, importPath, usesTypeScript);
|
|
146
|
+
// 测试块
|
|
147
|
+
const testBlocks = source.exports.map(exp => generateTestBlock(exp, framework, usesDescribeIt, source.fileType));
|
|
148
|
+
// 组装内容
|
|
149
|
+
const content = `${imports}\n\n${testBlocks.join('\n\n')}\n`;
|
|
150
|
+
return content;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 生成导入语句
|
|
154
|
+
*/
|
|
155
|
+
function generateImports(exports, importPath, usesTypeScript) {
|
|
156
|
+
if (exports.length === 0) {
|
|
157
|
+
return usesTypeScript
|
|
158
|
+
? `import * as target from '../../${importPath}';`
|
|
159
|
+
: `const target = require('../../${importPath}');`;
|
|
160
|
+
}
|
|
161
|
+
return usesTypeScript
|
|
162
|
+
? `import { ${exports.join(', ')} } from '../../${importPath}';`
|
|
163
|
+
: `const { ${exports.join(', ')} } = require('../../${importPath}');`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 生成测试块
|
|
167
|
+
*/
|
|
168
|
+
function generateTestBlock(exportName, framework, usesDescribeIt, fileType) {
|
|
169
|
+
const useDescribe = usesDescribeIt ?? true;
|
|
170
|
+
const describeOrTest = usesDescribeIt ? 'describe' : 'test';
|
|
171
|
+
if (useDescribe) {
|
|
172
|
+
return `describe('${getDescribeTitle(exportName, fileType)}', () => {
|
|
173
|
+
it('should work correctly', () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const input = {};
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
const result = ${exportName}(input);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(result).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle edge cases', () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
const input = null;
|
|
187
|
+
|
|
188
|
+
// Act & Assert
|
|
189
|
+
expect(() => ${exportName}(input)).not.toThrow();
|
|
190
|
+
});
|
|
191
|
+
});`;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
return `test('${exportName}', () => {
|
|
195
|
+
expect(${exportName}).toBeDefined();
|
|
196
|
+
expect(${exportName}(null)).toBeDefined();
|
|
197
|
+
});`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 获取 describe 标题
|
|
202
|
+
*/
|
|
203
|
+
function getDescribeTitle(exportName, fileType) {
|
|
204
|
+
const typeLabels = {
|
|
205
|
+
component: 'Component',
|
|
206
|
+
service: 'Service',
|
|
207
|
+
util: 'Util',
|
|
208
|
+
module: 'Module',
|
|
209
|
+
class: 'Class',
|
|
210
|
+
function: 'Function'
|
|
211
|
+
};
|
|
212
|
+
const label = typeLabels[fileType] || 'Module';
|
|
213
|
+
return `${label}: ${exportName}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 生成测试用例描述
|
|
217
|
+
*/
|
|
218
|
+
function generateTestCaseDescriptions(source, testTypes) {
|
|
219
|
+
const cases = [];
|
|
220
|
+
let caseId = 0;
|
|
221
|
+
for (const exportName of source.exports) {
|
|
222
|
+
for (const testType of testTypes) {
|
|
223
|
+
cases.push({
|
|
224
|
+
id: `${source.path}-${exportName}-${caseId++}`,
|
|
225
|
+
name: `${exportName} - ${testType} test`,
|
|
226
|
+
type: testType,
|
|
227
|
+
description: `Test ${exportName} from ${source.path}`,
|
|
228
|
+
filePath: source.path,
|
|
229
|
+
sourceFile: source.path,
|
|
230
|
+
target: exportName,
|
|
231
|
+
priority: 'P2',
|
|
232
|
+
steps: [
|
|
233
|
+
{ step: 1, action: `Prepare test input for ${exportName}` },
|
|
234
|
+
{ step: 2, action: `Call ${exportName} with prepared input` },
|
|
235
|
+
{ step: 3, action: 'Verify result matches expected output' }
|
|
236
|
+
],
|
|
237
|
+
expectedResults: ['Function returns expected value', 'No errors thrown']
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return cases;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 生成 Mock 文件
|
|
245
|
+
*/
|
|
246
|
+
function generateMockFiles(sources, config, projectRoot) {
|
|
247
|
+
const mocks = [];
|
|
248
|
+
const mockDir = path.join(config.outputDir, '__mocks__');
|
|
249
|
+
for (const source of sources) {
|
|
250
|
+
// 为有外部依赖的文件生成 Mock
|
|
251
|
+
if (source.fileType === 'service' || source.fileType === 'module') {
|
|
252
|
+
const mockPath = path.join(mockDir, path.basename(source.path).replace(/\.(ts|js)$/, '.mock.ts'));
|
|
253
|
+
const mockContent = generateMockContent(source, config);
|
|
254
|
+
mocks.push({
|
|
255
|
+
path: mockPath,
|
|
256
|
+
content: mockContent,
|
|
257
|
+
type: 'module',
|
|
258
|
+
description: `Mock for ${source.path}`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return mocks;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 生成 Mock 内容
|
|
266
|
+
*/
|
|
267
|
+
function generateMockContent(source, config) {
|
|
268
|
+
const mockExports = source.exports.map(exp => {
|
|
269
|
+
return `export const ${exp} = vi.fn(() => ({
|
|
270
|
+
// Default mock implementation
|
|
271
|
+
data: null,
|
|
272
|
+
error: null
|
|
273
|
+
}));`;
|
|
274
|
+
});
|
|
275
|
+
return `// Mock for ${source.path}
|
|
276
|
+
import { vi } from 'vitest';
|
|
277
|
+
|
|
278
|
+
${mockExports.join('\n\n')}
|
|
279
|
+
|
|
280
|
+
// Reset all mocks before each test
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
vi.clearAllMocks();
|
|
283
|
+
});
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 生成 E2E 测试文件
|
|
288
|
+
*/
|
|
289
|
+
function generateE2ETestFiles(scanResult, config, uiComponents) {
|
|
290
|
+
const files = [];
|
|
291
|
+
if (!config.includeUI)
|
|
292
|
+
return files;
|
|
293
|
+
const e2eDir = 'tests/e2e';
|
|
294
|
+
for (const component of uiComponents) {
|
|
295
|
+
const testPath = path.join(e2eDir, `${component.exports[0] || 'component'}.spec.ts`);
|
|
296
|
+
const content = generateE2EContent(component, config.framework);
|
|
297
|
+
files.push({
|
|
298
|
+
path: testPath,
|
|
299
|
+
content,
|
|
300
|
+
type: 'e2e',
|
|
301
|
+
sourceFile: component.path,
|
|
302
|
+
testCaseIds: [`e2e-${component.path}`],
|
|
303
|
+
overwrites: false
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* 生成 E2E 测试内容
|
|
310
|
+
*/
|
|
311
|
+
function generateE2EContent(component, framework) {
|
|
312
|
+
if (framework === 'playwright') {
|
|
313
|
+
return `// E2E test for ${component.path}
|
|
314
|
+
import { test, expect } from '@playwright/test';
|
|
315
|
+
|
|
316
|
+
test.describe('${component.exports[0] || 'Component'} E2E', () => {
|
|
317
|
+
test.beforeEach(async ({ page }) => {
|
|
318
|
+
await page.goto('/');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('should render correctly', async ({ page }) => {
|
|
322
|
+
// Navigate to component page
|
|
323
|
+
await page.waitForSelector('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]');
|
|
324
|
+
|
|
325
|
+
// Take screenshot
|
|
326
|
+
await page.screenshot({ path: 'screenshots/${component.exports[0]?.toLowerCase() || 'component'}.png' });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('should handle user interaction', async ({ page }) => {
|
|
330
|
+
const element = await page.locator('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]');
|
|
331
|
+
await element.click();
|
|
332
|
+
|
|
333
|
+
// Verify state change
|
|
334
|
+
await expect(element).toHaveAttribute('data-active', 'true');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
// Default Cypress style
|
|
340
|
+
return `// E2E test for ${component.path}
|
|
341
|
+
describe('${component.exports[0] || 'Component'} E2E', () => {
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
cy.visit('/');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should render correctly', () => {
|
|
347
|
+
cy.get('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]')
|
|
348
|
+
.should('be.visible');
|
|
349
|
+
|
|
350
|
+
cy.screenshot('${component.exports[0]?.toLowerCase() || 'component'}');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle user interaction', () => {
|
|
354
|
+
cy.get('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]')
|
|
355
|
+
.click()
|
|
356
|
+
.should('have.attr', 'data-active', 'true');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 写入生成的测试文件
|
|
363
|
+
*/
|
|
364
|
+
function writeGeneratedFiles(files, mockFiles, projectRoot) {
|
|
365
|
+
// 写入测试文件
|
|
366
|
+
for (const file of files) {
|
|
367
|
+
const fullPath = path.join(projectRoot, file.path);
|
|
368
|
+
const dir = path.dirname(fullPath);
|
|
369
|
+
if (!fs.existsSync(dir)) {
|
|
370
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
if (file.overwrites || !fs.existsSync(fullPath)) {
|
|
373
|
+
fs.writeFileSync(fullPath, file.content, 'utf-8');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 写入 Mock 文件
|
|
377
|
+
for (const mock of mockFiles) {
|
|
378
|
+
const fullPath = path.join(projectRoot, mock.path);
|
|
379
|
+
const dir = path.dirname(fullPath);
|
|
380
|
+
if (!fs.existsSync(dir)) {
|
|
381
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
fs.writeFileSync(fullPath, mock.content, 'utf-8');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 生成测试文件(简化接口)
|
|
388
|
+
*/
|
|
389
|
+
function generateTests(projectRoot, scanResult, targetFiles) {
|
|
390
|
+
// 确定要生成测试的源文件
|
|
391
|
+
const sources = targetFiles
|
|
392
|
+
? scanResult.uncoveredSources.filter(s => targetFiles.includes(s.path))
|
|
393
|
+
: scanResult.uncoveredSources;
|
|
394
|
+
// 默认配置
|
|
395
|
+
const config = {
|
|
396
|
+
target: scanResult.target,
|
|
397
|
+
testTypes: ['unit'],
|
|
398
|
+
framework: scanResult.frameworks.find(f => f.isPrimary)?.framework || 'vitest',
|
|
399
|
+
includeUI: scanResult.isFrontend && scanResult.hasUIComponents,
|
|
400
|
+
includeMocks: true
|
|
401
|
+
};
|
|
402
|
+
return generateTestFiles(scanResult, config, sources);
|
|
403
|
+
}
|