@gnapi/cotester 1.2.4 → 1.2.6

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 (49) hide show
  1. package/dist/analyzer.d.ts +5 -0
  2. package/dist/analyzer.js +7 -2
  3. package/dist/analyzer.js.map +1 -1
  4. package/dist/auditLogger.d.ts +46 -0
  5. package/dist/auditLogger.js +107 -0
  6. package/dist/auditLogger.js.map +1 -0
  7. package/dist/checker.d.ts +3 -1
  8. package/dist/checker.js +1 -0
  9. package/dist/checker.js.map +1 -1
  10. package/dist/cli.js +39 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/configManager.d.ts +27 -0
  13. package/dist/configManager.js +87 -17
  14. package/dist/configManager.js.map +1 -1
  15. package/dist/fileWorker.js +6 -1
  16. package/dist/fileWorker.js.map +1 -1
  17. package/dist/frameworkAdapter.d.ts +51 -0
  18. package/dist/frameworkAdapter.js +77 -0
  19. package/dist/frameworkAdapter.js.map +1 -0
  20. package/dist/generator.d.ts +24 -1
  21. package/dist/generator.js +167 -69
  22. package/dist/generator.js.map +1 -1
  23. package/dist/hooksInstaller.d.ts +1 -0
  24. package/dist/hooksInstaller.js +246 -0
  25. package/dist/hooksInstaller.js.map +1 -0
  26. package/dist/importRepairer.d.ts +22 -0
  27. package/dist/importRepairer.js +226 -0
  28. package/dist/importRepairer.js.map +1 -0
  29. package/dist/interfaceShapeResolver.js +8 -3
  30. package/dist/interfaceShapeResolver.js.map +1 -1
  31. package/dist/migrator.d.ts +49 -0
  32. package/dist/migrator.js +335 -0
  33. package/dist/migrator.js.map +1 -0
  34. package/dist/mockGenerator.js +25 -1
  35. package/dist/mockGenerator.js.map +1 -1
  36. package/dist/reporter.d.ts +10 -0
  37. package/dist/reporter.js +270 -0
  38. package/dist/reporter.js.map +1 -0
  39. package/dist/scenarioEngine.js +4 -1
  40. package/dist/scenarioEngine.js.map +1 -1
  41. package/dist/sensitiveValueDetector.d.ts +62 -0
  42. package/dist/sensitiveValueDetector.js +147 -0
  43. package/dist/sensitiveValueDetector.js.map +1 -0
  44. package/dist/sharedMockRegistry.d.ts +27 -0
  45. package/dist/sharedMockRegistry.js +223 -0
  46. package/dist/sharedMockRegistry.js.map +1 -0
  47. package/dist/watcher.js +10 -1
  48. package/dist/watcher.js.map +1 -1
  49. package/package.json +1 -1
package/dist/generator.js CHANGED
@@ -33,18 +33,27 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.VERSION_MARKER = exports.CURRENT_FILE_VERSION = exports.TESTGEN_MARKER = void 0;
36
37
  exports.generateTestFile = generateTestFile;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const formatter_1 = require("./formatter");
40
41
  const utils_1 = require("./utils");
41
42
  const ormMockGenerator_1 = require("./ormMockGenerator");
43
+ const frameworkAdapter_1 = require("./frameworkAdapter");
42
44
  /**
43
45
  * Marker placed as the very first line of every generated test file.
44
46
  * Its presence lets cotester know the file was scaffolded (not hand-written),
45
47
  * but function-level merging works regardless of whether the marker is present.
46
48
  */
47
- const TESTGEN_MARKER = "// @cotester-generated";
49
+ exports.TESTGEN_MARKER = "// @cotester-generated";
50
+ /**
51
+ * Format version written into the second line of every generated test file.
52
+ * Used by `cotester migrate` to detect files produced by older versions and
53
+ * upgrade them in-place. Increment this when the generated format changes.
54
+ */
55
+ exports.CURRENT_FILE_VERSION = 2;
56
+ exports.VERSION_MARKER = `// @cotester-version: ${exports.CURRENT_FILE_VERSION}`;
48
57
  /**
49
58
  * Generate (or merge into) a test file for the given source file.
50
59
  *
@@ -58,34 +67,37 @@ const TESTGEN_MARKER = "// @cotester-generated";
58
67
  * Individual functions/methods that already have a describe block are never
59
68
  * touched, preserving any hand-written assertions inside them.
60
69
  */
61
- async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0, dryRun = false) {
70
+ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0, dryRun = false, framework = "jest", snapshotForComplexTypes = false) {
62
71
  if (scenarios.length === 0) {
63
72
  return null;
64
73
  }
74
+ const adapter = (0, frameworkAdapter_1.createAdapter)(framework);
65
75
  const absSrcRoot = path.resolve(projectRoot, srcDir);
66
76
  const testFilePath = (0, utils_1.mirrorPath)(srcFilePath, absSrcRoot, path.join(projectRoot, testDir), ".test.ts");
67
77
  // ── Dry-run: print generated content without touching the filesystem ──
68
78
  if (dryRun) {
69
- const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
79
+ const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
70
80
  const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
71
81
  const relTest = path.relative(projectRoot, testFilePath).replace(/\\/g, "/");
72
82
  process.stdout.write(`\n${"─".repeat(60)}\n`);
73
83
  process.stdout.write(`// [dry-run] Test file: ${relTest}\n`);
74
84
  process.stdout.write(`${"─".repeat(60)}\n`);
75
85
  process.stdout.write(formatted);
76
- return testFilePath;
86
+ const allNames = scenarios.map((s) => s.functionName);
87
+ return { testFilePath, added: allNames, merged: [], skipped: [] };
77
88
  }
78
89
  (0, utils_1.ensureDir)(path.dirname(testFilePath));
79
90
  // ── Brand-new file → generate everything ─────────────────────────────
80
91
  if (!fs.existsSync(testFilePath)) {
81
- const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
92
+ const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
82
93
  const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
83
94
  fs.writeFileSync(testFilePath, formatted, "utf-8");
84
95
  (0, utils_1.log)(`Test file → ${path.relative(projectRoot, testFilePath)}`, "success");
85
- return testFilePath;
96
+ const allNames = scenarios.map((s) => s.functionName);
97
+ return { testFilePath, added: allNames, merged: [], skipped: [] };
86
98
  }
87
99
  // ── File exists → function-level merge ───────────────────────────────
88
- return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
100
+ return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
89
101
  }
90
102
  /**
91
103
  * Merge missing function/method describe blocks into an already-existing test file.
@@ -98,11 +110,12 @@ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, tes
98
110
  * 5. For entire new classes → append a full class describe block.
99
111
  * 6. For any new dependencies (jest.mock) not already in the file → prepend them.
100
112
  */
101
- async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
113
+ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
102
114
  let content = fs.readFileSync(testFilePath, "utf-8");
103
115
  const existingDescribes = detectExistingDescribes(content);
104
116
  // ── Feature 6: signature-change detection for already-covered describes ──
105
117
  const existingSigs = parseExistingSignatures(content);
118
+ const sigChangedNames = new Set();
106
119
  for (const s of scenarios) {
107
120
  const key = s.functionName;
108
121
  if (!existingDescribes.has(key))
@@ -118,6 +131,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
118
131
  (0, utils_1.log)(` Signature changed for ${key} — injecting update warning.`, "warn");
119
132
  content = updateSignatureComment(content, key, newSig);
120
133
  content = injectSignatureWarning(content, key, oldSig, newSig);
134
+ sigChangedNames.add(key);
121
135
  }
122
136
  }
123
137
  // ── Classify scenarios ───────────────────────────────────────────────
@@ -140,7 +154,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
140
154
  for (const [className, methods] of classMap) {
141
155
  if (!existingDescribes.has(className)) {
142
156
  // Entire class is new → append full class describe block
143
- toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
157
+ toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
144
158
  newScenarios.push(...methods);
145
159
  }
146
160
  else {
@@ -148,7 +162,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
148
162
  const missingMethods = methods.filter(m => !existingDescribes.has(m.functionName));
149
163
  if (missingMethods.length > 0) {
150
164
  const methodBlocks = missingMethods
151
- .map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold), 1))
165
+ .map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1))
152
166
  .join("\n\n");
153
167
  content = injectIntoClassDescribe(content, className, methodBlocks);
154
168
  newScenarios.push(...missingMethods);
@@ -157,23 +171,29 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
157
171
  }
158
172
  // ── Standalone missing functions ─────────────────────────────────────
159
173
  for (const fn of missingStandalone) {
160
- toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
161
- }
174
+ toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
175
+ }
176
+ // ── Build audit lists ────────────────────────────────────────────────
177
+ const addedNames = newScenarios.map((s) => s.functionName);
178
+ const mergedNames = Array.from(sigChangedNames);
179
+ const skippedNames = scenarios
180
+ .filter((s) => existingDescribes.has(s.functionName) &&
181
+ !sigChangedNames.has(s.functionName))
182
+ .map((s) => s.functionName);
162
183
  if (toAppend.length === 0 && newScenarios.length === 0) {
163
184
  (0, utils_1.log)(` ${path.relative(projectRoot, testFilePath)} — all functions already covered.`, "info");
164
- return testFilePath;
185
+ return { testFilePath, added: [], merged: mergedNames, skipped: skippedNames };
165
186
  }
166
187
  // ── Append standalone / new-class blocks ─────────────────────────────
167
188
  if (toAppend.length > 0) {
168
189
  content = content.trimEnd() + "\n\n" + toAppend.join("\n\n") + "\n";
169
190
  }
170
- // ── Prepend missing jest.mock() / import lines ───────────────────────
171
- content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath);
191
+ // ── Prepend missing mock() / import lines ────────────────────────────
192
+ content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter);
172
193
  const formatted = await (0, formatter_1.formatCode)(content, projectRoot);
173
194
  fs.writeFileSync(testFilePath, formatted, "utf-8");
174
- const addedCount = newScenarios.length;
175
- (0, utils_1.log)(`Test file updated → ${path.relative(projectRoot, testFilePath)} (+${addedCount} function(s))`, "success");
176
- return testFilePath;
195
+ (0, utils_1.log)(`Test file updated → ${path.relative(projectRoot, testFilePath)} (+${addedNames.length} function(s))`, "success");
196
+ return { testFilePath, added: addedNames, merged: mergedNames, skipped: skippedNames };
177
197
  }
178
198
  // ─── Merge helpers ────────────────────────────────────────────────────────────
179
199
  /**
@@ -282,17 +302,16 @@ function injectIntoClassDescribe(content, className, newBlocks) {
282
302
  * are already mocked in the file. If not, prepend the missing jest.mock() calls
283
303
  * and import statements before the first existing import line.
284
304
  */
285
- function addMissingMocks(content, newScenarios, srcFilePath, testFilePath) {
305
+ function addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter = (0, frameworkAdapter_1.createAdapter)("jest")) {
286
306
  const allDeps = collectAllDependencies(newScenarios);
287
307
  const mockLines = [];
288
308
  const importLines = [];
289
309
  for (const dep of allDeps) {
290
310
  const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
291
311
  const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
292
- if (!content.includes(`jest.mock('${depImportPath}'`) &&
293
- !content.includes(`jest.mock("${depImportPath}"`)) {
294
- const mockFns = dep.importedNames.map(n => ` ${n}: jest.fn(),`).join("\n");
295
- mockLines.push(`jest.mock('${depImportPath}', () => ({\n${mockFns}\n}));`);
312
+ const checkStr = adapter.mockModuleCheck(depImportPath);
313
+ if (!content.includes(checkStr) && !content.includes(checkStr.replace("'", '"'))) {
314
+ mockLines.push(adapter.mockModule(depImportPath, dep.importedNames));
296
315
  }
297
316
  const names = dep.importedNames.filter(n => n !== "default");
298
317
  if (names.length > 0) {
@@ -316,24 +335,26 @@ function escapeForRegex(str) {
316
335
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
317
336
  }
318
337
  // ─── Build the full test source ──────────────────────────────────────────────
319
- function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
338
+ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
320
339
  const lines = [];
321
340
  // Marker — presence of this line tells cotester the file is auto-generated
322
341
  // and safe to regenerate. Remove it to protect hand-written edits.
323
- lines.push(TESTGEN_MARKER);
342
+ lines.push(exports.TESTGEN_MARKER);
343
+ lines.push(exports.VERSION_MARKER);
324
344
  lines.push("");
345
+ // ── Framework import (Vitest only; empty for Jest) ───────────────
346
+ const fwImport = adapter.frameworkImport();
347
+ if (fwImport) {
348
+ lines.push(fwImport);
349
+ lines.push("");
350
+ }
325
351
  // ── Collect all unique dependencies across scenarios ─────────────
326
352
  const allDeps = collectAllDependencies(scenarios);
327
- // ── jest.mock() calls (must appear before imports) ───────────────
353
+ // ── mock() calls (must appear before imports) ────────────────────
328
354
  for (const dep of allDeps) {
329
355
  const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
330
356
  const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
331
- const mockFns = dep.importedNames
332
- .map((n) => ` ${n}: jest.fn(),`)
333
- .join("\n");
334
- lines.push(`jest.mock('${depImportPath}', () => ({`);
335
- lines.push(mockFns);
336
- lines.push(`}));`);
357
+ lines.push(adapter.mockModule(depImportPath, dep.importedNames));
337
358
  lines.push("");
338
359
  }
339
360
  // ── Import the source module ────────────────────────────────────
@@ -382,18 +403,18 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
382
403
  }
383
404
  // ── Standalone functions ────────────────────────────────────────
384
405
  for (const fn of standaloneFns) {
385
- lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
406
+ lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
386
407
  lines.push("");
387
408
  }
388
409
  // ── Class methods ───────────────────────────────────────────────
389
410
  for (const [className, methods] of classMap) {
390
- lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
411
+ lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
391
412
  lines.push("");
392
413
  }
393
414
  return lines.join("\n");
394
415
  }
395
416
  // ─── Class describe block with constructor DI ────────────────────────────────
396
- function buildClassDescribe(className, methods, mockResult, tableThreshold = 0) {
417
+ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
397
418
  const lines = [];
398
419
  lines.push(`describe(${JSON.stringify(className)}, () => {`);
399
420
  lines.push(` let instance: ${className};`);
@@ -406,19 +427,21 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
406
427
  // If this constructor param matches the ORM variable, use ORM mocks
407
428
  if (ormUsage && (param.name === ormUsage.variableName || param.type.toLowerCase().includes(ormUsage.kind))) {
408
429
  const ormMocks = (0, ormMockGenerator_1.generateOrmMocks)(ormUsage);
409
- // Add ORM mock if not already declared at file level
410
- if (ormMocks.mockVarNames.length > 0) {
430
+ // Only alias if the file-level ORM var has a DIFFERENT name from the ctor param mock name
431
+ if (ormMocks.mockVarNames.length > 0 && ormMocks.mockVarNames[0] !== mockName) {
411
432
  mockDeclarations.push(` const ${mockName} = ${ormMocks.mockVarNames[0]}; // ORM mock`);
412
433
  }
413
- else {
434
+ else if (ormMocks.mockVarNames.length === 0) {
414
435
  mockDeclarations.push(` const ${mockName} = { } as any; // TODO: add mock methods for ${param.type}`);
415
436
  }
437
+ // else: same name — variable already declared at file level; no re-declaration needed
416
438
  }
417
439
  else {
418
440
  // Generate smart mock based on type name
419
441
  const typeLower = param.type.toLowerCase();
420
442
  if (typeLower.includes("repository") || typeLower.includes("repo")) {
421
- mockDeclarations.push(` const ${mockName} = { find: jest.fn(), findById: jest.fn(), save: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn() } as any;`);
443
+ const fn = adapter.mockFn();
444
+ mockDeclarations.push(` const ${mockName} = { find: ${fn}, findById: ${fn}, save: ${fn}, create: ${fn}, update: ${fn}, delete: ${fn} } as any;`);
422
445
  }
423
446
  else if (typeLower.includes("service")) {
424
447
  mockDeclarations.push(` const ${mockName} = { } as any; // TODO: mock ${param.type} methods`);
@@ -436,7 +459,7 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
436
459
  }
437
460
  lines.push("");
438
461
  lines.push(" beforeEach(() => {");
439
- lines.push(" jest.clearAllMocks();");
462
+ lines.push(` ${adapter.clearAllMocks()}`);
440
463
  if (ctorParams.length > 0) {
441
464
  const args = ctorParams
442
465
  .map((p) => `mock${capitalize(p.name)}`)
@@ -457,66 +480,66 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
457
480
  lines.push(" params,");
458
481
  lines.push(" query,");
459
482
  lines.push(" headers: { 'Content-Type': 'application/json' },");
460
- lines.push(" get: jest.fn(),");
483
+ lines.push(` get: ${adapter.mockFn()},`);
461
484
  lines.push(" } as any);");
462
485
  lines.push("");
463
486
  lines.push(" const mockResponse = () => {");
464
487
  lines.push(" const res: any = {};");
465
- lines.push(" res.status = jest.fn().mockReturnValue(res);");
466
- lines.push(" res.json = jest.fn().mockReturnValue(res);");
467
- lines.push(" res.send = jest.fn().mockReturnValue(res);");
468
- lines.push(" res.set = jest.fn().mockReturnValue(res);");
488
+ lines.push(` res.status = ${adapter.mockFnReturnValue("res")};`);
489
+ lines.push(` res.json = ${adapter.mockFnReturnValue("res")};`);
490
+ lines.push(` res.send = ${adapter.mockFnReturnValue("res")};`);
491
+ lines.push(` res.set = ${adapter.mockFnReturnValue("res")};`);
469
492
  lines.push(" return res;");
470
493
  lines.push(" };");
471
494
  lines.push("");
472
- lines.push(" const mockNext = jest.fn();");
495
+ lines.push(` const mockNext = ${adapter.mockFn()};`);
473
496
  }
474
497
  lines.push("");
475
498
  for (const method of methods) {
476
- lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold), 1));
499
+ lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1));
477
500
  lines.push("");
478
501
  }
479
502
  lines.push("});");
480
503
  return lines.join("\n");
481
504
  }
482
505
  // ─── describe block for a standalone function ────────────────────────────────
483
- function buildFunctionDescribe(fn, mockResult, tableThreshold = 0) {
506
+ function buildFunctionDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
484
507
  const lines = [];
485
508
  lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
486
509
  lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
487
510
  // Add beforeEach with clearAllMocks if this function has dependencies
488
511
  if (fn.dependencies.length > 0) {
489
512
  lines.push(" beforeEach(() => {");
490
- lines.push(" jest.clearAllMocks();");
513
+ lines.push(` ${adapter.clearAllMocks()}`);
491
514
  lines.push(" });");
492
515
  lines.push("");
493
516
  }
494
517
  const constName = mockResult?.constantNames.get(fn.functionName);
495
518
  const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
496
519
  if (tableCases.length > 0) {
497
- lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
520
+ lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
498
521
  lines.push("");
499
522
  }
500
523
  for (const c of individualCases) {
501
- lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
524
+ lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
502
525
  lines.push("");
503
526
  }
504
527
  lines.push("});");
505
528
  return lines.join("\n");
506
529
  }
507
530
  // ─── describe block for a class method ───────────────────────────────────────
508
- function buildMethodDescribe(fn, mockResult, tableThreshold = 0) {
531
+ function buildMethodDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
509
532
  const lines = [];
510
533
  lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
511
534
  lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
512
535
  const constName = mockResult?.constantNames.get(fn.functionName);
513
536
  const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
514
537
  if (tableCases.length > 0) {
515
- lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
538
+ lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
516
539
  lines.push("");
517
540
  }
518
541
  for (const c of individualCases) {
519
- lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
542
+ lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
520
543
  lines.push("");
521
544
  }
522
545
  lines.push("});");
@@ -550,7 +573,7 @@ function partitionCases(cases, paramCount, tableThreshold) {
550
573
  * Emit a `test.each([...])('%s', ...)` block for a group of standard cases.
551
574
  * Each row is `[label, ...argLiterals]` so the Jest title shows the scenario name.
552
575
  */
553
- function buildTableBlock(scenario, tableCases) {
576
+ function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false) {
554
577
  const lines = [];
555
578
  const isAsync = scenario.isAsync;
556
579
  const isMethod = !!scenario.className;
@@ -574,7 +597,7 @@ function buildTableBlock(scenario, tableCases) {
574
597
  lines.push(``);
575
598
  // Assert
576
599
  lines.push(` // Assert`);
577
- const expectLines = buildExpectStubs(scenario.returnType, "");
600
+ const expectLines = buildExpectStubs(scenario.returnType, "", snapshotForComplexTypes);
578
601
  for (const e of expectLines) {
579
602
  lines.push(` ${e}`);
580
603
  }
@@ -582,18 +605,18 @@ function buildTableBlock(scenario, tableCases) {
582
605
  return lines.join("\n");
583
606
  }
584
607
  // ─── Individual test block ───────────────────────────────────────────────────
585
- function buildTestBlock(scenario, testCase, mockConstName) {
608
+ function buildTestBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
586
609
  switch (testCase.kind) {
587
610
  case "throws":
588
611
  return buildThrowsBlock(scenario, testCase, mockConstName);
589
612
  case "rejects":
590
613
  return buildRejectsBlock(scenario, testCase, mockConstName);
591
614
  default:
592
- return buildStandardBlock(scenario, testCase, mockConstName);
615
+ return buildStandardBlock(scenario, testCase, mockConstName, adapter, snapshotForComplexTypes);
593
616
  }
594
617
  }
595
618
  // ─── Standard test block (Arrange / Act / Assert) ────────────────────────────
596
- function buildStandardBlock(scenario, testCase, mockConstName) {
619
+ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
597
620
  const lines = [];
598
621
  const fnName = scenario.functionName;
599
622
  const isAsync = scenario.isAsync;
@@ -603,21 +626,29 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
603
626
  // ── Arrange ──────────────────────────────────────────────────────
604
627
  const caseKey = camelCase(testCase.label);
605
628
  if (isDependencyTest) {
606
- // Emit mockReturnValue / mockResolvedValue so the SUT doesn't receive undefined
629
+ // Arrange: first declare input params from the first mock set's literals
630
+ const hasParams = testCase.paramNames.length > 0 && testCase.argLiterals.length > 0;
631
+ // Then set up mock return values for only the functions actually in importedFunctionMeta
607
632
  const mockSetupLines = [];
608
633
  for (const dep of scenario.dependencies) {
609
634
  for (const meta of dep.importedFunctionMeta) {
610
635
  const mockVal = buildMockReturnValue(meta.returnType);
611
636
  if (meta.isAsync) {
612
- mockSetupLines.push(` (${meta.name} as jest.Mock).mockResolvedValue(${mockVal});`);
637
+ mockSetupLines.push(` ${adapter.mockResolvedValue(meta.name, mockVal)}`);
613
638
  }
614
639
  else {
615
- mockSetupLines.push(` (${meta.name} as jest.Mock).mockReturnValue(${mockVal});`);
640
+ mockSetupLines.push(` ${adapter.mockReturnValue(meta.name, mockVal)}`);
616
641
  }
617
642
  }
618
643
  }
619
- if (mockSetupLines.length > 0) {
644
+ if (hasParams || mockSetupLines.length > 0) {
620
645
  lines.push(" // Arrange");
646
+ // Declare params so the function call compiles
647
+ if (hasParams) {
648
+ for (let i = 0; i < testCase.paramNames.length; i++) {
649
+ lines.push(` const ${testCase.paramNames[i]} = ${testCase.argLiterals[i] ?? "undefined"};`);
650
+ }
651
+ }
621
652
  for (const l of mockSetupLines)
622
653
  lines.push(l);
623
654
  lines.push("");
@@ -655,17 +686,28 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
655
686
  // ── Assert ──────────────────────────────────────────────────────
656
687
  lines.push(" // Assert");
657
688
  if (isDependencyTest) {
658
- // Verify the mocked functions were called
689
+ // Verify only functions that the analyzer confirmed are called (have meta)
690
+ // This avoids asserting on imports the function never uses
659
691
  for (const dep of scenario.dependencies) {
660
- for (const impName of dep.importedNames) {
661
- if (impName !== "default") {
662
- lines.push(` expect(${impName}).toHaveBeenCalled();`);
692
+ const metaList = dep.importedFunctionMeta.length > 0
693
+ ? dep.importedFunctionMeta
694
+ : dep.importedNames
695
+ .filter(n => n !== "default")
696
+ .map(n => ({ name: n, returnType: "unknown", isAsync: false, params: [] }));
697
+ for (const meta of metaList) {
698
+ const depParams = meta.params ?? [];
699
+ if (depParams.length > 0) {
700
+ const callWithArgs = buildCalledWithArgs(depParams, testCase.paramNames, testCase.argLiterals);
701
+ lines.push(` expect(${meta.name}).toHaveBeenCalledWith(${callWithArgs});`);
702
+ }
703
+ else {
704
+ lines.push(` expect(${meta.name}).toHaveBeenCalled();`);
663
705
  }
664
706
  }
665
707
  }
666
708
  }
667
709
  else {
668
- const expectLines = buildExpectStubs(scenario.returnType, testCase.label);
710
+ const expectLines = buildExpectStubs(scenario.returnType, testCase.label, snapshotForComplexTypes);
669
711
  for (const e of expectLines) {
670
712
  lines.push(` ${e}`);
671
713
  }
@@ -774,7 +816,22 @@ function buildMockReturnValue(returnType) {
774
816
  return "{ id: 1 }";
775
817
  }
776
818
  // ─── Expect stubs based on return type ───────────────────────────────────────
777
- function buildExpectStubs(returnType, scenarioLabel) {
819
+ /** Primitive/built-in type names that do NOT warrant snapshot assertions. */
820
+ const SNAPSHOT_SKIP_TYPES = new Set([
821
+ "number", "string", "boolean", "void", "undefined", "null",
822
+ "any", "never", "unknown", "object", "date", "record",
823
+ ]);
824
+ function isComplexReturnType(unwrapped) {
825
+ const base = unwrapped.replace(/\[\]$/, "").replace(/<[^>]+>$/, "").trim();
826
+ if (SNAPSHOT_SKIP_TYPES.has(base))
827
+ return false;
828
+ if (base.startsWith("array") || base.startsWith("map") || base.startsWith("set") || base.startsWith("record"))
829
+ return false;
830
+ if (base.endsWith("[]"))
831
+ return false;
832
+ return true;
833
+ }
834
+ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = false) {
778
835
  const t = returnType.trim().toLowerCase();
779
836
  const unwrapped = unwrapType(t);
780
837
  const stubs = [];
@@ -813,6 +870,9 @@ function buildExpectStubs(returnType, scenarioLabel) {
813
870
  }
814
871
  else {
815
872
  stubs.push("expect(result).not.toBeNull();");
873
+ if (snapshotForComplexTypes && isComplexReturnType(unwrapped)) {
874
+ stubs.push("expect(result).toMatchSnapshot(); // Update snapshot after first run");
875
+ }
816
876
  }
817
877
  return stubs;
818
878
  }
@@ -822,6 +882,44 @@ function unwrapType(t) {
822
882
  return promiseMatch[1].trim();
823
883
  return t;
824
884
  }
885
+ // ─── toHaveBeenCalledWith argument builder ────────────────────────────────────
886
+ /**
887
+ * Build the argument list for `toHaveBeenCalledWith(...)`.
888
+ *
889
+ * Strategy (per dep function parameter):
890
+ * 1. If a SUT param with the same name and compatible type exists in the test's
891
+ * arranged args, use that variable name (it was declared in Arrange).
892
+ * 2. Otherwise emit an `expect.any(Constructor)` / `expect.anything()` matcher.
893
+ */
894
+ function buildCalledWithArgs(depParams, sutParamNames, sutArgLiterals) {
895
+ if (depParams.length === 0)
896
+ return "";
897
+ const args = depParams.map((dp) => {
898
+ // 1. Name match: SUT has a param with the same name
899
+ const nameIdx = sutParamNames.indexOf(dp.name);
900
+ if (nameIdx !== -1)
901
+ return dp.name;
902
+ // 2. Positional fallback: same position in param list
903
+ const posIdx = depParams.indexOf(dp);
904
+ if (posIdx < sutParamNames.length)
905
+ return sutParamNames[posIdx];
906
+ // 3. Type-based matcher
907
+ return typeToMatcher(dp.type);
908
+ });
909
+ return args.join(", ");
910
+ }
911
+ function typeToMatcher(typeStr) {
912
+ const t = typeStr.trim().toLowerCase().replace(/^promise<(.+)>$/i, "$1");
913
+ if (t === "number")
914
+ return "expect.any(Number)";
915
+ if (t === "string")
916
+ return "expect.any(String)";
917
+ if (t === "boolean")
918
+ return "expect.any(Boolean)";
919
+ if (t.endsWith("[]") || t.startsWith("array"))
920
+ return "expect.any(Array)";
921
+ return "expect.anything()";
922
+ }
825
923
  // ─── Dependency helpers ──────────────────────────────────────────────────────
826
924
  function collectAllDependencies(scenarios) {
827
925
  const depMap = new Map();