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.
@@ -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
- const profile = options.profile || detectProfile(root, pkg);
112
- if (!["client", "server"].includes(profile)) {
113
- throw new SetupError("profile 必须是 client 或 server");
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 && sourceGlob) {
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
- profile,
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
- profile +
141
- ",源码 " +
142
- sourceGlob +
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 (!sourceGlob) {
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 || null,
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
  }
@@ -156,13 +156,27 @@ export function detectFramework(pkg) {
156
156
  */
157
157
  export function detectProfile(root, pkg) {
158
158
  const deps = directDependencyNames(pkg);
159
- if (existsSync(path.join(root, "index.html"))) return "client";
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/**/*." + SOURCE_EXT;
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 + "/**/*." + SOURCE_EXT;
194
+ return sourceGlobForDir(candidate);
181
195
  }
182
196
  if (existsSync(path.join(root, "app"))) {
183
- return "app/**/*." + SOURCE_EXT;
197
+ return sourceGlobForDir("app");
184
198
  }
185
199
  if (existsSync(path.join(root, "lib"))) {
186
- return "lib/**/*." + SOURCE_EXT;
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 --passWithNoTests";
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 --passWithNoTests";
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` 是范围事实来源。
@@ -92,7 +92,8 @@
92
92
  "dir": ".covgate",
93
93
  "formats": [
94
94
  "json",
95
- "markdown"
95
+ "markdown",
96
+ "html"
96
97
  ]
97
98
  }
98
99
  }