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.
Files changed (126) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +567 -0
  3. package/bin/install.js +348 -0
  4. package/commands/gywd/add-phase.md +207 -0
  5. package/commands/gywd/anticipate.md +271 -0
  6. package/commands/gywd/bootstrap.md +336 -0
  7. package/commands/gywd/challenge.md +344 -0
  8. package/commands/gywd/check-drift.md +144 -0
  9. package/commands/gywd/complete-milestone.md +106 -0
  10. package/commands/gywd/consider-issues.md +202 -0
  11. package/commands/gywd/context.md +93 -0
  12. package/commands/gywd/create-roadmap.md +115 -0
  13. package/commands/gywd/deps.md +169 -0
  14. package/commands/gywd/digest.md +138 -0
  15. package/commands/gywd/discuss-milestone.md +47 -0
  16. package/commands/gywd/discuss-phase.md +60 -0
  17. package/commands/gywd/execute-plan.md +161 -0
  18. package/commands/gywd/extract-decisions.md +325 -0
  19. package/commands/gywd/health.md +150 -0
  20. package/commands/gywd/help.md +556 -0
  21. package/commands/gywd/history.md +278 -0
  22. package/commands/gywd/impact.md +317 -0
  23. package/commands/gywd/init.md +95 -0
  24. package/commands/gywd/insert-phase.md +227 -0
  25. package/commands/gywd/list-phase-assumptions.md +50 -0
  26. package/commands/gywd/map-codebase.md +84 -0
  27. package/commands/gywd/memory.md +159 -0
  28. package/commands/gywd/new-milestone.md +59 -0
  29. package/commands/gywd/new-project.md +315 -0
  30. package/commands/gywd/pause-work.md +123 -0
  31. package/commands/gywd/plan-fix.md +205 -0
  32. package/commands/gywd/plan-phase.md +93 -0
  33. package/commands/gywd/preview-plan.md +139 -0
  34. package/commands/gywd/profile.md +363 -0
  35. package/commands/gywd/progress.md +317 -0
  36. package/commands/gywd/remove-phase.md +338 -0
  37. package/commands/gywd/research-phase.md +91 -0
  38. package/commands/gywd/resume-work.md +40 -0
  39. package/commands/gywd/rollback.md +179 -0
  40. package/commands/gywd/status.md +42 -0
  41. package/commands/gywd/sync-github.md +234 -0
  42. package/commands/gywd/verify-work.md +71 -0
  43. package/commands/gywd/why.md +251 -0
  44. package/docs/COMMANDS.md +722 -0
  45. package/docs/CONTRIBUTING.md +342 -0
  46. package/docs/EXAMPLES.md +535 -0
  47. package/docs/GETTING-STARTED.md +262 -0
  48. package/docs/README.md +55 -0
  49. package/docs/RELEASING.md +159 -0
  50. package/get-your-work-done/core/agent-patterns.md +331 -0
  51. package/get-your-work-done/core/architecture.md +334 -0
  52. package/get-your-work-done/core/context-model-schema.json +154 -0
  53. package/get-your-work-done/core/decisions-schema.json +193 -0
  54. package/get-your-work-done/core/learning-state-schema.json +133 -0
  55. package/get-your-work-done/core/profile-schema.json +257 -0
  56. package/get-your-work-done/references/adaptive-decomposition.md +175 -0
  57. package/get-your-work-done/references/checkpoints.md +287 -0
  58. package/get-your-work-done/references/confidence-scoring.md +169 -0
  59. package/get-your-work-done/references/continuation-format.md +255 -0
  60. package/get-your-work-done/references/git-integration.md +254 -0
  61. package/get-your-work-done/references/plan-format.md +428 -0
  62. package/get-your-work-done/references/principles.md +157 -0
  63. package/get-your-work-done/references/questioning.md +162 -0
  64. package/get-your-work-done/references/research-pitfalls.md +215 -0
  65. package/get-your-work-done/references/scope-estimation.md +172 -0
  66. package/get-your-work-done/references/tdd.md +263 -0
  67. package/get-your-work-done/templates/codebase/architecture.md +255 -0
  68. package/get-your-work-done/templates/codebase/concerns.md +310 -0
  69. package/get-your-work-done/templates/codebase/conventions.md +307 -0
  70. package/get-your-work-done/templates/codebase/integrations.md +280 -0
  71. package/get-your-work-done/templates/codebase/stack.md +186 -0
  72. package/get-your-work-done/templates/codebase/structure.md +285 -0
  73. package/get-your-work-done/templates/codebase/testing.md +480 -0
  74. package/get-your-work-done/templates/config.json +18 -0
  75. package/get-your-work-done/templates/context.md +161 -0
  76. package/get-your-work-done/templates/continue-here.md +78 -0
  77. package/get-your-work-done/templates/discovery.md +146 -0
  78. package/get-your-work-done/templates/issues.md +32 -0
  79. package/get-your-work-done/templates/milestone-archive.md +123 -0
  80. package/get-your-work-done/templates/milestone-context.md +93 -0
  81. package/get-your-work-done/templates/milestone.md +115 -0
  82. package/get-your-work-done/templates/phase-prompt.md +303 -0
  83. package/get-your-work-done/templates/project.md +184 -0
  84. package/get-your-work-done/templates/research.md +529 -0
  85. package/get-your-work-done/templates/roadmap.md +196 -0
  86. package/get-your-work-done/templates/state.md +210 -0
  87. package/get-your-work-done/templates/summary.md +273 -0
  88. package/get-your-work-done/templates/uat-issues.md +143 -0
  89. package/get-your-work-done/workflows/complete-milestone.md +643 -0
  90. package/get-your-work-done/workflows/create-milestone.md +416 -0
  91. package/get-your-work-done/workflows/create-roadmap.md +481 -0
  92. package/get-your-work-done/workflows/discovery-phase.md +293 -0
  93. package/get-your-work-done/workflows/discuss-milestone.md +236 -0
  94. package/get-your-work-done/workflows/discuss-phase.md +247 -0
  95. package/get-your-work-done/workflows/execute-phase.md +1625 -0
  96. package/get-your-work-done/workflows/list-phase-assumptions.md +178 -0
  97. package/get-your-work-done/workflows/map-codebase.md +434 -0
  98. package/get-your-work-done/workflows/plan-phase.md +488 -0
  99. package/get-your-work-done/workflows/research-phase.md +436 -0
  100. package/get-your-work-done/workflows/resume-project.md +287 -0
  101. package/get-your-work-done/workflows/transition.md +580 -0
  102. package/get-your-work-done/workflows/verify-work.md +202 -0
  103. package/lib/automation/dependency-analyzer.js +635 -0
  104. package/lib/automation/doc-generator.js +643 -0
  105. package/lib/automation/index.js +42 -0
  106. package/lib/automation/test-generator.js +628 -0
  107. package/lib/context/context-analyzer.js +554 -0
  108. package/lib/context/context-cache.js +426 -0
  109. package/lib/context/context-predictor.js +622 -0
  110. package/lib/context/index.js +44 -0
  111. package/lib/memory/confidence-calibrator.js +484 -0
  112. package/lib/memory/feedback-collector.js +551 -0
  113. package/lib/memory/global-memory.js +465 -0
  114. package/lib/memory/index.js +75 -0
  115. package/lib/memory/pattern-aggregator.js +487 -0
  116. package/lib/memory/team-sync.js +501 -0
  117. package/lib/profile/index.js +24 -0
  118. package/lib/profile/pattern-learner.js +303 -0
  119. package/lib/profile/profile-manager.js +445 -0
  120. package/lib/questioning/index.js +49 -0
  121. package/lib/questioning/question-engine.js +311 -0
  122. package/lib/questioning/question-templates.js +315 -0
  123. package/lib/validators/command-validator.js +188 -0
  124. package/lib/validators/index.js +29 -0
  125. package/lib/validators/schema-validator.js +183 -0
  126. 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
+ };