pmp-gywd 3.3.0
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/LICENSE +27 -0
- package/README.md +567 -0
- package/bin/install.js +348 -0
- package/commands/gywd/add-phase.md +207 -0
- package/commands/gywd/anticipate.md +271 -0
- package/commands/gywd/bootstrap.md +336 -0
- package/commands/gywd/challenge.md +344 -0
- package/commands/gywd/check-drift.md +144 -0
- package/commands/gywd/complete-milestone.md +106 -0
- package/commands/gywd/consider-issues.md +202 -0
- package/commands/gywd/context.md +93 -0
- package/commands/gywd/create-roadmap.md +115 -0
- package/commands/gywd/deps.md +169 -0
- package/commands/gywd/digest.md +138 -0
- package/commands/gywd/discuss-milestone.md +47 -0
- package/commands/gywd/discuss-phase.md +60 -0
- package/commands/gywd/execute-plan.md +161 -0
- package/commands/gywd/extract-decisions.md +325 -0
- package/commands/gywd/health.md +150 -0
- package/commands/gywd/help.md +556 -0
- package/commands/gywd/history.md +278 -0
- package/commands/gywd/impact.md +317 -0
- package/commands/gywd/init.md +95 -0
- package/commands/gywd/insert-phase.md +227 -0
- package/commands/gywd/list-phase-assumptions.md +50 -0
- package/commands/gywd/map-codebase.md +84 -0
- package/commands/gywd/memory.md +159 -0
- package/commands/gywd/new-milestone.md +59 -0
- package/commands/gywd/new-project.md +315 -0
- package/commands/gywd/pause-work.md +123 -0
- package/commands/gywd/plan-fix.md +205 -0
- package/commands/gywd/plan-phase.md +93 -0
- package/commands/gywd/preview-plan.md +139 -0
- package/commands/gywd/profile.md +363 -0
- package/commands/gywd/progress.md +317 -0
- package/commands/gywd/remove-phase.md +338 -0
- package/commands/gywd/research-phase.md +91 -0
- package/commands/gywd/resume-work.md +40 -0
- package/commands/gywd/rollback.md +179 -0
- package/commands/gywd/status.md +42 -0
- package/commands/gywd/sync-github.md +234 -0
- package/commands/gywd/verify-work.md +71 -0
- package/commands/gywd/why.md +251 -0
- package/docs/COMMANDS.md +722 -0
- package/docs/CONTRIBUTING.md +342 -0
- package/docs/EXAMPLES.md +535 -0
- package/docs/GETTING-STARTED.md +262 -0
- package/docs/README.md +55 -0
- package/docs/RELEASING.md +159 -0
- package/get-your-work-done/core/agent-patterns.md +331 -0
- package/get-your-work-done/core/architecture.md +334 -0
- package/get-your-work-done/core/context-model-schema.json +154 -0
- package/get-your-work-done/core/decisions-schema.json +193 -0
- package/get-your-work-done/core/learning-state-schema.json +133 -0
- package/get-your-work-done/core/profile-schema.json +257 -0
- package/get-your-work-done/references/adaptive-decomposition.md +175 -0
- package/get-your-work-done/references/checkpoints.md +287 -0
- package/get-your-work-done/references/confidence-scoring.md +169 -0
- package/get-your-work-done/references/continuation-format.md +255 -0
- package/get-your-work-done/references/git-integration.md +254 -0
- package/get-your-work-done/references/plan-format.md +428 -0
- package/get-your-work-done/references/principles.md +157 -0
- package/get-your-work-done/references/questioning.md +162 -0
- package/get-your-work-done/references/research-pitfalls.md +215 -0
- package/get-your-work-done/references/scope-estimation.md +172 -0
- package/get-your-work-done/references/tdd.md +263 -0
- package/get-your-work-done/templates/codebase/architecture.md +255 -0
- package/get-your-work-done/templates/codebase/concerns.md +310 -0
- package/get-your-work-done/templates/codebase/conventions.md +307 -0
- package/get-your-work-done/templates/codebase/integrations.md +280 -0
- package/get-your-work-done/templates/codebase/stack.md +186 -0
- package/get-your-work-done/templates/codebase/structure.md +285 -0
- package/get-your-work-done/templates/codebase/testing.md +480 -0
- package/get-your-work-done/templates/config.json +18 -0
- package/get-your-work-done/templates/context.md +161 -0
- package/get-your-work-done/templates/continue-here.md +78 -0
- package/get-your-work-done/templates/discovery.md +146 -0
- package/get-your-work-done/templates/issues.md +32 -0
- package/get-your-work-done/templates/milestone-archive.md +123 -0
- package/get-your-work-done/templates/milestone-context.md +93 -0
- package/get-your-work-done/templates/milestone.md +115 -0
- package/get-your-work-done/templates/phase-prompt.md +303 -0
- package/get-your-work-done/templates/project.md +184 -0
- package/get-your-work-done/templates/research.md +529 -0
- package/get-your-work-done/templates/roadmap.md +196 -0
- package/get-your-work-done/templates/state.md +210 -0
- package/get-your-work-done/templates/summary.md +273 -0
- package/get-your-work-done/templates/uat-issues.md +143 -0
- package/get-your-work-done/workflows/complete-milestone.md +643 -0
- package/get-your-work-done/workflows/create-milestone.md +416 -0
- package/get-your-work-done/workflows/create-roadmap.md +481 -0
- package/get-your-work-done/workflows/discovery-phase.md +293 -0
- package/get-your-work-done/workflows/discuss-milestone.md +236 -0
- package/get-your-work-done/workflows/discuss-phase.md +247 -0
- package/get-your-work-done/workflows/execute-phase.md +1625 -0
- package/get-your-work-done/workflows/list-phase-assumptions.md +178 -0
- package/get-your-work-done/workflows/map-codebase.md +434 -0
- package/get-your-work-done/workflows/plan-phase.md +488 -0
- package/get-your-work-done/workflows/research-phase.md +436 -0
- package/get-your-work-done/workflows/resume-project.md +287 -0
- package/get-your-work-done/workflows/transition.md +580 -0
- package/get-your-work-done/workflows/verify-work.md +202 -0
- package/lib/automation/dependency-analyzer.js +635 -0
- package/lib/automation/doc-generator.js +643 -0
- package/lib/automation/index.js +42 -0
- package/lib/automation/test-generator.js +628 -0
- package/lib/context/context-analyzer.js +554 -0
- package/lib/context/context-cache.js +426 -0
- package/lib/context/context-predictor.js +622 -0
- package/lib/context/index.js +44 -0
- package/lib/memory/confidence-calibrator.js +484 -0
- package/lib/memory/feedback-collector.js +551 -0
- package/lib/memory/global-memory.js +465 -0
- package/lib/memory/index.js +75 -0
- package/lib/memory/pattern-aggregator.js +487 -0
- package/lib/memory/team-sync.js +501 -0
- package/lib/profile/index.js +24 -0
- package/lib/profile/pattern-learner.js +303 -0
- package/lib/profile/profile-manager.js +445 -0
- package/lib/questioning/index.js +49 -0
- package/lib/questioning/question-engine.js +311 -0
- package/lib/questioning/question-templates.js +315 -0
- package/lib/validators/command-validator.js +188 -0
- package/lib/validators/index.js +29 -0
- package/lib/validators/schema-validator.js +183 -0
- package/package.json +61 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Generator
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates test stubs based on source code analysis.
|
|
5
|
+
* Detects exported functions, classes, and methods to create
|
|
6
|
+
* comprehensive test scaffolding.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Export detection patterns (reserved for future use)
|
|
14
|
+
*/
|
|
15
|
+
const _EXPORT_PATTERNS = {
|
|
16
|
+
// module.exports = { func1, func2 }
|
|
17
|
+
moduleExportsObject: /module\.exports\s*=\s*\{([^}]+)\}/g,
|
|
18
|
+
// module.exports.name = ...
|
|
19
|
+
moduleExportsProperty: /module\.exports\.(\w+)\s*=/g,
|
|
20
|
+
// module.exports = ClassName
|
|
21
|
+
moduleExportsSingle: /module\.exports\s*=\s*(\w+)/g,
|
|
22
|
+
// exports.name = ...
|
|
23
|
+
exportsProperty: /exports\.(\w+)\s*=/g,
|
|
24
|
+
// export function name() {}
|
|
25
|
+
exportFunction: /export\s+(?:async\s+)?function\s+(\w+)/g,
|
|
26
|
+
// export const/let/var name = ...
|
|
27
|
+
exportConst: /export\s+(?:const|let|var)\s+(\w+)/g,
|
|
28
|
+
// export class Name {}
|
|
29
|
+
exportClass: /export\s+class\s+(\w+)/g,
|
|
30
|
+
// export default ...
|
|
31
|
+
exportDefault: /export\s+default\s+(?:class|function)?\s*(\w+)?/g,
|
|
32
|
+
// export { name1, name2 }
|
|
33
|
+
exportNamed: /export\s*\{([^}]+)\}/g,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Function/class detection patterns (reserved for future use)
|
|
38
|
+
*/
|
|
39
|
+
const _DEFINITION_PATTERNS = {
|
|
40
|
+
// function name(params) {}
|
|
41
|
+
functionDecl: /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g,
|
|
42
|
+
// const name = function(params) {}
|
|
43
|
+
functionExpr: /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)/g,
|
|
44
|
+
// const name = (params) => {}
|
|
45
|
+
arrowFunction: /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g,
|
|
46
|
+
// class Name {}
|
|
47
|
+
classDecl: /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g,
|
|
48
|
+
// methodName(params) {} inside class
|
|
49
|
+
classMethod: /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/gm,
|
|
50
|
+
// static methodName(params) {}
|
|
51
|
+
staticMethod: /^\s*static\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/gm,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Test frameworks
|
|
56
|
+
*/
|
|
57
|
+
const TEST_FRAMEWORKS = {
|
|
58
|
+
JEST: 'jest',
|
|
59
|
+
MOCHA: 'mocha',
|
|
60
|
+
NODE_TEST: 'node:test',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Test Generator class
|
|
65
|
+
*/
|
|
66
|
+
class TestGenerator {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.framework = options.framework || TEST_FRAMEWORKS.JEST;
|
|
69
|
+
this.testDir = options.testDir || 'tests';
|
|
70
|
+
this.sourceDir = options.sourceDir || 'lib';
|
|
71
|
+
this.includeSnapshots = options.includeSnapshots || false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Analyze a source file and extract testable exports
|
|
76
|
+
* @param {string} filePath - Path to source file
|
|
77
|
+
* @returns {object} Analysis results
|
|
78
|
+
*/
|
|
79
|
+
analyzeFile(filePath) {
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
83
|
+
} catch {
|
|
84
|
+
return { error: `Could not read file: ${filePath}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const exports = this.extractExports(content);
|
|
88
|
+
const functions = this.extractFunctions(content);
|
|
89
|
+
const classes = this.extractClasses(content);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
filePath,
|
|
93
|
+
exports,
|
|
94
|
+
functions,
|
|
95
|
+
classes,
|
|
96
|
+
testable: this.identifyTestable(exports, functions, classes),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract exported names from content
|
|
102
|
+
* @param {string} content - File content
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
extractExports(content) {
|
|
106
|
+
const exports = new Set();
|
|
107
|
+
|
|
108
|
+
// module.exports = { a, b, c }
|
|
109
|
+
const objMatch = content.match(/module\.exports\s*=\s*\{([^}]+)\}/);
|
|
110
|
+
if (objMatch) {
|
|
111
|
+
const names = objMatch[1].split(',').map(n => n.trim().split(':')[0].trim());
|
|
112
|
+
names.forEach(n => exports.add(n));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// module.exports.name = ...
|
|
116
|
+
for (const match of content.matchAll(/module\.exports\.(\w+)\s*=/g)) {
|
|
117
|
+
exports.add(match[1]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// module.exports = SingleExport
|
|
121
|
+
const singleMatch = content.match(/module\.exports\s*=\s*(\w+)\s*;?\s*$/m);
|
|
122
|
+
if (singleMatch && !singleMatch[0].includes('{')) {
|
|
123
|
+
exports.add(singleMatch[1]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// exports.name = ...
|
|
127
|
+
for (const match of content.matchAll(/exports\.(\w+)\s*=/g)) {
|
|
128
|
+
exports.add(match[1]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// export function/class/const
|
|
132
|
+
for (const match of content.matchAll(/export\s+(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/g)) {
|
|
133
|
+
exports.add(match[1]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// export { a, b, c }
|
|
137
|
+
for (const match of content.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
138
|
+
const names = match[1].split(',').map(n => {
|
|
139
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
140
|
+
return parts[parts.length - 1].trim();
|
|
141
|
+
});
|
|
142
|
+
names.forEach(n => exports.add(n));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// export default
|
|
146
|
+
const defaultMatch = content.match(/export\s+default\s+(?:class|function)?\s*(\w+)/);
|
|
147
|
+
if (defaultMatch && defaultMatch[1]) {
|
|
148
|
+
exports.add(defaultMatch[1]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Array.from(exports).filter(e => e && e !== 'undefined');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Extract function definitions
|
|
156
|
+
* @param {string} content - File content
|
|
157
|
+
* @returns {Array<{name: string, params: string[], async: boolean}>}
|
|
158
|
+
*/
|
|
159
|
+
extractFunctions(content) {
|
|
160
|
+
const functions = [];
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
|
|
163
|
+
// Regular function declarations
|
|
164
|
+
for (const match of content.matchAll(/(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g)) {
|
|
165
|
+
if (!seen.has(match[1])) {
|
|
166
|
+
seen.add(match[1]);
|
|
167
|
+
functions.push({
|
|
168
|
+
name: match[1],
|
|
169
|
+
params: this.parseParams(match[2]),
|
|
170
|
+
async: match[0].includes('async'),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Function expressions (with function keyword)
|
|
176
|
+
const funcExprPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?function\s*\(([^)]*)\)/g;
|
|
177
|
+
for (const match of content.matchAll(funcExprPattern)) {
|
|
178
|
+
if (!seen.has(match[1])) {
|
|
179
|
+
seen.add(match[1]);
|
|
180
|
+
functions.push({
|
|
181
|
+
name: match[1],
|
|
182
|
+
params: this.parseParams(match[3]),
|
|
183
|
+
async: !!match[2],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Arrow functions
|
|
189
|
+
const arrowPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>/g;
|
|
190
|
+
for (const match of content.matchAll(arrowPattern)) {
|
|
191
|
+
if (!seen.has(match[1])) {
|
|
192
|
+
seen.add(match[1]);
|
|
193
|
+
functions.push({
|
|
194
|
+
name: match[1],
|
|
195
|
+
params: this.parseParams(match[3]),
|
|
196
|
+
async: !!match[2],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return functions;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Extract class definitions with methods
|
|
206
|
+
* @param {string} content - File content
|
|
207
|
+
* @returns {Array<{name: string, methods: Array}>}
|
|
208
|
+
*/
|
|
209
|
+
extractClasses(content) {
|
|
210
|
+
const classes = [];
|
|
211
|
+
|
|
212
|
+
// Find class declarations
|
|
213
|
+
const classRegex = /class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{/g;
|
|
214
|
+
let classMatch;
|
|
215
|
+
|
|
216
|
+
while ((classMatch = classRegex.exec(content)) !== null) {
|
|
217
|
+
const className = classMatch[1];
|
|
218
|
+
const extendsClass = classMatch[2] || null;
|
|
219
|
+
const classStart = classMatch.index + classMatch[0].length;
|
|
220
|
+
|
|
221
|
+
// Find matching closing brace
|
|
222
|
+
let braceCount = 1;
|
|
223
|
+
let classEnd = classStart;
|
|
224
|
+
|
|
225
|
+
for (let i = classStart; i < content.length && braceCount > 0; i++) {
|
|
226
|
+
if (content[i] === '{') braceCount++;
|
|
227
|
+
if (content[i] === '}') braceCount--;
|
|
228
|
+
classEnd = i;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const classBody = content.slice(classStart, classEnd);
|
|
232
|
+
const methods = this.extractMethods(classBody);
|
|
233
|
+
|
|
234
|
+
classes.push({
|
|
235
|
+
name: className,
|
|
236
|
+
extends: extendsClass,
|
|
237
|
+
methods,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return classes;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extract methods from class body
|
|
246
|
+
* @param {string} classBody - Class body content
|
|
247
|
+
* @returns {Array<{name: string, params: string[], static: boolean, async: boolean}>}
|
|
248
|
+
*/
|
|
249
|
+
extractMethods(classBody) {
|
|
250
|
+
const methods = [];
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
|
|
253
|
+
// Match method patterns
|
|
254
|
+
const methodRegex = /^\s*(static\s+)?(async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/gm;
|
|
255
|
+
let match;
|
|
256
|
+
|
|
257
|
+
while ((match = methodRegex.exec(classBody)) !== null) {
|
|
258
|
+
const name = match[3];
|
|
259
|
+
|
|
260
|
+
// Skip constructor and private methods
|
|
261
|
+
if (name === 'constructor' || name.startsWith('_') || name.startsWith('#')) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!seen.has(name)) {
|
|
266
|
+
seen.add(name);
|
|
267
|
+
methods.push({
|
|
268
|
+
name,
|
|
269
|
+
params: this.parseParams(match[4]),
|
|
270
|
+
static: !!match[1],
|
|
271
|
+
async: !!match[2],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return methods;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Parse parameter string into array
|
|
281
|
+
* @param {string} paramStr - Parameter string
|
|
282
|
+
* @returns {string[]}
|
|
283
|
+
*/
|
|
284
|
+
parseParams(paramStr) {
|
|
285
|
+
if (!paramStr || !paramStr.trim()) return [];
|
|
286
|
+
|
|
287
|
+
return paramStr
|
|
288
|
+
.split(',')
|
|
289
|
+
.map(p => p.trim())
|
|
290
|
+
.filter(p => p)
|
|
291
|
+
.map(p => {
|
|
292
|
+
// Handle default values and destructuring
|
|
293
|
+
const name = p.split('=')[0].trim();
|
|
294
|
+
return name.replace(/[{}[\]]/g, '').trim();
|
|
295
|
+
})
|
|
296
|
+
.filter(p => p);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Identify testable items from analysis
|
|
301
|
+
* @param {string[]} exports - Exported names
|
|
302
|
+
* @param {Array} functions - Function definitions
|
|
303
|
+
* @param {Array} classes - Class definitions
|
|
304
|
+
* @returns {Array}
|
|
305
|
+
*/
|
|
306
|
+
identifyTestable(exports, functions, classes) {
|
|
307
|
+
const testable = [];
|
|
308
|
+
const exportSet = new Set(exports);
|
|
309
|
+
|
|
310
|
+
// Add exported functions
|
|
311
|
+
for (const func of functions) {
|
|
312
|
+
if (exportSet.has(func.name)) {
|
|
313
|
+
testable.push({
|
|
314
|
+
type: 'function',
|
|
315
|
+
...func,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Add exported classes
|
|
321
|
+
for (const cls of classes) {
|
|
322
|
+
if (exportSet.has(cls.name)) {
|
|
323
|
+
testable.push({
|
|
324
|
+
type: 'class',
|
|
325
|
+
...cls,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return testable;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Generate test file content
|
|
335
|
+
* @param {string} sourcePath - Source file path
|
|
336
|
+
* @param {object} analysis - Analysis results (optional)
|
|
337
|
+
* @returns {string}
|
|
338
|
+
*/
|
|
339
|
+
generateTestContent(sourcePath, analysis = null) {
|
|
340
|
+
if (!analysis) {
|
|
341
|
+
analysis = this.analyzeFile(sourcePath);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (analysis.error) {
|
|
345
|
+
return `// Error: ${analysis.error}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const relativePath = this.getRelativeImportPath(sourcePath);
|
|
349
|
+
const fileName = path.basename(sourcePath, path.extname(sourcePath));
|
|
350
|
+
|
|
351
|
+
const lines = [
|
|
352
|
+
'/**',
|
|
353
|
+
` * Tests for ${fileName}`,
|
|
354
|
+
' * Auto-generated test stub',
|
|
355
|
+
' */',
|
|
356
|
+
'',
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
// Add imports based on framework
|
|
360
|
+
if (this.framework === TEST_FRAMEWORKS.JEST) {
|
|
361
|
+
lines.push(`const {`);
|
|
362
|
+
for (const item of analysis.testable) {
|
|
363
|
+
lines.push(` ${item.name},`);
|
|
364
|
+
}
|
|
365
|
+
lines.push(`} = require('${relativePath}');`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
} else if (this.framework === TEST_FRAMEWORKS.MOCHA) {
|
|
368
|
+
lines.push(`const { expect } = require('chai');`);
|
|
369
|
+
lines.push(`const {`);
|
|
370
|
+
for (const item of analysis.testable) {
|
|
371
|
+
lines.push(` ${item.name},`);
|
|
372
|
+
}
|
|
373
|
+
lines.push(`} = require('${relativePath}');`);
|
|
374
|
+
lines.push('');
|
|
375
|
+
} else if (this.framework === TEST_FRAMEWORKS.NODE_TEST) {
|
|
376
|
+
lines.push(`const { describe, it } = require('node:test');`);
|
|
377
|
+
lines.push(`const assert = require('node:assert');`);
|
|
378
|
+
lines.push(`const {`);
|
|
379
|
+
for (const item of analysis.testable) {
|
|
380
|
+
lines.push(` ${item.name},`);
|
|
381
|
+
}
|
|
382
|
+
lines.push(`} = require('${relativePath}');`);
|
|
383
|
+
lines.push('');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Generate test blocks
|
|
387
|
+
for (const item of analysis.testable) {
|
|
388
|
+
if (item.type === 'function') {
|
|
389
|
+
lines.push(...this.generateFunctionTests(item));
|
|
390
|
+
} else if (item.type === 'class') {
|
|
391
|
+
lines.push(...this.generateClassTests(item));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Generate tests for a function
|
|
400
|
+
* @param {object} func - Function definition
|
|
401
|
+
* @returns {string[]}
|
|
402
|
+
*/
|
|
403
|
+
generateFunctionTests(func) {
|
|
404
|
+
const lines = [
|
|
405
|
+
`describe('${func.name}', () => {`,
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
if (func.async) {
|
|
409
|
+
lines.push(` test('should handle async operation', async () => {`);
|
|
410
|
+
if (func.params.length > 0) {
|
|
411
|
+
lines.push(` // TODO: Provide test arguments`);
|
|
412
|
+
lines.push(` const result = await ${func.name}(/* ${func.params.join(', ')} */);`);
|
|
413
|
+
} else {
|
|
414
|
+
lines.push(` const result = await ${func.name}();`);
|
|
415
|
+
}
|
|
416
|
+
lines.push(` expect(result).toBeDefined();`);
|
|
417
|
+
lines.push(` });`);
|
|
418
|
+
} else {
|
|
419
|
+
lines.push(` test('should return expected result', () => {`);
|
|
420
|
+
if (func.params.length > 0) {
|
|
421
|
+
lines.push(` // TODO: Provide test arguments`);
|
|
422
|
+
lines.push(` const result = ${func.name}(/* ${func.params.join(', ')} */);`);
|
|
423
|
+
} else {
|
|
424
|
+
lines.push(` const result = ${func.name}();`);
|
|
425
|
+
}
|
|
426
|
+
lines.push(` expect(result).toBeDefined();`);
|
|
427
|
+
lines.push(` });`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Add edge case stubs
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push(` test('should handle edge cases', () => {`);
|
|
433
|
+
lines.push(` // TODO: Add edge case tests`);
|
|
434
|
+
lines.push(` });`);
|
|
435
|
+
|
|
436
|
+
if (func.params.length > 0) {
|
|
437
|
+
lines.push('');
|
|
438
|
+
lines.push(` test('should handle invalid input', () => {`);
|
|
439
|
+
lines.push(` // TODO: Test with invalid/missing parameters`);
|
|
440
|
+
lines.push(` });`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
lines.push(`});`);
|
|
444
|
+
lines.push('');
|
|
445
|
+
|
|
446
|
+
return lines;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Generate tests for a class
|
|
451
|
+
* @param {object} cls - Class definition
|
|
452
|
+
* @returns {string[]}
|
|
453
|
+
*/
|
|
454
|
+
generateClassTests(cls) {
|
|
455
|
+
const lines = [
|
|
456
|
+
`describe('${cls.name}', () => {`,
|
|
457
|
+
` let instance;`,
|
|
458
|
+
'',
|
|
459
|
+
` beforeEach(() => {`,
|
|
460
|
+
` instance = new ${cls.name}();`,
|
|
461
|
+
` });`,
|
|
462
|
+
'',
|
|
463
|
+
` describe('constructor', () => {`,
|
|
464
|
+
` test('should create instance', () => {`,
|
|
465
|
+
` expect(instance).toBeInstanceOf(${cls.name});`,
|
|
466
|
+
` });`,
|
|
467
|
+
` });`,
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
// Add method tests
|
|
471
|
+
for (const method of cls.methods) {
|
|
472
|
+
lines.push('');
|
|
473
|
+
lines.push(...this.generateMethodTests(method, cls.name));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
lines.push(`});`);
|
|
477
|
+
lines.push('');
|
|
478
|
+
|
|
479
|
+
return lines;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Generate tests for a class method
|
|
484
|
+
* @param {object} method - Method definition
|
|
485
|
+
* @param {string} className - Class name
|
|
486
|
+
* @returns {string[]}
|
|
487
|
+
*/
|
|
488
|
+
generateMethodTests(method, className) {
|
|
489
|
+
const target = method.static ? className : 'instance';
|
|
490
|
+
const lines = [
|
|
491
|
+
` describe('${method.name}', () => {`,
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
if (method.async) {
|
|
495
|
+
lines.push(` test('should handle async operation', async () => {`);
|
|
496
|
+
if (method.params.length > 0) {
|
|
497
|
+
lines.push(` // TODO: Provide test arguments`);
|
|
498
|
+
lines.push(` const result = await ${target}.${method.name}(/* ${method.params.join(', ')} */);`);
|
|
499
|
+
} else {
|
|
500
|
+
lines.push(` const result = await ${target}.${method.name}();`);
|
|
501
|
+
}
|
|
502
|
+
lines.push(` expect(result).toBeDefined();`);
|
|
503
|
+
lines.push(` });`);
|
|
504
|
+
} else {
|
|
505
|
+
lines.push(` test('should return expected result', () => {`);
|
|
506
|
+
if (method.params.length > 0) {
|
|
507
|
+
lines.push(` // TODO: Provide test arguments`);
|
|
508
|
+
lines.push(` const result = ${target}.${method.name}(/* ${method.params.join(', ')} */);`);
|
|
509
|
+
} else {
|
|
510
|
+
lines.push(` const result = ${target}.${method.name}();`);
|
|
511
|
+
}
|
|
512
|
+
lines.push(` expect(result).toBeDefined();`);
|
|
513
|
+
lines.push(` });`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
lines.push(` });`);
|
|
517
|
+
|
|
518
|
+
return lines;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get relative import path for test file
|
|
523
|
+
* @param {string} sourcePath - Source file path
|
|
524
|
+
* @returns {string}
|
|
525
|
+
*/
|
|
526
|
+
getRelativeImportPath(sourcePath) {
|
|
527
|
+
const testPath = this.getTestFilePath(sourcePath);
|
|
528
|
+
const testDir = path.dirname(testPath);
|
|
529
|
+
|
|
530
|
+
let relative = path.relative(testDir, sourcePath);
|
|
531
|
+
if (!relative.startsWith('.')) {
|
|
532
|
+
relative = `./${ relative}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Remove extension for require
|
|
536
|
+
return relative.replace(/\.[^.]+$/, '');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get test file path for a source file
|
|
541
|
+
* @param {string} sourcePath - Source file path
|
|
542
|
+
* @returns {string}
|
|
543
|
+
*/
|
|
544
|
+
getTestFilePath(sourcePath) {
|
|
545
|
+
const ext = path.extname(sourcePath);
|
|
546
|
+
const baseName = path.basename(sourcePath, ext);
|
|
547
|
+
const sourceDir = path.dirname(sourcePath);
|
|
548
|
+
|
|
549
|
+
// Determine test directory structure
|
|
550
|
+
const relativeSrc = sourceDir.replace(this.sourceDir, '').replace(/^[/\\]/, '');
|
|
551
|
+
|
|
552
|
+
return path.join(this.testDir, relativeSrc, `${baseName}.test${ext || '.js'}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Generate test file for a source file
|
|
557
|
+
* @param {string} sourcePath - Source file path
|
|
558
|
+
* @param {boolean} dryRun - If true, don't write file
|
|
559
|
+
* @returns {{path: string, content: string, written: boolean}}
|
|
560
|
+
*/
|
|
561
|
+
generateTestFile(sourcePath, dryRun = false) {
|
|
562
|
+
const analysis = this.analyzeFile(sourcePath);
|
|
563
|
+
const content = this.generateTestContent(sourcePath, analysis);
|
|
564
|
+
const testPath = this.getTestFilePath(sourcePath);
|
|
565
|
+
|
|
566
|
+
const result = {
|
|
567
|
+
path: testPath,
|
|
568
|
+
content,
|
|
569
|
+
written: false,
|
|
570
|
+
analysis,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (!dryRun) {
|
|
574
|
+
const testDir = path.dirname(testPath);
|
|
575
|
+
if (!fs.existsSync(testDir)) {
|
|
576
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
577
|
+
}
|
|
578
|
+
fs.writeFileSync(testPath, content);
|
|
579
|
+
result.written = true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Generate tests for all files in a directory
|
|
587
|
+
* @param {string} dir - Directory to scan
|
|
588
|
+
* @param {boolean} dryRun - If true, don't write files
|
|
589
|
+
* @returns {Array}
|
|
590
|
+
*/
|
|
591
|
+
generateForDirectory(dir, dryRun = false) {
|
|
592
|
+
const results = [];
|
|
593
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs'];
|
|
594
|
+
|
|
595
|
+
const scan = (scanDir) => {
|
|
596
|
+
let entries;
|
|
597
|
+
try {
|
|
598
|
+
entries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
599
|
+
} catch {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
const fullPath = path.join(scanDir, entry.name);
|
|
605
|
+
|
|
606
|
+
if (entry.isDirectory()) {
|
|
607
|
+
if (!['node_modules', '.git', 'test', 'tests', '__tests__'].includes(entry.name)) {
|
|
608
|
+
scan(fullPath);
|
|
609
|
+
}
|
|
610
|
+
} else if (entry.isFile()) {
|
|
611
|
+
const ext = path.extname(entry.name);
|
|
612
|
+
if (extensions.includes(ext) && !entry.name.includes('.test.') && !entry.name.includes('.spec.')) {
|
|
613
|
+
const result = this.generateTestFile(fullPath, dryRun);
|
|
614
|
+
results.push(result);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
scan(dir);
|
|
621
|
+
return results;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
module.exports = {
|
|
626
|
+
TestGenerator,
|
|
627
|
+
TEST_FRAMEWORKS,
|
|
628
|
+
};
|