dsh-notify-say 0.4.45 → 0.4.46

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/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
3
3
  > 本文件由 `tools/release-build.ps1` 从开发提交自动生成,**请勿手工编辑**。
4
4
  > 只列面向使用者的变更(新增功能与修复),不含内部维护提交。
5
5
 
6
+ ## v0.4.46 — 2026-10-18
7
+
8
+ - fix(test): v0.4.46 —— 修「本地绿、CI 红」+ 发版流程补发布包内测试
9
+
6
10
  ## v0.4.45 — 2026-10-18
7
11
 
8
12
  - feat(release): v0.4.45 —— CHANGELOG 自动生成 + 垃圾文件闸
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-say",
3
- "version": "0.4.45",
3
+ "version": "0.4.46",
4
4
  "description": "Desktop notifications for DSH Desktop that actually speak: Windows TTS out of the box (zero download), VOICEVOX voice packs optional, a selectable system-sound fallback, and an idle gate that waits for running subagents before saying the turn is done. Includes a settings-page GUI (sound source, per-event overrides, jump-back-to-session, cooldowns) and a self hot-reload for development.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -203,53 +203,55 @@ test('D5: index.js 有重载漏报诊断', () => {
203
203
  assert.match(INDEX, /Symbol\.for\('dsh-notify-say\.eventCount'\)/,
204
204
  'key 应带插件名(避免与其它插件冲突)');
205
205
  });
206
+ // ══ 打包与变更记录(v0.4.45)════════════════════════════════════════════
207
+ //
208
+ // ⚠️ 一个重要区别:`tools/release-build.ps1` 是**维护者工具**,
209
+ // 不随发布包分发(发布包的 tools\ 里只有 notify-config.ps1)。
210
+ // 所以检查它的那些断言在发布包里必须 skip,否则 CI 必然红。
211
+ //(v0.4.45 首跑 CI 就是这么红的:D6/D7/D8 去读一个不存在的文件。)
212
+ const RELEASE_BUILD = join(ROOT, 'tools', 'release-build.ps1');
213
+ const hasReleaseBuild = existsSync(RELEASE_BUILD);
214
+ const SKIP_DEVONLY = hasReleaseBuild ? false : 'release-build.ps1 是维护者工具,不在发布包内';
215
+
206
216
  // ── D6: 打包不得带临时/备份文件(fail-closed)──────────────────────────
207
- test('D6: release-build 有垃圾文件闸(.bak/.tmp 不得进发布包)', () => {
217
+ test('D6: release-build 有垃圾文件闸(.bak/.tmp 不得进发布包)', { skip: SKIP_DEVONLY }, () => {
208
218
  // ⚠️ 2026-10-18 真实事故:我把 mutation 测试用的
209
219
  // `lib/dev-reload.js.bak` 忘在源码目录里,release-build 把它一起
210
220
  // 复制进发布包 —— **而且已经推到 GitHub 与 npm**。
211
221
  //
212
222
  // 内容无害,但发布物里出现 .bak 是明显疏漏:用户疑惑、包体积白涨、
213
- // 而且说明打包流程没检查"目录里有没有残留"。
214
- //
215
- // 所以加一道 fail-closed 的闸:宁可中止构建,也不发带垃圾的包。
216
- const rb = readFileSync(join(ROOT, 'tools', 'release-build.ps1'), 'utf8');
223
+ // 而且说明打包流程没检查「目录里有没有残留」。
224
+ const rb = readFileSync(RELEASE_BUILD, 'utf8');
217
225
 
218
- assert.match(rb, /junkPattern/,
219
- '应有垃圾文件模式定义');
226
+ assert.match(rb, /junkPattern/, '应有垃圾文件模式定义');
220
227
  assert.match(rb, /\.\(bak\|tmp\|orig\|rej\|swp\)/,
221
228
  '应覆盖常见编辑器/工具残留(.bak/.tmp/.orig/.rej/.swp)');
222
- assert.match(rb, /exit 1/,
223
- '发现垃圾文件必须中止构建(fail-closed),不能只警告');
229
+ assert.match(rb, /exit 1/, '发现垃圾文件必须中止构建(fail-closed),不能只警告');
224
230
 
225
- // 闸门要在"复制完成之后"才有效 —— 检查位置在 CHANGELOG 之前
226
231
  const guardAt = rb.indexOf('$junkPattern');
227
232
  const clAt = rb.indexOf('生成 CHANGELOG');
228
233
  assert.ok(guardAt > 0 && clAt > 0 && guardAt < clAt,
229
234
  '垃圾闸应在复制完成后、写 CHANGELOG 之前');
230
235
  });
231
236
 
232
- // ── D7: CHANGELOG 过滤器必须挡住内部内容 ────────────────────────────────
233
- test('D7: CHANGELOG 生成器过滤内部标记与 hash', () => {
237
+ // ── D7: CHANGELOG 过滤器必须挡住内部内容 ──────────────────────────────
238
+ test('D7: CHANGELOG 生成器过滤内部标记与 hash', { skip: SKIP_DEVONLY }, () => {
234
239
  // 公开仓库只有 release: vX.Y.Z 快照,外部看不到每次改了什么。
235
240
  // 但**也不能原样输出开发提交标题** —— 实测早期标题是内部笔记风格:
236
241
  // · CARD:#7849 跟进评论回写
237
242
  // · RECOVERY.md 救援说明(三级回滚…)
238
243
  // · deploy-snapshots/desktop-profile-pre-...
239
244
  // · a306b8d / b9d83e1(私有仓库 hash)
240
- const rb = readFileSync(join(ROOT, 'tools', 'release-build.ps1'), 'utf8');
245
+ const rb = readFileSync(RELEASE_BUILD, 'utf8');
241
246
 
242
- assert.match(rb, /publicTypes/,
243
- '应只保留面向使用者的提交类型');
247
+ assert.match(rb, /publicTypes/, '应只保留面向使用者的提交类型');
244
248
  for (const t of ['feat', 'fix', 'perf', 'refactor']) {
245
249
  assert.match(rb, new RegExp(`'${t}'`), `publicTypes 应含 ${t}`);
246
250
  }
247
- // docs/test/chore/ci 是内部维护,不应出现在公开变更记录里
248
251
  assert.doesNotMatch(rb, /publicTypes = @\([^)]*'(docs|test|chore|ci)'/,
249
252
  'docs/test/chore/ci 不应算面向使用者的类型');
250
253
 
251
- assert.match(rb, /internalMarkers/,
252
- '应有内部标记黑名单');
254
+ assert.match(rb, /internalMarkers/, '应有内部标记黑名单');
253
255
  for (const m of ['CARD', 'RECOVERY', '交接备忘', 'deploy-snapshots']) {
254
256
  assert.ok(rb.includes(m),
255
257
  `internalMarkers 应含 ${m} —— 实测这些标记会带出内部资料引用`);
@@ -259,28 +261,80 @@ test('D7: CHANGELOG 生成器过滤内部标记与 hash', () => {
259
261
  '应剥掉 7~40 位十六进制串(私有仓库 hash 在公开仓库里指不到东西)');
260
262
  });
261
263
 
262
- // ── D8: CHANGELOG.md 必须在 npm 的 files 白名单里 ───────────────────────
263
- test('D8: CHANGELOG.md 随包分发,且内部资料不随包', () => {
264
- // ⚠️ 注意:**源 package.json 与发布包 package.json 是两份不同的东西**。
265
- //
266
- // 源的 `files` 服务于本地 link 安装(甚至列了 CARD.md / RECOVERY.md);
267
- // 发布包的 `files` 由 release-build.ps1 重写 —— 那才是 npm 用户拿到的。
268
- //(我第一版拿源 package.json 去断言,是搞错了对象。)
269
- const rb = readFileSync(join(ROOT, 'tools', 'release-build.ps1'), 'utf8');
264
+ // ── D8: 已发布的 CHANGELOG 本身要干净(这条在两种环境都跑)────────────
265
+ test('D8: 随包发布的 CHANGELOG 不含内部标记 / hash / 本机路径', () => {
266
+ // 这条查的是**产物**而不是生成器,所以在发布包里也能跑 ——
267
+ // 而且它比"检查生成器源码"更接近真实:用户拿到的就是这份文件。
268
+ const cl = join(ROOT, 'CHANGELOG.md');
269
+ if (!existsSync(cl)) return; // 开发目录没有(它是构建产物),跳过
270
+
271
+ const c = readFileSync(cl, 'utf8');
272
+
273
+ // 内部资料标记:提到这些说明过滤失效了
274
+ for (const m of ['CARD', 'RECOVERY', '交接备忘', 'deploy-snapshots', 'app.asar', '待做清单']) {
275
+ assert.ok(!c.includes(m),
276
+ `CHANGELOG 里出现了内部标记「${m}」—— 过滤失效,会带出内部资料引用`);
277
+ }
278
+
279
+ // 私有仓库的提交 hash 在公开仓库里指不到任何东西,反而误导
280
+ const hex = c.match(/\b[0-9a-f]{7,40}\b/g) || [];
281
+ assert.deepEqual(hex, [],
282
+ `CHANGELOG 里出现了疑似提交 hash:${hex.slice(0, 5).join(', ')}`);
283
+
284
+ // 本机绝对路径
285
+ assert.doesNotMatch(c, /[A-Za-z]:\\\\/, '不得含本机绝对路径');
286
+
287
+ // 得有实际内容(防"生成器坏了、产出一个空文件")
288
+ assert.match(c, /^## v\d+\.\d+\.\d+/m, 'CHANGELOG 应含至少一个版本节');
289
+ });
290
+
291
+ // ── D8b: 生成器配置(仅开发目录)───────────────────────────────────────
292
+ test('D8b: CHANGELOG 由构建脚本生成,且内部资料不进发布包', { skip: SKIP_DEVONLY }, () => {
293
+ // 注意:**源 package.json 与发布包 package.json 是两份不同的东西**。
294
+ // 源的 files 服务于本地 link 安装(甚至列了 CARD.md / RECOVERY.md);
295
+ // 发布包那份由 release-build.ps1 重写 —— 那才是 npm 用户拿到的。
296
+ const rb = readFileSync(RELEASE_BUILD, 'utf8');
270
297
 
271
- // 1) CHANGELOG.md 必须进发布包的 files(否则 npm 用户看不到变更记录)
272
298
  assert.match(rb, /'CHANGELOG\.md',/,
273
299
  'CHANGELOG.md 必须出现在 release-build 写出的 files 数组里 —— '
274
300
  + '同类事故:THIRD-PARTY-NOTICES.md 曾漏了 files,导致 npm 包没有任何第三方声明');
275
301
 
276
- // 2) 内部资料不得进发布包的 files
277
302
  const filesBlock = rb.slice(rb.indexOf('files'), rb.indexOf('keywords'));
278
303
  for (const internal of ['CARD.md', 'RECOVERY.md']) {
279
304
  assert.ok(!filesBlock.includes(internal),
280
305
  `${internal} 是内部资料,不得出现在发布包的 files 里`);
281
306
  }
282
307
 
283
- // 3) 它必须真的被"生成"出来,而不是靠复制
284
308
  assert.match(rb, /WriteAllText\(\(Join-Path \$Destination 'CHANGELOG\.md'\)/,
285
309
  'CHANGELOG.md 应由构建脚本生成到目标目录(不是从源码目录复制)');
286
310
  });
311
+
312
+ // ── D9: 发版流程必须在**发布包里**跑一次测试 ────────────────────────────
313
+ test('D9: release.ps1 在构建后、推送前,于发布包内跑测试', { skip: SKIP_DEVONLY }, () => {
314
+ // ⚠️ 2026-10-18 真实代价:D6/D7/D8 去读 `tools/release-build.ps1`,
315
+ // 而那是**维护者工具、不在发布包里** ——
316
+ // 于是开发目录全绿、推上去 CI 三平台全红。
317
+ //
318
+ // 根因不是那三条测试写错,而是**发版流程只在开发目录跑测试**,
319
+ // 而 CI 跑的是发布包(公开仓库本身就是发布包)。两者文件集不同。
320
+ //
321
+ // 修法:步骤 3 重建发布包之后、步骤 4 推送之前,再跑一次测试。
322
+ // 这样"本地绿、线上红"在推之前就会暴露。
323
+ const rp = readFileSync(join(ROOT, 'tools', 'release.ps1'), 'utf8');
324
+
325
+ const step35 = rp.indexOf('3.5 在发布包里跑测试');
326
+ const step3 = rp.indexOf("Write-Step '3. 重建发布包'");
327
+ const step4 = rp.indexOf("Write-Step '4. 提交并推送 GitHub'");
328
+
329
+ assert.ok(step3 > 0 && step35 > 0 && step4 > 0,
330
+ 'release.ps1 应有步骤 3 / 3.5 / 4');
331
+ assert.ok(step35 > step3 && step35 < step4,
332
+ '3.5 必须在「重建发布包」之后、「推送 GitHub」之前 —— '
333
+ + '顺序错了就起不到拦截作用');
334
+
335
+ // 必须在发布包目录里跑,且失败要中止
336
+ assert.match(rp, /Push-Location \$ReleaseDir/,
337
+ '应在发布包目录里执行');
338
+ assert.match(rp, /throw 'release-package tests failed'/,
339
+ '发布包内测试失败必须中止发版(fail-closed)');
340
+ });