covgate 0.2.0 → 0.2.2
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/README.md +52 -17
- package/docs/ai-workflow.md +1 -0
- package/docs/coverage-viewer.png +0 -0
- package/docs/design.md +1 -1
- package/docs/setup.md +30 -28
- package/package.json +1 -1
- package/schema/covgate.schema.json +4 -2
- package/src/args.js +14 -0
- package/src/cli.js +41 -6
- package/src/config.js +2 -2
- package/src/coverage/model.js +3 -0
- package/src/gate.js +48 -4
- package/src/git.js +86 -0
- package/src/index.js +1 -0
- package/src/report/build.js +13 -3
- package/src/report/html.js +257 -0
- package/src/report/index.js +21 -3
- package/src/report/markdown.js +3 -1
- package/src/report/text.js +5 -2
- package/src/scope.js +33 -3
- package/src/setup/agent.js +3 -1
- package/src/setup/config.js +43 -26
- package/src/setup/index.js +41 -13
- package/src/setup/project.js +82 -7
- package/templates/AGENTS.md.snippet.md +1 -0
- package/templates/covgate.config.json +2 -1
package/src/setup/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
detectFramework,
|
|
9
9
|
detectPackageManager,
|
|
10
10
|
detectProfile,
|
|
11
|
+
detectScopeSpecs,
|
|
11
12
|
detectSourceGlob,
|
|
12
13
|
displayPath,
|
|
13
14
|
installVitest,
|
|
@@ -108,23 +109,40 @@ export async function runSetup(options = {}) {
|
|
|
108
109
|
testCommand = "npm run test:coverage";
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
112
|
+
let scopeSpecs;
|
|
113
|
+
if (options.profile || options.source) {
|
|
114
|
+
const profile = options.profile || detectProfile(root, pkg);
|
|
115
|
+
if (!["client", "server"].includes(profile)) {
|
|
116
|
+
throw new SetupError("profile 必须是 client 或 server");
|
|
117
|
+
}
|
|
118
|
+
const sourceGlob = normalizeGlob(
|
|
119
|
+
root,
|
|
120
|
+
options.source || detectSourceGlob(root, profile) || ""
|
|
121
|
+
);
|
|
122
|
+
scopeSpecs = sourceGlob
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
name: profile,
|
|
126
|
+
profile,
|
|
127
|
+
label: profile === "client" ? "客户端" : "服务端",
|
|
128
|
+
sourceGlob
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
: [];
|
|
132
|
+
} else {
|
|
133
|
+
scopeSpecs = detectScopeSpecs(root, pkg);
|
|
114
134
|
}
|
|
115
|
-
const sourceGlob = normalizeGlob(root, options.source || detectSourceGlob(root, profile) || "");
|
|
116
135
|
const coveragePath = normalizeGlob(root, options.coverage || DEFAULT_COVERAGE_PATH);
|
|
117
136
|
const configPath = path.join(root, "covgate.config.json");
|
|
118
137
|
let configWritten = false;
|
|
119
138
|
|
|
120
|
-
if (options.noConfig !== true && testCommand &&
|
|
139
|
+
if (options.noConfig !== true && testCommand && scopeSpecs.length > 0) {
|
|
121
140
|
if (existsSync(configPath) && options.force !== true) {
|
|
122
141
|
log("[covgate] " + displayPath(root, configPath) + " 已存在,保留现有配置");
|
|
123
142
|
} else {
|
|
124
143
|
const config = await buildStarterConfig({
|
|
125
144
|
root,
|
|
126
|
-
|
|
127
|
-
sourceGlob,
|
|
145
|
+
scopes: scopeSpecs,
|
|
128
146
|
command: testCommand,
|
|
129
147
|
coveragePath
|
|
130
148
|
});
|
|
@@ -137,9 +155,9 @@ export async function runSetup(options = {}) {
|
|
|
137
155
|
"[covgate] 生成 " +
|
|
138
156
|
displayPath(root, configPath) +
|
|
139
157
|
"(" +
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
scopeSpecs
|
|
159
|
+
.map((scope) => scope.profile + ":" + scope.sourceGlob)
|
|
160
|
+
.join(",") +
|
|
143
161
|
")"
|
|
144
162
|
);
|
|
145
163
|
if (framework.id && !coverageScriptFor(framework.id) && !options.testCommand) {
|
|
@@ -150,7 +168,7 @@ export async function runSetup(options = {}) {
|
|
|
150
168
|
if (!testCommand) {
|
|
151
169
|
log("[covgate] 未找到可用的覆盖率测试命令,跳过 covgate.config.json。");
|
|
152
170
|
}
|
|
153
|
-
if (
|
|
171
|
+
if (scopeSpecs.length === 0) {
|
|
154
172
|
log("[covgate] 未找到源码目录,跳过 covgate.config.json;请用 --source 指定。");
|
|
155
173
|
}
|
|
156
174
|
}
|
|
@@ -171,8 +189,9 @@ export async function runSetup(options = {}) {
|
|
|
171
189
|
packageManager,
|
|
172
190
|
framework: framework.id || null,
|
|
173
191
|
installedFramework,
|
|
174
|
-
profile,
|
|
175
|
-
sourceGlob: sourceGlob
|
|
192
|
+
profile: scopeSpecs.length === 1 ? scopeSpecs[0].profile : null,
|
|
193
|
+
sourceGlob: scopeSpecs.length === 1 ? scopeSpecs[0].sourceGlob : null,
|
|
194
|
+
scopes: scopeSpecs,
|
|
176
195
|
testCommand: testCommand || null,
|
|
177
196
|
coveragePath,
|
|
178
197
|
configPath: configWritten ? configPath : existsSync(configPath) ? configPath : null,
|
|
@@ -191,7 +210,16 @@ export async function runSetup(options = {}) {
|
|
|
191
210
|
if (summary.framework) {
|
|
192
211
|
log("[covgate] 测试框架: " + (framework.label || summary.framework));
|
|
193
212
|
}
|
|
213
|
+
if (scopeSpecs.length > 0) {
|
|
214
|
+
log(
|
|
215
|
+
"[covgate] Scope: " +
|
|
216
|
+
scopeSpecs
|
|
217
|
+
.map((scope) => scope.name + "/" + scope.profile + " (" + scope.sourceGlob + ")")
|
|
218
|
+
.join(", ")
|
|
219
|
+
);
|
|
220
|
+
}
|
|
194
221
|
log("[covgate] 运行门禁: " + (summary.dryRun ? "npx covgate run" : "npx --no-install covgate run"));
|
|
222
|
+
log("[covgate] 注意:CovGate 不会生成或补写测试;项目必须为行为变更补真实测试后才能通过门禁。");
|
|
195
223
|
if (!options.noAgent) {
|
|
196
224
|
log("[covgate] AI 接入: AGENTS.md 与 scripts/covgate-agent-gate.*");
|
|
197
225
|
}
|
package/src/setup/project.js
CHANGED
|
@@ -156,13 +156,27 @@ export function detectFramework(pkg) {
|
|
|
156
156
|
*/
|
|
157
157
|
export function detectProfile(root, pkg) {
|
|
158
158
|
const deps = directDependencyNames(pkg);
|
|
159
|
-
|
|
159
|
+
const htmlLocations = [
|
|
160
|
+
"index.html",
|
|
161
|
+
"src/index.html",
|
|
162
|
+
"app/index.html",
|
|
163
|
+
"renderer/index.html"
|
|
164
|
+
];
|
|
165
|
+
if (htmlLocations.some((file) => existsSync(path.join(root, file)))) return "client";
|
|
160
166
|
if (pkg?.browser) return "client";
|
|
167
|
+
if (deps.has("electron") || deps.has("electron-builder")) return "client";
|
|
161
168
|
if ([...BROWSER_DEPS].some((dependency) => deps.has(dependency))) return "client";
|
|
162
169
|
if ([...SERVER_DEPS].some((dependency) => deps.has(dependency))) return "server";
|
|
163
170
|
return "server";
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} directory
|
|
175
|
+
*/
|
|
176
|
+
export function sourceGlobForDir(directory) {
|
|
177
|
+
return directory + "/**/*." + SOURCE_EXT;
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
/**
|
|
167
181
|
* @param {string} root
|
|
168
182
|
* @param {"client" | "server"} profile
|
|
@@ -170,33 +184,94 @@ export function detectProfile(root, pkg) {
|
|
|
170
184
|
*/
|
|
171
185
|
export function detectSourceGlob(root, profile) {
|
|
172
186
|
if (existsSync(path.join(root, "src"))) {
|
|
173
|
-
return "src
|
|
187
|
+
return sourceGlobForDir("src");
|
|
174
188
|
}
|
|
175
189
|
if (profile === "client" && existsSync(path.join(root, "app.js"))) {
|
|
176
190
|
return "app.js";
|
|
177
191
|
}
|
|
178
192
|
const candidate = profile === "client" ? "client" : "server";
|
|
179
193
|
if (existsSync(path.join(root, candidate))) {
|
|
180
|
-
return candidate
|
|
194
|
+
return sourceGlobForDir(candidate);
|
|
181
195
|
}
|
|
182
196
|
if (existsSync(path.join(root, "app"))) {
|
|
183
|
-
return "app
|
|
197
|
+
return sourceGlobForDir("app");
|
|
184
198
|
}
|
|
185
199
|
if (existsSync(path.join(root, "lib"))) {
|
|
186
|
-
return "lib
|
|
200
|
+
return sourceGlobForDir("lib");
|
|
187
201
|
}
|
|
188
202
|
return null;
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Electron apps usually have a renderer tree (src) and a main-process tree
|
|
207
|
+
* (desktop). Treating src as server (the old setup default) was wrong.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} root
|
|
210
|
+
* @param {any} pkg
|
|
211
|
+
*/
|
|
212
|
+
export function detectElectronApp(root, pkg) {
|
|
213
|
+
const deps = directDependencyNames(pkg);
|
|
214
|
+
if (deps.has("electron") || deps.has("electron-builder") || deps.has("@electron/packager")) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
const main = typeof pkg?.main === "string" ? pkg.main.replaceAll("\\", "/") : "";
|
|
218
|
+
return main.startsWith("desktop/") && existsSync(path.join(root, "src"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} root
|
|
223
|
+
* @param {any} pkg
|
|
224
|
+
* @returns {Array<{ name: string, profile: "client" | "server", label: string, sourceGlob: string }>}
|
|
225
|
+
*/
|
|
226
|
+
export function detectScopeSpecs(root, pkg) {
|
|
227
|
+
if (detectElectronApp(root, pkg)) {
|
|
228
|
+
const clientDir = ["src", "app", "renderer", "client"].find((directory) =>
|
|
229
|
+
existsSync(path.join(root, directory))
|
|
230
|
+
);
|
|
231
|
+
const serverDir = ["desktop", "electron", "main"].find((directory) =>
|
|
232
|
+
existsSync(path.join(root, directory))
|
|
233
|
+
);
|
|
234
|
+
const specs = [];
|
|
235
|
+
if (clientDir) {
|
|
236
|
+
specs.push({
|
|
237
|
+
name: "client",
|
|
238
|
+
profile: "client",
|
|
239
|
+
label: "桌面客户端",
|
|
240
|
+
sourceGlob: sourceGlobForDir(clientDir)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (serverDir) {
|
|
244
|
+
specs.push({
|
|
245
|
+
name: "server",
|
|
246
|
+
profile: "server",
|
|
247
|
+
label: "桌面主进程",
|
|
248
|
+
sourceGlob: sourceGlobForDir(serverDir)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (specs.length > 0) return specs;
|
|
252
|
+
}
|
|
253
|
+
const profile = detectProfile(root, pkg);
|
|
254
|
+
const sourceGlob = detectSourceGlob(root, profile);
|
|
255
|
+
if (!sourceGlob) return [];
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
name: profile,
|
|
259
|
+
profile,
|
|
260
|
+
label: profile === "client" ? "客户端" : "服务端",
|
|
261
|
+
sourceGlob
|
|
262
|
+
}
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
191
266
|
/**
|
|
192
267
|
* @param {"vitest" | "jest" | string} frameworkId
|
|
193
268
|
*/
|
|
194
269
|
export function coverageScriptFor(frameworkId) {
|
|
195
270
|
if (frameworkId === "vitest") {
|
|
196
|
-
return "vitest run --coverage --coverage.reporter=json --coverage.reporter=lcov
|
|
271
|
+
return "vitest run --coverage --coverage.reporter=json --coverage.reporter=lcov";
|
|
197
272
|
}
|
|
198
273
|
if (frameworkId === "jest") {
|
|
199
|
-
return "jest --coverage --coverageReporters=json --coverageReporters=lcov
|
|
274
|
+
return "jest --coverage --coverageReporters=json --coverageReporters=lcov";
|
|
200
275
|
}
|
|
201
276
|
return null;
|
|
202
277
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- 任何代码修改完成后,必须运行 `npx --no-install covgate run --json`;**退出码 0 才能结束任务**。
|
|
4
4
|
- 退出码 `1`:补测试或修实现;`2`:修配置;`3`:修测试命令/覆盖率报告。不要绕过。
|
|
5
5
|
- 禁止降低 `covgate.config.json` 中的门槛、扩大 exclude、删除豁免检查来让门禁通过。任何配置改动都必须由人类评审。
|
|
6
|
+
- **行为变更必须新增或更新覆盖该行为的真实测试**;不能把“门禁失败”当作完成理由,也不得用空报告、`--passWithNoTests`、ignore 或降低门槛绕过。
|
|
6
7
|
- 禁止 `istanbul ignore`、`c8 ignore`、`v8 ignore`、`node:coverage ignore`。
|
|
7
8
|
- 确实不可达的代码使用 `covgate-ignore-next-line`、`covgate-ignore-start/end` 或 `covgate-ignore-file`,并写具体原因、owner 和 expires。
|
|
8
9
|
- 新增或修改的源码必须出现在覆盖率报告中;`sources.include` 是范围事实来源。
|