@devness/coverit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/agents/orchestrator.d.ts +21 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.js +235 -0
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/reporter.d.ts +13 -0
- package/dist/agents/reporter.d.ts.map +1 -0
- package/dist/agents/reporter.js +323 -0
- package/dist/agents/reporter.js.map +1 -0
- package/dist/ai/anthropic-provider.d.ts +19 -0
- package/dist/ai/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/anthropic-provider.js +83 -0
- package/dist/ai/anthropic-provider.js.map +1 -0
- package/dist/ai/claude-cli-provider.d.ts +22 -0
- package/dist/ai/claude-cli-provider.d.ts.map +1 -0
- package/dist/ai/claude-cli-provider.js +197 -0
- package/dist/ai/claude-cli-provider.js.map +1 -0
- package/dist/ai/index.d.ts +15 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +16 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/ollama-provider.d.ts +17 -0
- package/dist/ai/ollama-provider.d.ts.map +1 -0
- package/dist/ai/ollama-provider.js +88 -0
- package/dist/ai/ollama-provider.js.map +1 -0
- package/dist/ai/openai-provider.d.ts +20 -0
- package/dist/ai/openai-provider.d.ts.map +1 -0
- package/dist/ai/openai-provider.js +74 -0
- package/dist/ai/openai-provider.js.map +1 -0
- package/dist/ai/prompts.d.ts +36 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +259 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/provider-factory.d.ts +26 -0
- package/dist/ai/provider-factory.d.ts.map +1 -0
- package/dist/ai/provider-factory.js +111 -0
- package/dist/ai/provider-factory.js.map +1 -0
- package/dist/ai/types.d.ts +37 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +10 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/analysis/code-scanner.d.ts +9 -0
- package/dist/analysis/code-scanner.d.ts.map +1 -0
- package/dist/analysis/code-scanner.js +409 -0
- package/dist/analysis/code-scanner.js.map +1 -0
- package/dist/analysis/dependency-graph.d.ts +9 -0
- package/dist/analysis/dependency-graph.d.ts.map +1 -0
- package/dist/analysis/dependency-graph.js +149 -0
- package/dist/analysis/dependency-graph.js.map +1 -0
- package/dist/analysis/diff-analyzer.d.ts +9 -0
- package/dist/analysis/diff-analyzer.d.ts.map +1 -0
- package/dist/analysis/diff-analyzer.js +232 -0
- package/dist/analysis/diff-analyzer.js.map +1 -0
- package/dist/analysis/index.d.ts +5 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +5 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/strategy-planner.d.ts +11 -0
- package/dist/analysis/strategy-planner.d.ts.map +1 -0
- package/dist/analysis/strategy-planner.js +384 -0
- package/dist/analysis/strategy-planner.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +288 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/executors/base-executor.d.ts +35 -0
- package/dist/executors/base-executor.d.ts.map +1 -0
- package/dist/executors/base-executor.js +138 -0
- package/dist/executors/base-executor.js.map +1 -0
- package/dist/executors/browser-runner.d.ts +24 -0
- package/dist/executors/browser-runner.d.ts.map +1 -0
- package/dist/executors/browser-runner.js +194 -0
- package/dist/executors/browser-runner.js.map +1 -0
- package/dist/executors/cloud-runner.d.ts +41 -0
- package/dist/executors/cloud-runner.d.ts.map +1 -0
- package/dist/executors/cloud-runner.js +153 -0
- package/dist/executors/cloud-runner.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.d.ts.map +1 -0
- package/dist/executors/index.js +28 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/local-runner.d.ts +40 -0
- package/dist/executors/local-runner.d.ts.map +1 -0
- package/dist/executors/local-runner.js +395 -0
- package/dist/executors/local-runner.js.map +1 -0
- package/dist/executors/reporter.d.ts +6 -0
- package/dist/executors/reporter.d.ts.map +1 -0
- package/dist/executors/reporter.js +6 -0
- package/dist/executors/reporter.js.map +1 -0
- package/dist/executors/simulator-runner.d.ts +30 -0
- package/dist/executors/simulator-runner.d.ts.map +1 -0
- package/dist/executors/simulator-runner.js +339 -0
- package/dist/executors/simulator-runner.js.map +1 -0
- package/dist/generators/api-test.d.ts +22 -0
- package/dist/generators/api-test.d.ts.map +1 -0
- package/dist/generators/api-test.js +235 -0
- package/dist/generators/api-test.js.map +1 -0
- package/dist/generators/base-generator.d.ts +79 -0
- package/dist/generators/base-generator.d.ts.map +1 -0
- package/dist/generators/base-generator.js +234 -0
- package/dist/generators/base-generator.js.map +1 -0
- package/dist/generators/desktop-test.d.ts +22 -0
- package/dist/generators/desktop-test.d.ts.map +1 -0
- package/dist/generators/desktop-test.js +290 -0
- package/dist/generators/desktop-test.js.map +1 -0
- package/dist/generators/e2e-browser.d.ts +19 -0
- package/dist/generators/e2e-browser.d.ts.map +1 -0
- package/dist/generators/e2e-browser.js +233 -0
- package/dist/generators/e2e-browser.js.map +1 -0
- package/dist/generators/index.d.ts +21 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +66 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/mobile-test.d.ts +22 -0
- package/dist/generators/mobile-test.d.ts.map +1 -0
- package/dist/generators/mobile-test.js +286 -0
- package/dist/generators/mobile-test.js.map +1 -0
- package/dist/generators/unit-test.d.ts +23 -0
- package/dist/generators/unit-test.d.ts.map +1 -0
- package/dist/generators/unit-test.js +282 -0
- package/dist/generators/unit-test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/types/index.d.ts +295 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/framework-detector.d.ts +28 -0
- package/dist/utils/framework-detector.d.ts.map +1 -0
- package/dist/utils/framework-detector.js +184 -0
- package/dist/utils/framework-detector.js.map +1 -0
- package/dist/utils/git.d.ts +33 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +82 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +47 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import { resolve, extname } from "path";
|
|
3
|
+
const EXTENSION_TO_LANGUAGE = {
|
|
4
|
+
".ts": "typescript",
|
|
5
|
+
".tsx": "typescript",
|
|
6
|
+
".js": "javascript",
|
|
7
|
+
".jsx": "javascript",
|
|
8
|
+
".mjs": "javascript",
|
|
9
|
+
".cjs": "javascript",
|
|
10
|
+
".py": "python",
|
|
11
|
+
".go": "go",
|
|
12
|
+
".rs": "rust",
|
|
13
|
+
".java": "java",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Path-based rules evaluated in order. First match wins.
|
|
17
|
+
* Each rule is [pattern, fileType] where pattern is a regex tested against the normalized posix path.
|
|
18
|
+
*/
|
|
19
|
+
const FILE_TYPE_RULES = [
|
|
20
|
+
// Tests first — they override everything
|
|
21
|
+
[/\.(test|spec)\.[^/]+$/, "test"],
|
|
22
|
+
// Platform-specific
|
|
23
|
+
[/\/src-tauri\//, "desktop-window"],
|
|
24
|
+
[/\/desktop\/.*components?\//, "desktop-component"],
|
|
25
|
+
[/\/desktop\//, "desktop-window"],
|
|
26
|
+
[/\/mobile\/.*screens?\//, "mobile-screen"],
|
|
27
|
+
[/\/mobile\/.*components?\//, "mobile-component"],
|
|
28
|
+
[/\/expo\/.*screens?\//, "mobile-screen"],
|
|
29
|
+
[/\/expo\/.*components?\//, "mobile-component"],
|
|
30
|
+
[/\/mobile\//, "mobile-screen"],
|
|
31
|
+
[/\/expo\//, "mobile-screen"],
|
|
32
|
+
// API layer
|
|
33
|
+
[/\/routes?\//, "api-route"],
|
|
34
|
+
[/\/api\//, "api-route"],
|
|
35
|
+
[/\/controllers?\//, "api-controller"],
|
|
36
|
+
[/\/middleware\//, "middleware"],
|
|
37
|
+
// React patterns
|
|
38
|
+
[/\/pages?\//, "react-page"],
|
|
39
|
+
[/\/screens?\//, "react-page"],
|
|
40
|
+
[/\/hooks?\//, "react-hook"],
|
|
41
|
+
[/\/components?\//, "react-component"],
|
|
42
|
+
// Backend patterns
|
|
43
|
+
[/\/services?\//, "service"],
|
|
44
|
+
[/\/utils?\//, "utility"],
|
|
45
|
+
[/\/helpers?\//, "utility"],
|
|
46
|
+
[/\/models?\//, "model"],
|
|
47
|
+
[/\/entities\//, "model"],
|
|
48
|
+
[/\/schemas?\//, "schema"],
|
|
49
|
+
[/\/migrations?\//, "migration"],
|
|
50
|
+
// Config files
|
|
51
|
+
[/\.(config|rc)\.[^/]+$/, "config"],
|
|
52
|
+
[/\/config\//, "config"],
|
|
53
|
+
// Style files
|
|
54
|
+
[/\.(css|scss|sass|less|styl)$/, "style"],
|
|
55
|
+
];
|
|
56
|
+
function detectLanguage(filePath) {
|
|
57
|
+
const ext = extname(filePath).toLowerCase();
|
|
58
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? "unknown";
|
|
59
|
+
}
|
|
60
|
+
function detectFileType(filePath) {
|
|
61
|
+
// Normalize to forward slashes for consistent matching
|
|
62
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
63
|
+
for (const [pattern, fileType] of FILE_TYPE_RULES) {
|
|
64
|
+
if (pattern.test(normalized)) {
|
|
65
|
+
return fileType;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// TSX files with no other pattern match are likely React components
|
|
69
|
+
if (normalized.endsWith(".tsx")) {
|
|
70
|
+
return "react-component";
|
|
71
|
+
}
|
|
72
|
+
return "unknown";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse unified diff hunks from raw diff text for a single file.
|
|
76
|
+
*/
|
|
77
|
+
function parseHunks(rawDiff, _filePath) {
|
|
78
|
+
const hunks = [];
|
|
79
|
+
// Find the section for this file and extract @@ hunk headers
|
|
80
|
+
const hunkHeaderRegex = /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/gm;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = hunkHeaderRegex.exec(rawDiff)) !== null) {
|
|
83
|
+
const startLine = parseInt(match[1], 10);
|
|
84
|
+
const lineCount = match[2] !== undefined ? parseInt(match[2], 10) : 1;
|
|
85
|
+
const endLine = startLine + Math.max(lineCount - 1, 0);
|
|
86
|
+
// Capture the hunk content (lines between this header and the next one)
|
|
87
|
+
const headerEnd = match.index + match[0].length;
|
|
88
|
+
const nextHunkIdx = rawDiff.indexOf("\n@@", headerEnd);
|
|
89
|
+
const nextFileIdx = rawDiff.indexOf("\ndiff --git", headerEnd);
|
|
90
|
+
let sliceEnd;
|
|
91
|
+
if (nextHunkIdx === -1 && nextFileIdx === -1) {
|
|
92
|
+
sliceEnd = rawDiff.length;
|
|
93
|
+
}
|
|
94
|
+
else if (nextHunkIdx === -1) {
|
|
95
|
+
sliceEnd = nextFileIdx;
|
|
96
|
+
}
|
|
97
|
+
else if (nextFileIdx === -1) {
|
|
98
|
+
sliceEnd = nextHunkIdx;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
sliceEnd = Math.min(nextHunkIdx, nextFileIdx);
|
|
102
|
+
}
|
|
103
|
+
const content = rawDiff.slice(headerEnd, sliceEnd).trim();
|
|
104
|
+
hunks.push({ startLine, endLine, content });
|
|
105
|
+
}
|
|
106
|
+
return hunks;
|
|
107
|
+
}
|
|
108
|
+
function mapDiffStatus(status) {
|
|
109
|
+
switch (status) {
|
|
110
|
+
case "A":
|
|
111
|
+
return "added";
|
|
112
|
+
case "M":
|
|
113
|
+
return "modified";
|
|
114
|
+
case "D":
|
|
115
|
+
return "deleted";
|
|
116
|
+
case "R":
|
|
117
|
+
case "R100":
|
|
118
|
+
return "renamed";
|
|
119
|
+
default:
|
|
120
|
+
// Renames with similarity (R075 etc.) or copies
|
|
121
|
+
if (status.startsWith("R"))
|
|
122
|
+
return "renamed";
|
|
123
|
+
return "modified";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Analyze git diff to identify changed files with metadata.
|
|
128
|
+
*
|
|
129
|
+
* @param projectRoot - Absolute path to the git repository root
|
|
130
|
+
* @param baseBranch - Branch to diff against (defaults to HEAD for uncommitted changes)
|
|
131
|
+
*/
|
|
132
|
+
export async function analyzeDiff(projectRoot, baseBranch) {
|
|
133
|
+
const root = resolve(projectRoot);
|
|
134
|
+
const git = simpleGit(root);
|
|
135
|
+
// Verify this is a git repository
|
|
136
|
+
const isRepo = await git.checkIsRepo();
|
|
137
|
+
if (!isRepo) {
|
|
138
|
+
throw new Error(`Not a git repository: ${root}`);
|
|
139
|
+
}
|
|
140
|
+
const currentBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() || "HEAD";
|
|
141
|
+
const base = baseBranch ?? currentBranch;
|
|
142
|
+
let diffSummaryFiles;
|
|
143
|
+
let rawDiff;
|
|
144
|
+
if (baseBranch) {
|
|
145
|
+
// Diff between base branch and current HEAD
|
|
146
|
+
const summary = await git.diffSummary([baseBranch]);
|
|
147
|
+
diffSummaryFiles = summary.files;
|
|
148
|
+
rawDiff = await git.diff([baseBranch]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Combine staged + unstaged changes against HEAD
|
|
152
|
+
const stagedSummary = await git.diffSummary(["--cached"]);
|
|
153
|
+
const unstagedSummary = await git.diffSummary();
|
|
154
|
+
// Merge results, preferring staged if both exist
|
|
155
|
+
const fileMap = new Map();
|
|
156
|
+
for (const f of unstagedSummary.files) {
|
|
157
|
+
fileMap.set(f.file, f);
|
|
158
|
+
}
|
|
159
|
+
for (const f of stagedSummary.files) {
|
|
160
|
+
const existing = fileMap.get(f.file);
|
|
161
|
+
if (existing) {
|
|
162
|
+
// Combine additions/deletions from both staged and unstaged
|
|
163
|
+
fileMap.set(f.file, {
|
|
164
|
+
...f,
|
|
165
|
+
insertions: f.insertions + existing.insertions,
|
|
166
|
+
deletions: f.deletions + existing.deletions,
|
|
167
|
+
changes: f.changes + existing.changes,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
fileMap.set(f.file, f);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
diffSummaryFiles = Array.from(fileMap.values());
|
|
175
|
+
// Get raw diff for hunk parsing
|
|
176
|
+
const stagedRaw = await git.diff(["--cached"]);
|
|
177
|
+
const unstagedRaw = await git.diff();
|
|
178
|
+
rawDiff = stagedRaw + "\n" + unstagedRaw;
|
|
179
|
+
}
|
|
180
|
+
// Filter out binary files and non-source artifacts
|
|
181
|
+
const sourceFiles = diffSummaryFiles.filter((f) => !f.binary && !f.file.includes("node_modules/") && !f.file.includes("dist/"));
|
|
182
|
+
const files = sourceFiles.map((f) => {
|
|
183
|
+
// simple-git provides status info in diffSummary via the status property on StatusResult,
|
|
184
|
+
// but DiffResultTextFile does not carry it. We infer from insertions/deletions.
|
|
185
|
+
let status = "modified";
|
|
186
|
+
if (f.deletions === 0 && f.insertions > 0) {
|
|
187
|
+
status = "added";
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
path: f.file,
|
|
191
|
+
status,
|
|
192
|
+
additions: f.insertions,
|
|
193
|
+
deletions: f.deletions,
|
|
194
|
+
hunks: parseHunks(rawDiff, f.file),
|
|
195
|
+
language: detectLanguage(f.file),
|
|
196
|
+
fileType: detectFileType(f.file),
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
// Augment with status from `git status` for more accurate added/deleted detection
|
|
200
|
+
const statusResult = await git.status();
|
|
201
|
+
const statusMap = new Map();
|
|
202
|
+
for (const f of statusResult.created)
|
|
203
|
+
statusMap.set(f, "A");
|
|
204
|
+
for (const f of statusResult.deleted)
|
|
205
|
+
statusMap.set(f, "D");
|
|
206
|
+
for (const f of statusResult.renamed)
|
|
207
|
+
statusMap.set(f.to, "R");
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const st = statusMap.get(file.path);
|
|
210
|
+
if (st) {
|
|
211
|
+
file.status = mapDiffStatus(st);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const addedCount = files.filter((f) => f.status === "added").length;
|
|
215
|
+
const modifiedCount = files.filter((f) => f.status === "modified").length;
|
|
216
|
+
const deletedCount = files.filter((f) => f.status === "deleted").length;
|
|
217
|
+
const summary = [
|
|
218
|
+
`${files.length} file(s) changed`,
|
|
219
|
+
addedCount > 0 ? `${addedCount} added` : null,
|
|
220
|
+
modifiedCount > 0 ? `${modifiedCount} modified` : null,
|
|
221
|
+
deletedCount > 0 ? `${deletedCount} deleted` : null,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(", ");
|
|
225
|
+
return {
|
|
226
|
+
files,
|
|
227
|
+
summary,
|
|
228
|
+
baseBranch: base,
|
|
229
|
+
headBranch: currentBranch,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=diff-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-analyzer.js","sourceRoot":"","sources":["../../src/analysis/diff-analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DiffResult, CodeScanResult, DependencyGraph, TestStrategy } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plan a testing strategy based on diff analysis, code scanning, and dependency graph.
|
|
4
|
+
*
|
|
5
|
+
* @param diff - Results from analyzeDiff
|
|
6
|
+
* @param scans - Results from scanCode for each changed file
|
|
7
|
+
* @param graph - Dependency graph from buildDependencyGraph
|
|
8
|
+
* @param projectRoot - Absolute path to the project root
|
|
9
|
+
*/
|
|
10
|
+
export declare function planStrategy(diff: DiffResult, scans: CodeScanResult[], graph: DependencyGraph, projectRoot: string): Promise<TestStrategy>;
|
|
11
|
+
//# sourceMappingURL=strategy-planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy-planner.d.ts","sourceRoot":"","sources":["../../src/analysis/strategy-planner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,EAWb,MAAM,mBAAmB,CAAC;AA8M3B;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,cAAc,EAAE,EACvB,KAAK,EAAE,eAAe,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAiIvB"}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { resolve, basename } from "path";
|
|
2
|
+
import { readFile, access } from "fs/promises";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
// ─── File type → test type mapping ──────────────────────────────
|
|
5
|
+
const FILE_TYPE_TEST_MAP = {
|
|
6
|
+
"api-route": ["unit", "api"],
|
|
7
|
+
"api-controller": ["unit", "api"],
|
|
8
|
+
"react-component": ["unit", "e2e-browser"],
|
|
9
|
+
"react-page": ["unit", "e2e-browser"],
|
|
10
|
+
"react-hook": ["unit"],
|
|
11
|
+
service: ["unit"],
|
|
12
|
+
utility: ["unit"],
|
|
13
|
+
"mobile-screen": ["unit", "e2e-mobile"],
|
|
14
|
+
"mobile-component": ["unit", "e2e-mobile"],
|
|
15
|
+
"desktop-window": ["unit", "e2e-desktop"],
|
|
16
|
+
"desktop-component": ["unit", "e2e-desktop"],
|
|
17
|
+
middleware: ["unit", "integration"],
|
|
18
|
+
model: ["unit"],
|
|
19
|
+
schema: ["unit"],
|
|
20
|
+
};
|
|
21
|
+
// ─── Test type → execution environment mapping ──────────────────
|
|
22
|
+
const TEST_TYPE_ENVIRONMENT = {
|
|
23
|
+
unit: "local",
|
|
24
|
+
integration: "local",
|
|
25
|
+
api: "local",
|
|
26
|
+
"e2e-browser": "browser",
|
|
27
|
+
"e2e-mobile": "mobile-simulator",
|
|
28
|
+
"e2e-desktop": "desktop-app",
|
|
29
|
+
snapshot: "local",
|
|
30
|
+
performance: "cloud-sandbox",
|
|
31
|
+
};
|
|
32
|
+
// ─── Estimated test counts by file type ─────────────────────────
|
|
33
|
+
const ESTIMATED_TESTS_PER_TYPE = {
|
|
34
|
+
unit: 5,
|
|
35
|
+
integration: 3,
|
|
36
|
+
api: 4,
|
|
37
|
+
"e2e-browser": 2,
|
|
38
|
+
"e2e-mobile": 2,
|
|
39
|
+
"e2e-desktop": 2,
|
|
40
|
+
snapshot: 1,
|
|
41
|
+
performance: 1,
|
|
42
|
+
};
|
|
43
|
+
// ─── Project detection helpers ──────────────────────────────────
|
|
44
|
+
async function fileExists(path) {
|
|
45
|
+
try {
|
|
46
|
+
await access(path);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readJson(path) {
|
|
54
|
+
try {
|
|
55
|
+
const content = await readFile(path, "utf-8");
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function detectPackageManager(root) {
|
|
63
|
+
if (await fileExists(resolve(root, "bun.lockb")))
|
|
64
|
+
return "bun";
|
|
65
|
+
if (await fileExists(resolve(root, "bun.lock")))
|
|
66
|
+
return "bun";
|
|
67
|
+
if (await fileExists(resolve(root, "pnpm-lock.yaml")))
|
|
68
|
+
return "pnpm";
|
|
69
|
+
if (await fileExists(resolve(root, "yarn.lock")))
|
|
70
|
+
return "yarn";
|
|
71
|
+
return "npm";
|
|
72
|
+
}
|
|
73
|
+
async function detectFramework(root) {
|
|
74
|
+
const pkg = await readJson(resolve(root, "package.json"));
|
|
75
|
+
if (!pkg)
|
|
76
|
+
return "unknown";
|
|
77
|
+
const deps = {
|
|
78
|
+
...pkg["dependencies"],
|
|
79
|
+
...pkg["devDependencies"],
|
|
80
|
+
};
|
|
81
|
+
// Order matters: more specific frameworks first
|
|
82
|
+
if (deps["@nestjs/core"])
|
|
83
|
+
return "nestjs";
|
|
84
|
+
if (deps["next"])
|
|
85
|
+
return "next";
|
|
86
|
+
if (deps["expo"])
|
|
87
|
+
return "expo";
|
|
88
|
+
if (deps["react-native"])
|
|
89
|
+
return "react-native";
|
|
90
|
+
if (deps["@tauri-apps/api"])
|
|
91
|
+
return "tauri";
|
|
92
|
+
if (deps["electron"])
|
|
93
|
+
return "electron";
|
|
94
|
+
if (deps["hono"])
|
|
95
|
+
return "hono";
|
|
96
|
+
if (deps["fastify"])
|
|
97
|
+
return "fastify";
|
|
98
|
+
if (deps["express"])
|
|
99
|
+
return "express";
|
|
100
|
+
if (deps["react"])
|
|
101
|
+
return "react";
|
|
102
|
+
return "none";
|
|
103
|
+
}
|
|
104
|
+
async function detectTestFramework(root) {
|
|
105
|
+
const pkg = await readJson(resolve(root, "package.json"));
|
|
106
|
+
if (!pkg)
|
|
107
|
+
return "unknown";
|
|
108
|
+
const deps = {
|
|
109
|
+
...pkg["dependencies"],
|
|
110
|
+
...pkg["devDependencies"],
|
|
111
|
+
};
|
|
112
|
+
if (deps["vitest"])
|
|
113
|
+
return "vitest";
|
|
114
|
+
if (deps["jest"] || deps["@jest/core"])
|
|
115
|
+
return "jest";
|
|
116
|
+
if (deps["playwright"] || deps["@playwright/test"])
|
|
117
|
+
return "playwright";
|
|
118
|
+
if (deps["cypress"])
|
|
119
|
+
return "cypress";
|
|
120
|
+
if (deps["detox"])
|
|
121
|
+
return "detox";
|
|
122
|
+
if (deps["mocha"])
|
|
123
|
+
return "mocha";
|
|
124
|
+
if (deps["pytest"])
|
|
125
|
+
return "pytest";
|
|
126
|
+
return "unknown";
|
|
127
|
+
}
|
|
128
|
+
async function detectExistingTests(root) {
|
|
129
|
+
const testFiles = await fg(["**/*.test.{ts,tsx,js,jsx}", "**/*.spec.{ts,tsx,js,jsx}", "**/__tests__/**/*.{ts,tsx,js,jsx}"], {
|
|
130
|
+
cwd: root,
|
|
131
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
132
|
+
});
|
|
133
|
+
const patterns = new Set();
|
|
134
|
+
for (const f of testFiles) {
|
|
135
|
+
if (f.includes(".test."))
|
|
136
|
+
patterns.add("*.test.*");
|
|
137
|
+
if (f.includes(".spec."))
|
|
138
|
+
patterns.add("*.spec.*");
|
|
139
|
+
if (f.includes("__tests__"))
|
|
140
|
+
patterns.add("__tests__/");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
hasTests: testFiles.length > 0,
|
|
144
|
+
patterns: Array.from(patterns),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function detectProjectInfo(root) {
|
|
148
|
+
const pkg = await readJson(resolve(root, "package.json"));
|
|
149
|
+
const name = pkg?.["name"] ?? basename(root);
|
|
150
|
+
const packageManager = await detectPackageManager(root);
|
|
151
|
+
const framework = await detectFramework(root);
|
|
152
|
+
const testFramework = await detectTestFramework(root);
|
|
153
|
+
const { hasTests, patterns } = await detectExistingTests(root);
|
|
154
|
+
// Detect primary language from tsconfig presence
|
|
155
|
+
const hasTsConfig = await fileExists(resolve(root, "tsconfig.json"));
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
root,
|
|
159
|
+
language: hasTsConfig ? "typescript" : "javascript",
|
|
160
|
+
framework,
|
|
161
|
+
testFramework,
|
|
162
|
+
packageManager,
|
|
163
|
+
hasExistingTests: hasTests,
|
|
164
|
+
existingTestPatterns: patterns,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ─── Plan generation ────────────────────────────────────────────
|
|
168
|
+
let planIdCounter = 0;
|
|
169
|
+
function generatePlanId() {
|
|
170
|
+
planIdCounter++;
|
|
171
|
+
return `plan_${planIdCounter.toString().padStart(3, "0")}`;
|
|
172
|
+
}
|
|
173
|
+
function determinePriority(filePath, changedFiles, graph) {
|
|
174
|
+
// Directly changed files are critical
|
|
175
|
+
if (changedFiles.has(filePath))
|
|
176
|
+
return "critical";
|
|
177
|
+
// Files that directly depend on a changed file are high priority
|
|
178
|
+
const node = graph.get(filePath);
|
|
179
|
+
if (node) {
|
|
180
|
+
for (const dep of node.dependsOn) {
|
|
181
|
+
if (changedFiles.has(dep))
|
|
182
|
+
return "high";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return "medium";
|
|
186
|
+
}
|
|
187
|
+
function buildTestTarget(filePath, scan) {
|
|
188
|
+
return {
|
|
189
|
+
files: [filePath],
|
|
190
|
+
functions: scan?.functions.filter((f) => f.isExported).map((f) => f.name) ?? [],
|
|
191
|
+
endpoints: scan?.endpoints ?? [],
|
|
192
|
+
components: scan?.components.map((c) => c.name) ?? [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Plan a testing strategy based on diff analysis, code scanning, and dependency graph.
|
|
197
|
+
*
|
|
198
|
+
* @param diff - Results from analyzeDiff
|
|
199
|
+
* @param scans - Results from scanCode for each changed file
|
|
200
|
+
* @param graph - Dependency graph from buildDependencyGraph
|
|
201
|
+
* @param projectRoot - Absolute path to the project root
|
|
202
|
+
*/
|
|
203
|
+
export async function planStrategy(diff, scans, graph, projectRoot) {
|
|
204
|
+
const root = resolve(projectRoot);
|
|
205
|
+
// Reset plan counter for deterministic IDs within a single strategy
|
|
206
|
+
planIdCounter = 0;
|
|
207
|
+
const project = await detectProjectInfo(root);
|
|
208
|
+
const scanMap = new Map();
|
|
209
|
+
for (const scan of scans) {
|
|
210
|
+
scanMap.set(scan.file, scan);
|
|
211
|
+
}
|
|
212
|
+
const changedPaths = new Set(diff.files.map((f) => f.path));
|
|
213
|
+
const plans = [];
|
|
214
|
+
// Generate plans for each changed file (skip deleted files and test files)
|
|
215
|
+
for (const file of diff.files) {
|
|
216
|
+
if (file.status === "deleted")
|
|
217
|
+
continue;
|
|
218
|
+
if (file.fileType === "test")
|
|
219
|
+
continue;
|
|
220
|
+
if (file.fileType === "config" || file.fileType === "style" || file.fileType === "migration")
|
|
221
|
+
continue;
|
|
222
|
+
const testTypes = FILE_TYPE_TEST_MAP[file.fileType] ?? ["unit"];
|
|
223
|
+
const scan = scanMap.get(file.path);
|
|
224
|
+
const priority = determinePriority(file.path, changedPaths, graph);
|
|
225
|
+
for (const testType of testTypes) {
|
|
226
|
+
const planId = generatePlanId();
|
|
227
|
+
const target = buildTestTarget(file.path, scan);
|
|
228
|
+
// Describe what we're testing
|
|
229
|
+
const targetDesc = target.endpoints.length > 0
|
|
230
|
+
? `${target.endpoints.length} endpoint(s)`
|
|
231
|
+
: target.components.length > 0
|
|
232
|
+
? `${target.components.length} component(s)`
|
|
233
|
+
: target.functions.length > 0
|
|
234
|
+
? `${target.functions.length} function(s)`
|
|
235
|
+
: "module";
|
|
236
|
+
plans.push({
|
|
237
|
+
id: planId,
|
|
238
|
+
type: testType,
|
|
239
|
+
target,
|
|
240
|
+
priority,
|
|
241
|
+
description: `${testType} tests for ${file.path} — ${targetDesc}`,
|
|
242
|
+
estimatedTests: ESTIMATED_TESTS_PER_TYPE[testType] ?? 3,
|
|
243
|
+
dependencies: [],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Also plan tests for files that depend on changed files (ripple effect)
|
|
248
|
+
for (const file of diff.files) {
|
|
249
|
+
if (file.status === "deleted")
|
|
250
|
+
continue;
|
|
251
|
+
const node = graph.get(file.path);
|
|
252
|
+
if (!node)
|
|
253
|
+
continue;
|
|
254
|
+
for (const dependentPath of node.dependedBy) {
|
|
255
|
+
// Skip if already in our changed files or if it's a test file
|
|
256
|
+
if (changedPaths.has(dependentPath))
|
|
257
|
+
continue;
|
|
258
|
+
if (/\.(test|spec)\./.test(dependentPath))
|
|
259
|
+
continue;
|
|
260
|
+
const depNode = graph.get(dependentPath);
|
|
261
|
+
if (!depNode)
|
|
262
|
+
continue;
|
|
263
|
+
// Only add unit tests for ripple-effect files
|
|
264
|
+
const planId = generatePlanId();
|
|
265
|
+
plans.push({
|
|
266
|
+
id: planId,
|
|
267
|
+
type: "unit",
|
|
268
|
+
target: {
|
|
269
|
+
files: [dependentPath],
|
|
270
|
+
functions: [],
|
|
271
|
+
endpoints: [],
|
|
272
|
+
components: [],
|
|
273
|
+
},
|
|
274
|
+
priority: "high",
|
|
275
|
+
description: `unit tests for ${dependentPath} (depends on changed file ${file.path})`,
|
|
276
|
+
estimatedTests: 3,
|
|
277
|
+
dependencies: [],
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Wire up dependencies: integration/api/e2e plans depend on their unit plan for the same file
|
|
282
|
+
const unitPlansByFile = new Map();
|
|
283
|
+
for (const plan of plans) {
|
|
284
|
+
if (plan.type === "unit" && plan.target.files.length > 0) {
|
|
285
|
+
unitPlansByFile.set(plan.target.files[0], plan.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const plan of plans) {
|
|
289
|
+
if (plan.type !== "unit" && plan.target.files.length > 0) {
|
|
290
|
+
const unitPlanId = unitPlansByFile.get(plan.target.files[0]);
|
|
291
|
+
if (unitPlanId) {
|
|
292
|
+
plan.dependencies.push(unitPlanId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Build execution phases: group non-dependent plans into parallel phases
|
|
297
|
+
const executionOrder = buildExecutionPhases(plans);
|
|
298
|
+
// Estimate total duration (rough: 2s per test for unit, 5s for integration, 10s for e2e)
|
|
299
|
+
const durationPerType = {
|
|
300
|
+
unit: 2,
|
|
301
|
+
integration: 5,
|
|
302
|
+
api: 5,
|
|
303
|
+
"e2e-browser": 10,
|
|
304
|
+
"e2e-mobile": 15,
|
|
305
|
+
"e2e-desktop": 15,
|
|
306
|
+
snapshot: 1,
|
|
307
|
+
performance: 20,
|
|
308
|
+
};
|
|
309
|
+
const estimatedDuration = plans.reduce((total, plan) => total + plan.estimatedTests * (durationPerType[plan.type] ?? 5), 0);
|
|
310
|
+
return {
|
|
311
|
+
project,
|
|
312
|
+
plans,
|
|
313
|
+
executionOrder,
|
|
314
|
+
estimatedDuration,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Group test plans into parallel execution phases using topological ordering.
|
|
319
|
+
* Plans with no dependencies run in the earliest phase. Plans with dependencies
|
|
320
|
+
* run in the phase after their last dependency completes.
|
|
321
|
+
*/
|
|
322
|
+
function buildExecutionPhases(plans) {
|
|
323
|
+
const planMap = new Map();
|
|
324
|
+
for (const plan of plans) {
|
|
325
|
+
planMap.set(plan.id, plan);
|
|
326
|
+
}
|
|
327
|
+
// Compute phase number for each plan
|
|
328
|
+
const phaseAssignment = new Map();
|
|
329
|
+
const visited = new Set();
|
|
330
|
+
function getPhase(planId) {
|
|
331
|
+
if (phaseAssignment.has(planId))
|
|
332
|
+
return phaseAssignment.get(planId);
|
|
333
|
+
if (visited.has(planId))
|
|
334
|
+
return 0; // Circular dependency guard
|
|
335
|
+
visited.add(planId);
|
|
336
|
+
const plan = planMap.get(planId);
|
|
337
|
+
if (!plan || plan.dependencies.length === 0) {
|
|
338
|
+
phaseAssignment.set(planId, 0);
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
let maxDepPhase = 0;
|
|
342
|
+
for (const depId of plan.dependencies) {
|
|
343
|
+
maxDepPhase = Math.max(maxDepPhase, getPhase(depId) + 1);
|
|
344
|
+
}
|
|
345
|
+
phaseAssignment.set(planId, maxDepPhase);
|
|
346
|
+
return maxDepPhase;
|
|
347
|
+
}
|
|
348
|
+
for (const plan of plans) {
|
|
349
|
+
getPhase(plan.id);
|
|
350
|
+
}
|
|
351
|
+
// Group plans by phase
|
|
352
|
+
const phaseGroups = new Map();
|
|
353
|
+
for (const [planId, phase] of phaseAssignment) {
|
|
354
|
+
const group = phaseGroups.get(phase) ?? [];
|
|
355
|
+
group.push(planId);
|
|
356
|
+
phaseGroups.set(phase, group);
|
|
357
|
+
}
|
|
358
|
+
// Sort phases and determine environment per phase
|
|
359
|
+
const sortedPhases = Array.from(phaseGroups.entries()).sort(([a], [b]) => a - b);
|
|
360
|
+
return sortedPhases.map(([phase, planIds]) => {
|
|
361
|
+
// Determine the heaviest environment needed in this phase
|
|
362
|
+
const environments = planIds
|
|
363
|
+
.map((id) => {
|
|
364
|
+
const plan = planMap.get(id);
|
|
365
|
+
return plan ? TEST_TYPE_ENVIRONMENT[plan.type] : "local";
|
|
366
|
+
})
|
|
367
|
+
.filter((e) => e !== undefined);
|
|
368
|
+
// Pick the most "heavyweight" environment for the phase
|
|
369
|
+
const envPriority = [
|
|
370
|
+
"mobile-simulator",
|
|
371
|
+
"desktop-app",
|
|
372
|
+
"browser",
|
|
373
|
+
"cloud-sandbox",
|
|
374
|
+
"local",
|
|
375
|
+
];
|
|
376
|
+
const environment = envPriority.find((e) => environments.includes(e)) ?? "local";
|
|
377
|
+
return {
|
|
378
|
+
phase,
|
|
379
|
+
plans: planIds,
|
|
380
|
+
environment,
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=strategy-planner.js.map
|