@gnapi/cotester 1.2.5 → 1.2.7

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 +4 -0
  2. package/dist/analyzer.js +4 -0
  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 +11 -0
  8. package/dist/checker.js +32 -0
  9. package/dist/checker.js.map +1 -1
  10. package/dist/cli.js +22 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/configManager.d.ts +16 -0
  13. package/dist/configManager.js +84 -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 +8 -0
  18. package/dist/frameworkAdapter.js +13 -1
  19. package/dist/frameworkAdapter.js.map +1 -1
  20. package/dist/generator.d.ts +24 -1
  21. package/dist/generator.js +161 -36
  22. package/dist/generator.js.map +1 -1
  23. package/dist/importRepairer.d.ts +22 -0
  24. package/dist/importRepairer.js +226 -0
  25. package/dist/importRepairer.js.map +1 -0
  26. package/dist/interfaceShapeResolver.js +8 -3
  27. package/dist/interfaceShapeResolver.js.map +1 -1
  28. package/dist/migrator.d.ts +49 -0
  29. package/dist/migrator.js +335 -0
  30. package/dist/migrator.js.map +1 -0
  31. package/dist/mockDataEngine.js +128 -0
  32. package/dist/mockDataEngine.js.map +1 -1
  33. package/dist/mockGenerator.js +3 -1
  34. package/dist/mockGenerator.js.map +1 -1
  35. package/dist/reporter.d.ts +1 -1
  36. package/dist/reporter.js +84 -0
  37. package/dist/reporter.js.map +1 -1
  38. package/dist/scenarioEngine.d.ts +3 -0
  39. package/dist/scenarioEngine.js +70 -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/validator.d.ts +25 -0
  45. package/dist/validator.js +150 -0
  46. package/dist/validator.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
@@ -13,6 +13,7 @@ const astCache_1 = require("./astCache");
13
13
  const scenarioEngine_1 = require("./scenarioEngine");
14
14
  const mockGenerator_1 = require("./mockGenerator");
15
15
  const generator_1 = require("./generator");
16
+ const auditLogger_1 = require("./auditLogger");
16
17
  const { filePath, projectRoot, config } = worker_threads_1.workerData;
17
18
  (async () => {
18
19
  try {
@@ -23,7 +24,11 @@ const { filePath, projectRoot, config } = worker_threads_1.workerData;
23
24
  }
24
25
  const scenarios = (0, scenarioEngine_1.generateScenarios)(functions, config, projectRoot);
25
26
  const mockResult = await (0, mockGenerator_1.generateMockFile)(filePath, scenarios, projectRoot, config.srcDir, config.mockDir);
26
- await (0, generator_1.generateTestFile)(filePath, scenarios, projectRoot, config.srcDir, config.testDir, mockResult, config.scenarioRules.tableThreshold);
27
+ const genResult = await (0, generator_1.generateTestFile)(filePath, scenarios, projectRoot, config.srcDir, config.testDir, mockResult, config.scenarioRules.tableThreshold);
28
+ if (genResult) {
29
+ const entry = (0, auditLogger_1.buildAuditEntry)(filePath, projectRoot, genResult.added, genResult.merged, genResult.skipped);
30
+ (0, auditLogger_1.writeAuditEntry)(entry, projectRoot);
31
+ }
27
32
  worker_threads_1.parentPort?.postMessage({ ok: true });
28
33
  }
29
34
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"fileWorker.js","sourceRoot":"","sources":["../src/fileWorker.ts"],"names":[],"mappings":";;AAAA;;;;;;;GAOG;AACH,mDAAwD;AACxD,yCAA+C;AAC/C,qDAAqD;AACrD,mDAAmD;AACnD,2CAA+C;AAc/C,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,2BAAyB,CAAC;AAEpE,CAAC,KAAK,IAAI,EAAE;IACV,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAA,4BAAiB,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAE3D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,2BAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAyB,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,IAAA,kCAAiB,EAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QAEpE,MAAM,UAAU,GAAG,MAAM,IAAA,gCAAgB,EACvC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,OAAO,CACf,CAAC;QAEF,MAAM,IAAA,4BAAgB,EACpB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,OAAO,EACd,UAAU,EACV,MAAM,CAAC,aAAa,CAAC,cAAc,CACpC,CAAC;QAEF,2BAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAyB,CAAC,CAAC;IAC/D,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,2BAAU,EAAE,WAAW,CAAC;YACtB,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC;SACZ,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
1
+ {"version":3,"file":"fileWorker.js","sourceRoot":"","sources":["../src/fileWorker.ts"],"names":[],"mappings":";;AAAA;;;;;;;GAOG;AACH,mDAAwD;AACxD,yCAA+C;AAC/C,qDAAqD;AACrD,mDAAmD;AACnD,2CAA+C;AAE/C,+CAAiE;AAajE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,2BAAyB,CAAC;AAEpE,CAAC,KAAK,IAAI,EAAE;IACV,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAA,4BAAiB,EAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAE3D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,2BAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAyB,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,IAAA,kCAAiB,EAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QAEpE,MAAM,UAAU,GAAG,MAAM,IAAA,gCAAgB,EACvC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,OAAO,CACf,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,IAAA,4BAAgB,EACtC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,OAAO,EACd,UAAU,EACV,MAAM,CAAC,aAAa,CAAC,cAAc,CACpC,CAAC;QAEF,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,IAAA,6BAAe,EAC3B,QAAQ,EACR,WAAW,EACX,SAAS,CAAC,KAAK,EACf,SAAS,CAAC,MAAM,EAChB,SAAS,CAAC,OAAO,CAClB,CAAC;YACF,IAAA,6BAAe,EAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QACtC,CAAC;QAED,2BAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAyB,CAAC,CAAC;IAC/D,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,2BAAU,EAAE,WAAW,CAAC;YACtB,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC;SACZ,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
@@ -13,6 +13,10 @@ export interface TestFrameworkAdapter {
13
13
  mockFn(): string;
14
14
  /** `jest.clearAllMocks()` / `vi.clearAllMocks()` */
15
15
  clearAllMocks(): string;
16
+ /** `jest.resetAllMocks()` / `vi.resetAllMocks()` — also resets mock implementations */
17
+ resetAllMocks(): string;
18
+ /** `jest.restoreAllMocks()` / `vi.restoreAllMocks()` — restores spyOn mocks */
19
+ restoreAllMocks(): string;
16
20
  /**
17
21
  * Cast expression prefix for setting up a mock return value on a named import.
18
22
  * Jest: `(name as jest.Mock)`
@@ -32,6 +36,8 @@ export declare class JestAdapter implements TestFrameworkAdapter {
32
36
  mockModuleCheck(importPath: string): string;
33
37
  mockFn(): string;
34
38
  clearAllMocks(): string;
39
+ resetAllMocks(): string;
40
+ restoreAllMocks(): string;
35
41
  mockCast(name: string): string;
36
42
  mockResolvedValue(name: string, val: string): string;
37
43
  mockReturnValue(name: string, val: string): string;
@@ -43,6 +49,8 @@ export declare class VitestAdapter implements TestFrameworkAdapter {
43
49
  mockModuleCheck(importPath: string): string;
44
50
  mockFn(): string;
45
51
  clearAllMocks(): string;
52
+ resetAllMocks(): string;
53
+ restoreAllMocks(): string;
46
54
  mockCast(name: string): string;
47
55
  mockResolvedValue(name: string, val: string): string;
48
56
  mockReturnValue(name: string, val: string): string;
@@ -24,6 +24,12 @@ class JestAdapter {
24
24
  clearAllMocks() {
25
25
  return "jest.clearAllMocks();";
26
26
  }
27
+ resetAllMocks() {
28
+ return "jest.resetAllMocks();";
29
+ }
30
+ restoreAllMocks() {
31
+ return "jest.restoreAllMocks();";
32
+ }
27
33
  mockCast(name) {
28
34
  return `(${name} as jest.Mock)`;
29
35
  }
@@ -41,7 +47,7 @@ exports.JestAdapter = JestAdapter;
41
47
  // ── Vitest adapter ────────────────────────────────────────────────────────────
42
48
  class VitestAdapter {
43
49
  frameworkImport() {
44
- return "import { describe, test, expect, vi, beforeEach } from 'vitest';";
50
+ return "import { describe, test, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';";
45
51
  }
46
52
  mockModule(importPath, importedNames) {
47
53
  const fns = importedNames.map((n) => ` ${n}: vi.fn(),`).join("\n");
@@ -56,6 +62,12 @@ class VitestAdapter {
56
62
  clearAllMocks() {
57
63
  return "vi.clearAllMocks();";
58
64
  }
65
+ resetAllMocks() {
66
+ return "vi.resetAllMocks();";
67
+ }
68
+ restoreAllMocks() {
69
+ return "vi.restoreAllMocks();";
70
+ }
59
71
  mockCast(name) {
60
72
  return `vi.mocked(${name})`;
61
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"frameworkAdapter.js","sourceRoot":"","sources":["../src/frameworkAdapter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAuHH,sCAEC;AAtFD,iFAAiF;AAEjF,MAAa,WAAW;IACpB,eAAe;QACX,OAAO,EAAE,CAAC;IACd,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtE,OAAO,cAAc,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,cAAc,UAAU,GAAG,CAAC;IACvC,CAAC;IAED,MAAM;QACF,OAAO,WAAW,CAAC;IACvB,CAAC;IAED,aAAa;QACT,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,IAAI,IAAI,gBAAgB,CAAC;IACpC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,6BAA6B,GAAG,GAAG,CAAC;IAC/C,CAAC;CACJ;AArCD,kCAqCC;AAED,iFAAiF;AAEjF,MAAa,aAAa;IACtB,eAAe;QACX,OAAO,kEAAkE,CAAC;IAC9E,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpE,OAAO,YAAY,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC7D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,YAAY,UAAU,GAAG,CAAC;IACrC,CAAC;IAED,MAAM;QACF,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,aAAa;QACT,OAAO,qBAAqB,CAAC;IACjC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,aAAa,IAAI,GAAG,CAAC;IAChC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,2BAA2B,GAAG,GAAG,CAAC;IAC7C,CAAC;CACJ;AArCD,sCAqCC;AAED,iFAAiF;AAEjF,SAAgB,aAAa,CAAC,YAA+B,MAAM;IAC/D,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;AAC5E,CAAC"}
1
+ {"version":3,"file":"frameworkAdapter.js","sourceRoot":"","sources":["../src/frameworkAdapter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AA6IH,sCAEC;AAtGD,iFAAiF;AAEjF,MAAa,WAAW;IACpB,eAAe;QACX,OAAO,EAAE,CAAC;IACd,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtE,OAAO,cAAc,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,cAAc,UAAU,GAAG,CAAC;IACvC,CAAC;IAED,MAAM;QACF,OAAO,WAAW,CAAC;IACvB,CAAC;IAED,aAAa;QACT,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED,aAAa;QACT,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED,eAAe;QACX,OAAO,yBAAyB,CAAC;IACrC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,IAAI,IAAI,gBAAgB,CAAC;IACpC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,6BAA6B,GAAG,GAAG,CAAC;IAC/C,CAAC;CACJ;AA7CD,kCA6CC;AAED,iFAAiF;AAEjF,MAAa,aAAa;IACtB,eAAe;QACX,OAAO,uFAAuF,CAAC;IACnG,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpE,OAAO,YAAY,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC7D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,YAAY,UAAU,GAAG,CAAC;IACrC,CAAC;IAED,MAAM;QACF,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,aAAa;QACT,OAAO,qBAAqB,CAAC;IACjC,CAAC;IAED,aAAa;QACT,OAAO,qBAAqB,CAAC;IACjC,CAAC;IAED,eAAe;QACX,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,aAAa,IAAI,GAAG,CAAC;IAChC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,2BAA2B,GAAG,GAAG,CAAC;IAC7C,CAAC;CACJ;AA7CD,sCA6CC;AAED,iFAAiF;AAEjF,SAAgB,aAAa,CAAC,YAA+B,MAAM;IAC/D,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;AAC5E,CAAC"}
@@ -1,5 +1,28 @@
1
1
  import { TestScenario } from "./scenarioEngine";
2
2
  import { MockFileResult } from "./mockGenerator";
3
+ /**
4
+ * Marker placed as the very first line of every generated test file.
5
+ * Its presence lets cotester know the file was scaffolded (not hand-written),
6
+ * but function-level merging works regardless of whether the marker is present.
7
+ */
8
+ export declare const TESTGEN_MARKER = "// @cotester-generated";
9
+ /**
10
+ * Format version written into the second line of every generated test file.
11
+ * Used by `cotester migrate` to detect files produced by older versions and
12
+ * upgrade them in-place. Increment this when the generated format changes.
13
+ */
14
+ export declare const CURRENT_FILE_VERSION = 2;
15
+ export declare const VERSION_MARKER = "// @cotester-version: 2";
16
+ /** Returned by generateTestFile — carries the test file path and audit data. */
17
+ export interface GenerateResult {
18
+ testFilePath: string;
19
+ /** Function names for which a new describe block was written */
20
+ added: string[];
21
+ /** Function names whose signature changed (describe existed, warning injected) */
22
+ merged: string[];
23
+ /** Function names already covered and left untouched */
24
+ skipped: string[];
25
+ }
3
26
  /**
4
27
  * Generate (or merge into) a test file for the given source file.
5
28
  *
@@ -13,4 +36,4 @@ import { MockFileResult } from "./mockGenerator";
13
36
  * Individual functions/methods that already have a describe block are never
14
37
  * touched, preserving any hand-written assertions inside them.
15
38
  */
16
- export declare function generateTestFile(srcFilePath: string, scenarios: TestScenario[], projectRoot: string, srcDir: string, testDir: string, mockResult: MockFileResult | null, tableThreshold?: number, dryRun?: boolean, framework?: "jest" | "vitest", snapshotForComplexTypes?: boolean): Promise<string | null>;
39
+ export declare function generateTestFile(srcFilePath: string, scenarios: TestScenario[], projectRoot: string, srcDir: string, testDir: string, mockResult: MockFileResult | null, tableThreshold?: number, dryRun?: boolean, framework?: "jest" | "vitest", snapshotForComplexTypes?: boolean): Promise<GenerateResult | null>;
package/dist/generator.js CHANGED
@@ -33,6 +33,7 @@ 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"));
@@ -45,7 +46,14 @@ const frameworkAdapter_1 = require("./frameworkAdapter");
45
46
  * Its presence lets cotester know the file was scaffolded (not hand-written),
46
47
  * but function-level merging works regardless of whether the marker is present.
47
48
  */
48
- 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}`;
49
57
  /**
50
58
  * Generate (or merge into) a test file for the given source file.
51
59
  *
@@ -75,7 +83,8 @@ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, tes
75
83
  process.stdout.write(`// [dry-run] Test file: ${relTest}\n`);
76
84
  process.stdout.write(`${"─".repeat(60)}\n`);
77
85
  process.stdout.write(formatted);
78
- return testFilePath;
86
+ const allNames = scenarios.map((s) => s.functionName);
87
+ return { testFilePath, added: allNames, merged: [], skipped: [] };
79
88
  }
80
89
  (0, utils_1.ensureDir)(path.dirname(testFilePath));
81
90
  // ── Brand-new file → generate everything ─────────────────────────────
@@ -84,7 +93,8 @@ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, tes
84
93
  const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
85
94
  fs.writeFileSync(testFilePath, formatted, "utf-8");
86
95
  (0, utils_1.log)(`Test file → ${path.relative(projectRoot, testFilePath)}`, "success");
87
- return testFilePath;
96
+ const allNames = scenarios.map((s) => s.functionName);
97
+ return { testFilePath, added: allNames, merged: [], skipped: [] };
88
98
  }
89
99
  // ── File exists → function-level merge ───────────────────────────────
90
100
  return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
@@ -105,6 +115,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
105
115
  const existingDescribes = detectExistingDescribes(content);
106
116
  // ── Feature 6: signature-change detection for already-covered describes ──
107
117
  const existingSigs = parseExistingSignatures(content);
118
+ const sigChangedNames = new Set();
108
119
  for (const s of scenarios) {
109
120
  const key = s.functionName;
110
121
  if (!existingDescribes.has(key))
@@ -120,6 +131,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
120
131
  (0, utils_1.log)(` Signature changed for ${key} — injecting update warning.`, "warn");
121
132
  content = updateSignatureComment(content, key, newSig);
122
133
  content = injectSignatureWarning(content, key, oldSig, newSig);
134
+ sigChangedNames.add(key);
123
135
  }
124
136
  }
125
137
  // ── Classify scenarios ───────────────────────────────────────────────
@@ -161,9 +173,16 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
161
173
  for (const fn of missingStandalone) {
162
174
  toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
163
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);
164
183
  if (toAppend.length === 0 && newScenarios.length === 0) {
165
184
  (0, utils_1.log)(` ${path.relative(projectRoot, testFilePath)} — all functions already covered.`, "info");
166
- return testFilePath;
185
+ return { testFilePath, added: [], merged: mergedNames, skipped: skippedNames };
167
186
  }
168
187
  // ── Append standalone / new-class blocks ─────────────────────────────
169
188
  if (toAppend.length > 0) {
@@ -173,9 +192,8 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
173
192
  content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter);
174
193
  const formatted = await (0, formatter_1.formatCode)(content, projectRoot);
175
194
  fs.writeFileSync(testFilePath, formatted, "utf-8");
176
- const addedCount = newScenarios.length;
177
- (0, utils_1.log)(`Test file updated → ${path.relative(projectRoot, testFilePath)} (+${addedCount} function(s))`, "success");
178
- 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 };
179
197
  }
180
198
  // ─── Merge helpers ────────────────────────────────────────────────────────────
181
199
  /**
@@ -305,13 +323,42 @@ function addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapt
305
323
  }
306
324
  if (mockLines.length === 0 && importLines.length === 0)
307
325
  return content;
308
- const injection = [...mockLines, ...importLines].join("\n") + "\n\n";
309
- // Prepend before the first import statement (or before the first describe as fallback)
310
- const firstImportIdx = content.indexOf("import ");
311
- if (firstImportIdx !== -1) {
312
- return content.substring(0, firstImportIdx) + injection + content.substring(firstImportIdx);
326
+ // ── Step 1: insert jest.mock() calls before ALL imports ─────────────
327
+ // Jest's babel-jest transform hoists jest.mock() above imports, but keeping
328
+ // them before imports is cleaner and avoids confusion.
329
+ let result = content;
330
+ if (mockLines.length > 0) {
331
+ const mockBlock = mockLines.join("\n") + "\n\n";
332
+ const firstImportIdx = result.indexOf("import ");
333
+ if (firstImportIdx !== -1) {
334
+ result = result.substring(0, firstImportIdx) + mockBlock + result.substring(firstImportIdx);
335
+ }
336
+ else {
337
+ result = mockBlock + result;
338
+ }
339
+ }
340
+ // ── Step 2: insert named imports AFTER the last existing import ─────
341
+ // This keeps framework imports (vitest/jest) at the top and source
342
+ // imports grouped together.
343
+ if (importLines.length > 0) {
344
+ const importBlock = importLines.join("\n");
345
+ // Find the last "import " line to append new imports after it
346
+ const lines = result.split("\n");
347
+ let lastImportLineIdx = -1;
348
+ for (let i = 0; i < lines.length; i++) {
349
+ if (lines[i].trimStart().startsWith("import ")) {
350
+ lastImportLineIdx = i;
351
+ }
352
+ }
353
+ if (lastImportLineIdx !== -1) {
354
+ lines.splice(lastImportLineIdx + 1, 0, importBlock);
355
+ result = lines.join("\n");
356
+ }
357
+ else {
358
+ result = result + "\n" + importBlock;
359
+ }
313
360
  }
314
- return injection + content;
361
+ return result;
315
362
  }
316
363
  function escapeForRegex(str) {
317
364
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -321,7 +368,8 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
321
368
  const lines = [];
322
369
  // Marker — presence of this line tells cotester the file is auto-generated
323
370
  // and safe to regenerate. Remove it to protect hand-written edits.
324
- lines.push(TESTGEN_MARKER);
371
+ lines.push(exports.TESTGEN_MARKER);
372
+ lines.push(exports.VERSION_MARKER);
325
373
  lines.push("");
326
374
  // ── Framework import (Vitest only; empty for Jest) ───────────────
327
375
  const fwImport = adapter.frameworkImport();
@@ -398,7 +446,11 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
398
446
  function buildClassDescribe(className, methods, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
399
447
  const lines = [];
400
448
  lines.push(`describe(${JSON.stringify(className)}, () => {`);
401
- lines.push(` let instance: ${className};`);
449
+ // Determine if the class has any instance methods (non-static) before declaring `instance`
450
+ const hasInstanceMethods = methods.some((m) => !m.isStatic);
451
+ if (hasInstanceMethods) {
452
+ lines.push(` let instance: ${className};`);
453
+ }
402
454
  // Generate mock objects for constructor dependencies
403
455
  const ctorParams = methods[0]?.constructorParams ?? [];
404
456
  const ormUsage = methods[0]?.ormUsage;
@@ -425,10 +477,27 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
425
477
  mockDeclarations.push(` const ${mockName} = { find: ${fn}, findById: ${fn}, save: ${fn}, create: ${fn}, update: ${fn}, delete: ${fn} } as any;`);
426
478
  }
427
479
  else if (typeLower.includes("service")) {
428
- mockDeclarations.push(` const ${mockName} = { } as any; // TODO: mock ${param.type} methods`);
480
+ const fn = adapter.mockFn();
481
+ mockDeclarations.push(` const ${mockName} = { findAll: ${fn}, findOne: ${fn}, findById: ${fn}, create: ${fn}, update: ${fn}, delete: ${fn} } as any;`);
482
+ mockDeclarations.push(` // TODO: add or remove ${param.type} mock methods to match the actual interface`);
483
+ }
484
+ else if (typeLower.includes("logger") || typeLower.includes("log")) {
485
+ const fn = adapter.mockFn();
486
+ mockDeclarations.push(` const ${mockName} = { log: ${fn}, warn: ${fn}, error: ${fn}, debug: ${fn}, verbose: ${fn} } as any;`);
487
+ }
488
+ else if (typeLower.includes("cache") || typeLower.includes("redis")) {
489
+ const fn = adapter.mockFn();
490
+ mockDeclarations.push(` const ${mockName} = { get: ${fn}, set: ${fn}, del: ${fn}, exists: ${fn} } as any;`);
491
+ }
492
+ else if (typeLower.includes("queue") || typeLower.includes("producer") || typeLower.includes("publisher")) {
493
+ const fn = adapter.mockFn();
494
+ mockDeclarations.push(` const ${mockName} = { send: ${fn}, emit: ${fn}, publish: ${fn}, add: ${fn} } as any;`);
429
495
  }
430
496
  else {
497
+ const fn = adapter.mockFn();
431
498
  mockDeclarations.push(` const ${mockName} = { } as any; // TODO: add mock methods for ${param.type}`);
499
+ // Suppress the unused variable warning when no methods are known
500
+ mockDeclarations.push(` void ${mockName}; // remove this line once you add method stubs above`);
432
501
  }
433
502
  }
434
503
  }
@@ -440,17 +509,29 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
440
509
  }
441
510
  lines.push("");
442
511
  lines.push(" beforeEach(() => {");
443
- lines.push(` ${adapter.clearAllMocks()}`);
444
- if (ctorParams.length > 0) {
445
- const args = ctorParams
446
- .map((p) => `mock${capitalize(p.name)}`)
447
- .join(", ");
448
- lines.push(` instance = new ${className}(${args});`);
449
- }
450
- else {
451
- lines.push(` instance = new ${className}();`);
512
+ lines.push(` ${adapter.resetAllMocks()}`);
513
+ if (hasInstanceMethods) {
514
+ if (ctorParams.length > 0) {
515
+ const args = ctorParams
516
+ .map((p) => `mock${capitalize(p.name)}`)
517
+ .join(", ");
518
+ lines.push(` instance = new ${className}(${args});`);
519
+ }
520
+ else {
521
+ lines.push(` instance = new ${className}();`);
522
+ }
452
523
  }
453
524
  lines.push(" });");
525
+ lines.push("");
526
+ lines.push(" afterEach(() => {");
527
+ lines.push(` ${adapter.restoreAllMocks()}`);
528
+ lines.push(" });");
529
+ if (ctorParams.length > 0) {
530
+ lines.push("");
531
+ lines.push(" afterAll(() => {");
532
+ lines.push(" // TODO: release shared resources (DB connections, file handles, timers)");
533
+ lines.push(" });");
534
+ }
454
535
  // ── FileType-specific setup ──────────────────────────────────────
455
536
  const fileType = methods[0]?.fileType ?? "unknown";
456
537
  if (fileType === "controller") {
@@ -476,10 +557,23 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0,
476
557
  lines.push(` const mockNext = ${adapter.mockFn()};`);
477
558
  }
478
559
  lines.push("");
479
- for (const method of methods) {
560
+ // Instance methods
561
+ const instanceMethods = methods.filter((m) => !m.isStatic);
562
+ for (const method of instanceMethods) {
480
563
  lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1));
481
564
  lines.push("");
482
565
  }
566
+ // Static methods — grouped in a nested describe block, called via ClassName.method()
567
+ const staticMethods = methods.filter((m) => m.isStatic);
568
+ if (staticMethods.length > 0) {
569
+ lines.push(` describe('static methods', () => {`);
570
+ for (const method of staticMethods) {
571
+ lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 2));
572
+ lines.push("");
573
+ }
574
+ lines.push(` });`);
575
+ lines.push("");
576
+ }
483
577
  lines.push("});");
484
578
  return lines.join("\n");
485
579
  }
@@ -488,10 +582,14 @@ function buildFunctionDescribe(fn, mockResult, tableThreshold = 0, adapter = (0,
488
582
  const lines = [];
489
583
  lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
490
584
  lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
491
- // Add beforeEach with clearAllMocks if this function has dependencies
585
+ // Add beforeEach/afterEach if this function has dependencies
492
586
  if (fn.dependencies.length > 0) {
493
587
  lines.push(" beforeEach(() => {");
494
- lines.push(` ${adapter.clearAllMocks()}`);
588
+ lines.push(` ${adapter.resetAllMocks()}`);
589
+ lines.push(" });");
590
+ lines.push("");
591
+ lines.push(" afterEach(() => {");
592
+ lines.push(` ${adapter.restoreAllMocks()}`);
495
593
  lines.push(" });");
496
594
  lines.push("");
497
595
  }
@@ -560,7 +658,10 @@ function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false)
560
658
  const isMethod = !!scenario.className;
561
659
  const fnName = scenario.functionName;
562
660
  const paramNames = scenario.parameters.map((p) => p.name);
563
- lines.push(`test.each([`);
661
+ // Build TypeScript generic annotation: test.each<[string, ParamType1, ParamType2]>
662
+ const paramTypes = scenario.parameters.map((p) => p.type || "unknown");
663
+ const genericAnnotation = `<[string, ${paramTypes.join(", ")}]>`;
664
+ lines.push(`test.each${genericAnnotation}([`);
564
665
  for (const c of tableCases) {
565
666
  const args = c.argLiterals.join(", ");
566
667
  lines.push(` [${JSON.stringify(c.label)}, ${args}],`);
@@ -568,7 +669,10 @@ function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false)
568
669
  lines.push(`])('%s', ${isAsync ? "async " : ""}(_label, ${paramNames.join(", ")}) => {`);
569
670
  // Act
570
671
  lines.push(` // Act`);
571
- const caller = isMethod ? `instance.${fnName}` : fnName;
672
+ const isStatic = !!scenario.isStatic;
673
+ const caller = isStatic
674
+ ? `${scenario.className}.${fnName}`
675
+ : isMethod ? `instance.${fnName}` : fnName;
572
676
  if (isAsync) {
573
677
  lines.push(` const result = await ${caller}(${paramNames.join(", ")});`);
574
678
  }
@@ -602,6 +706,7 @@ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, fra
602
706
  const fnName = scenario.functionName;
603
707
  const isAsync = scenario.isAsync;
604
708
  const isMethod = !!scenario.className;
709
+ const isStatic = !!scenario.isStatic;
605
710
  const isDependencyTest = testCase.label === "verifies dependency interactions";
606
711
  lines.push(`test(${JSON.stringify(testCase.label)}, ${isAsync ? "async " : ""}() => {`);
607
712
  // ── Arrange ──────────────────────────────────────────────────────
@@ -655,10 +760,13 @@ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, fra
655
760
  // ── Act ──────────────────────────────────────────────────────────
656
761
  lines.push(" // Act");
657
762
  const argsStr = testCase.paramNames.join(", ");
658
- const caller = isMethod ? `instance.${fnName}` : fnName;
763
+ const caller = isStatic
764
+ ? `${scenario.className}.${fnName}`
765
+ : isMethod ? `instance.${fnName}` : fnName;
659
766
  const callExpr = `${caller}(${argsStr})`;
660
767
  if (isAsync) {
661
768
  lines.push(` const result = await ${callExpr};`);
769
+ lines.push(` // TIP: alternatively use: await expect(${callExpr}).resolves.toBeDefined()`);
662
770
  }
663
771
  else {
664
772
  lines.push(` const result = ${callExpr};`);
@@ -701,6 +809,7 @@ function buildThrowsBlock(scenario, testCase, mockConstName) {
701
809
  const lines = [];
702
810
  const fnName = scenario.functionName;
703
811
  const isMethod = !!scenario.className;
812
+ const isStatic = !!scenario.isStatic;
704
813
  lines.push(`test(${JSON.stringify(testCase.label)}, () => {`);
705
814
  // ── Arrange ──────────────────────────────────────────────────────
706
815
  const caseKey = camelCase(testCase.label);
@@ -724,8 +833,11 @@ function buildThrowsBlock(scenario, testCase, mockConstName) {
724
833
  // ── Act & Assert ────────────────────────────────────────────────
725
834
  lines.push(" // Act & Assert");
726
835
  const argsStr = testCase.paramNames.join(", ");
727
- const caller = isMethod ? `instance.${fnName}` : fnName;
728
- lines.push(` expect(() => ${caller}(${argsStr})).toThrow();`);
836
+ const caller = isStatic
837
+ ? `${scenario.className}.${fnName}`
838
+ : isMethod ? `instance.${fnName}` : fnName;
839
+ lines.push(` expect(() => ${caller}(${argsStr})).toThrow(Error);`);
840
+ lines.push(` // TODO: narrow to a specific error type, e.g. .toThrow(ValidationError) or .toThrow('message')`);
729
841
  lines.push("});");
730
842
  return lines.join("\n");
731
843
  }
@@ -734,6 +846,7 @@ function buildRejectsBlock(scenario, testCase, mockConstName) {
734
846
  const lines = [];
735
847
  const fnName = scenario.functionName;
736
848
  const isMethod = !!scenario.className;
849
+ const isStatic = !!scenario.isStatic;
737
850
  lines.push(`test(${JSON.stringify(testCase.label)}, async () => {`);
738
851
  // ── Arrange ──────────────────────────────────────────────────────
739
852
  const caseKey = camelCase(testCase.label);
@@ -757,8 +870,11 @@ function buildRejectsBlock(scenario, testCase, mockConstName) {
757
870
  // ── Act & Assert ────────────────────────────────────────────────
758
871
  lines.push(" // Act & Assert");
759
872
  const argsStr = testCase.paramNames.join(", ");
760
- const caller = isMethod ? `instance.${fnName}` : fnName;
873
+ const caller = isStatic
874
+ ? `${scenario.className}.${fnName}`
875
+ : isMethod ? `instance.${fnName}` : fnName;
761
876
  lines.push(` await expect(${caller}(${argsStr})).rejects.toThrow();`);
877
+ lines.push(` // TODO: narrow rejection, e.g. .rejects.toThrow(SpecificError) or .rejects.toThrow('message')`);
762
878
  lines.push("});");
763
879
  return lines.join("\n");
764
880
  }
@@ -825,6 +941,7 @@ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = f
825
941
  stubs.push("// expect(result).toBe(0);");
826
942
  if (scenarioLabel.includes("boundary"))
827
943
  stubs.push("expect(Number.isFinite(result)).toBe(true);");
944
+ stubs.push("// TODO: add domain assertion — e.g. expect(result).toBe(expectedValue)");
828
945
  }
829
946
  else if (unwrapped === "string") {
830
947
  stubs.push("expect(typeof result).toBe('string');");
@@ -832,6 +949,7 @@ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = f
832
949
  stubs.push("// expect(result).toBe('');");
833
950
  if (scenarioLabel.includes("valid"))
834
951
  stubs.push("expect(result.length).toBeGreaterThan(0);");
952
+ stubs.push("// TODO: add domain assertion — e.g. expect(result).toEqual(expectedString)");
835
953
  }
836
954
  else if (unwrapped === "boolean") {
837
955
  stubs.push("expect(typeof result).toBe('boolean');");
@@ -839,21 +957,28 @@ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = f
839
957
  stubs.push("// expect(result).toBe(true);");
840
958
  if (scenarioLabel.includes("falsy"))
841
959
  stubs.push("// expect(result).toBe(false);");
960
+ stubs.push("// TODO: add domain assertion — e.g. expect(result).toBe(true)");
961
+ }
962
+ else if (unwrapped === "null") {
963
+ stubs.length = 0;
964
+ stubs.push("expect(result).toBeNull();");
842
965
  }
843
- else if (unwrapped === "void") {
966
+ else if (unwrapped === "void" || unwrapped === "undefined") {
844
967
  stubs.length = 0;
845
968
  stubs.push("expect(result).toBeUndefined();");
846
969
  }
847
- else if (unwrapped.endsWith("[]")) {
970
+ else if (unwrapped.endsWith("[]") || unwrapped.startsWith("array<")) {
848
971
  stubs.push("expect(Array.isArray(result)).toBe(true);");
849
972
  if (scenarioLabel.includes("empty"))
850
973
  stubs.push("// expect(result).toHaveLength(0);");
974
+ stubs.push("// TODO: add domain assertion — e.g. expect(result).toHaveLength(n) or toContainEqual(item)");
851
975
  }
852
976
  else {
853
977
  stubs.push("expect(result).not.toBeNull();");
854
978
  if (snapshotForComplexTypes && isComplexReturnType(unwrapped)) {
855
979
  stubs.push("expect(result).toMatchSnapshot(); // Update snapshot after first run");
856
980
  }
981
+ stubs.push("// TODO: add domain assertion — e.g. expect(result).toEqual(expected) or toHaveProperty('key', value)");
857
982
  }
858
983
  return stubs;
859
984
  }