@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.
- package/dist/analyzer.d.ts +4 -0
- package/dist/analyzer.js +4 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/auditLogger.d.ts +46 -0
- package/dist/auditLogger.js +107 -0
- package/dist/auditLogger.js.map +1 -0
- package/dist/checker.d.ts +11 -0
- package/dist/checker.js +32 -0
- package/dist/checker.js.map +1 -1
- package/dist/cli.js +22 -3
- package/dist/cli.js.map +1 -1
- package/dist/configManager.d.ts +16 -0
- package/dist/configManager.js +84 -17
- package/dist/configManager.js.map +1 -1
- package/dist/fileWorker.js +6 -1
- package/dist/fileWorker.js.map +1 -1
- package/dist/frameworkAdapter.d.ts +8 -0
- package/dist/frameworkAdapter.js +13 -1
- package/dist/frameworkAdapter.js.map +1 -1
- package/dist/generator.d.ts +24 -1
- package/dist/generator.js +161 -36
- package/dist/generator.js.map +1 -1
- package/dist/importRepairer.d.ts +22 -0
- package/dist/importRepairer.js +226 -0
- package/dist/importRepairer.js.map +1 -0
- package/dist/interfaceShapeResolver.js +8 -3
- package/dist/interfaceShapeResolver.js.map +1 -1
- package/dist/migrator.d.ts +49 -0
- package/dist/migrator.js +335 -0
- package/dist/migrator.js.map +1 -0
- package/dist/mockDataEngine.js +128 -0
- package/dist/mockDataEngine.js.map +1 -1
- package/dist/mockGenerator.js +3 -1
- package/dist/mockGenerator.js.map +1 -1
- package/dist/reporter.d.ts +1 -1
- package/dist/reporter.js +84 -0
- package/dist/reporter.js.map +1 -1
- package/dist/scenarioEngine.d.ts +3 -0
- package/dist/scenarioEngine.js +70 -1
- package/dist/scenarioEngine.js.map +1 -1
- package/dist/sensitiveValueDetector.d.ts +62 -0
- package/dist/sensitiveValueDetector.js +147 -0
- package/dist/sensitiveValueDetector.js.map +1 -0
- package/dist/validator.d.ts +25 -0
- package/dist/validator.js +150 -0
- package/dist/validator.js.map +1 -0
- package/dist/watcher.js +10 -1
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
package/dist/fileWorker.js
CHANGED
|
@@ -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) {
|
package/dist/fileWorker.js.map
CHANGED
|
@@ -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;
|
|
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;
|
package/dist/frameworkAdapter.js
CHANGED
|
@@ -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;;;
|
|
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"}
|
package/dist/generator.d.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
728
|
-
|
|
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 =
|
|
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
|
}
|