test(工具链): ★★★ stripComments 两趟正则把真代码当注释吃掉 137 行 —— 改单趟扫描

**现象**:harmony-admin 那条「服务端要注册 GET /auth/me」报红,而
server/cmd/server/main.go:194 **明明写着** r.Get("/auth/me", handler.Me)。

**真因**(不是服务端写错,是读取器错了):
    main.go:78   // 与 /api/v1/agent/* 完全同一份代码
                                          ↑ 这个 /* 在 // 里面
stripComments 原来是**两趟正则**(先块 {/\*[\s\S]*?\*\//g}、后行),
两趟**互相看不见对方** ⇒ 块注释那趟在**还没删行注释**的文本上看到那个 /*,
当块注释开头,一路找下一个 */(在 :214)⇒ **137 行 / 37 条路由注册**
被当注释抹掉,含它正在断言的 r.Get("/auth/me", …)。
全仓另有 12 处同样写法(/me/*、/assets/*、plugins/*…)。

⇒ 失效形状是「**读取器静默少给一段真代码**」(不抛错、不警告),
  症状却出现在**被测对象**上 —— 看起来像"服务端把路由删了"。

**修法**:单趟字符扫描,且状态只用源码(注释内部不参与字符串状态)。
并**补上正则字面量**这一条 —— 漏认的方向是**假绿**:
harmony-device.mjs:59 的 /"bundleName"\s*:\s*"([^"]+)"/ 有 4 个引号(奇数),
打开的"字符串"永不闭合 ⇒ 后面所有注释被当字符串跳过。

五条性质逐条实测:① 行号不变 ② 'http://…' 字符串不被腰斩
③ 注释里的引号不污染状态 ④ 正则字面量被当正则 ⑤ 块注释连文本一起删。
变异测试:把旧实现放回去 ⇒ harmony-admin 确实变红(确认真修好了,
而不是"改的东西恰好没人用")。

**顺带修的三处判据自身缺陷**(都不是源码问题):
· inbox-fallback-poll:原断言钉的是**行尾注释里的字**
  (删掉注释照样绿、塞进 await fetchInbox() 也照样绿)⇒ 改为取
  if (lastTotal === null) { … } 整个分支做结构断言
· inbox-fallback-poll / sse-credentials:裸 readFileSync ⇒ 具名 code()
  (换成更严格的读取后变红,暴露的是判据本来就在判错的对象)
· appearance-defaults:写死包名 ⇒ ourBundle()(deviceprobe 那条在盯这个)
· criteria-hygiene:自检样本「以 // 开头的字面量」被判成写死路径 ⇒
  按**形状**排除,**不按文件/变量名豁免**(该文件自己的注释已写过
  「豁免按名字或目录裁 = 给逃逸指路」)。变异验证:改成真路径仍红。

边界 / 未做:本函数**不区分模板串里的 ${…} 与字符类里的 /**,
失效方向是假绿(少剥注释),与 stripStrings 记的方向一致。
This commit is contained in:
2026-10-03 10:43:11 +08:00
parent 5ad75fb229
commit 93697c4061
5 changed files with 184 additions and 21 deletions

View File

@ -17,13 +17,10 @@
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { code } from './lib/read.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const pollSrc = readFileSync(join(HERE, '..', 'src', 'lib', 'inboxFallbackPoll.ts'), 'utf8');
const appSrc = readFileSync(join(HERE, '..', 'src', 'App.tsx'), 'utf8');
const pollSrc = code('src/lib/inboxFallbackPoll.ts');
const appSrc = code('src/App.tsx');
test('★ 探针只取计数(limit=1),变了才重拉', () => {
// 全量重拉会让接口与渲染无谓抖动;探针要廉价
@ -36,9 +33,25 @@ test('★ 探针只取计数(limit=1),变了才重拉', () => {
});
test('★ 首轮只建基线不触发刷新(否则刚打开页面就白拉一次)', () => {
/*
* ★ 2026-10-03 改写:原来这一格断言的是 `return false; // 首轮建立基线` ——
* **它钉的是行尾注释里的字**,所以 (a) 任何人改个措辞就假红,
* (b) 把注释删掉反而**照样通过**(`return false;` 还在,只是没了后缀)。
* `CRITERIA.md` §1「判结构与行为,不判字面与邻接」钉的就是这一族。
* 顺带发现:本判据原来用裸 `readFileSync` 读全文,注释与代码混在一起判 ——
* 换成具名的 `code()`(剥注释)之后,这一格才**暴露**出来它本来就判错了对象。
*
* 现在判的是**结构**:`lastTotal === null` 的分支里只有赋值 + `return false`,
* **不得**出现 `fetchInbox`(出现了就意味着首轮会白拉一次列表)。
*/
const branch = pollSrc.match(/if \(lastTotal === null\) \{([\s\S]*?)\n {4}\}/);
assert.ok(branch, '未找到「首轮建立基线」那个分支(`if (lastTotal === null)`)');
assert.match(branch[1], /lastTotal = total;/, '首轮分支必须先把基线记下来');
assert.match(branch[1], /return false;/, '首轮分支必须返回 false(不触发刷新)');
assert.ok(
/lastTotal === null/.test(pollSrc) && /return false; \/\/ 首轮建立基线/.test(pollSrc),
'首轮必须只记录基线,不触发刷新');
!/fetchInbox/.test(branch[1]),
'★ 首轮分支里出现 fetchInbox ⇒ 刚打开页面就白拉一次列表 —— ' +
'这正是这条判据要拦的行为(原来只判了行尾注释,删掉注释也照样绿)');
});
test('★ 探针失败不得重置基线', () => {