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

@ -1,19 +1,18 @@
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 { readFileSync as read } from 'node:fs';
import { code } from './lib/read.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = join(HERE, '..', 'src');
const sse = readFileSync(join(SRC, 'api', 'sse.ts'), 'utf8');
const app = readFileSync(join(SRC, 'App.tsx'), 'utf8');
// ★ 服务端在**仓库根**(`server/`),而 `code()` 的相对基准是**包目录**
// (`test/lib/read.mjs` 的 `PKG_ROOT`)⇒ 跨到包外必须给绝对路径。
// 绝对路径由 `code()` 原样透传(见该文件 `abspath` 的注释),所以这样是安全的。
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const sse = code('src/api/sse.ts');
const app = code('src/App.tsx');
// 服务端在另一个仓(../../../../server),读它是为了钉「两侧同步」这条 ——
// 只改客户端不改服务端时,重放永远不生效,而**没有任何东西会红**。
const sseMgr = read(
join(SRC, '..', '..', '..', 'server', 'internal', 'sse', 'manager.go'),
'utf8');
const sseMgr = code(join(REPO_ROOT, 'server/internal/sse/manager.go'));
/**
* WebUI 实时性:SSE 订阅必须**跟着账号凭证**走 + 断线重连要能补回漏掉的事件。