diff --git a/client/electron/test/harmony-calendar.test.mjs b/client/electron/test/harmony-calendar.test.mjs index ff9a726..c3d4221 100644 --- a/client/electron/test/harmony-calendar.test.mjs +++ b/client/electron/test/harmony-calendar.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { readdirSync } from 'node:fs'; -import { code } from './lib/read.mjs'; +import { code, stripStrings } from './lib/read.mjs'; /* * `code()` 去的是**注释**,不是**字符串**(pi 2026-09-14 §2):任何只是"提到"被禁写法的 @@ -11,16 +11,25 @@ import { code } from './lib/read.mjs'; * * **口径现在就定下来**(两条路里选"只在表达式位置匹配"):先整段去掉字符串字面量,再匹配。 * 于是"提到"不再误红,而**真的用它取日期键**照样红。 - * 副作用写在旁边,免得下一个人当成漏网:这是**正则近似、不是解析器** —— - * 转义引号、跨行模板、嵌套模板都处理不干净,它是采样而非完备性证明。 * **出现同族误红时不许"再改一次扫描口径"把判据往宽里挪。** + * + * ★★ 这里原来有**自己的一份** `stripStrings`(正则近似),现在改用 `lib/read.mjs` 那份 + * (pi 2026-09-15 抓到"同一件东西两份实现")。三条理由,按重要性: + * + * ① **这份判据打印 `文件:行号`**(`hits.push(\`…:${i+1} …\`)`),行号是它**唯一**的定位手段。 + * 旧那份不保证行数守恒 —— 实测 89 个 `.ets`/`.ts` 里 **3 个**抹除后行数变少 + * (`plugins/dsh-mail-bridge/src/index.ts` 原 2248 → 2245),于是跨行模板之后的 + * **报错行号整体前移**。**这正是我两轮前为 `stripComments` 修过的同一条** + * ("行号必须保持不变")—— 那次修在主实现里,**兄弟副本里没修**。 + * ② `read.mjs` 那份是**逐字符**扫的(不是正则近似),**保留引号与换行** ⇒ 行数守恒 + * (实测 89/89 不变,旧那份 3 个不对)。 + * ③ 两份里**只有一份**经过那五轮边界修(注释/字符串/等价写法/锚/正则引号)—— + * 留两份,第六轮只会被修在其中一份里。**不给自己留兄弟副本。** + * + * ★ 换之前我实测过兼容性:两份实现在**这份判据真正用的那个性质**上结论一致 + * ——89 个文件里"抹除后还有没有 `toISOString`"**全部相同**(0 处分歧)。 + * ⇒ 换实现**不改判据结论**,只是让行号可信。 */ -function stripStrings(src) { - return src - .replace(/`(?:[^`\\]|\\.)*`/g, '``') - .replace(/'(?:[^'\\\n]|\\.)*'/g, "''") - .replace(/"(?:[^"\\\n]|\\.)*"/g, '""'); -} /* ★ 日历的纯逻辑(P6 第 1 步的逻辑那一半)—— **不需要设备**,所以它不该进静态欠账。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 24747a6..a041eac 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -236,6 +236,21 @@ const PHASE = process.env.AGENTMAIL_CRITERIA_PHASE === 'install' ? 'install' : ' function sideOf(file) { return SIDES[file] || SIDE.ARTIFACT; } const brokens = []; +/* + * ★★ 本相位**实际会跑**的文件 —— 期望值必须由**同一个判据**算出来。 + * + * pi 2026-09-15 抓到的假红:我上一版把"记录数"跟 `SUITE.length` 比, + * 而 install 相位有 2 条构建相位判据**进不了循环**(不 push 记录)⇒ `records=23 ≠ 25` ⇒ 报红, + * 报文还给出**恰好错误**的成因("漏了或多跑了")—— 什么都没漏、也没多跑。 + * 而这条命令正是 `install.sh --check` 的前端门禁(`AGENTMAIL_CRITERIA_PHASE=install npm test`) + * ⇒ 一个**假红**会打出一行 `[FAIL]`,教读者"那行 `[FAIL]` 可以忽略"。 + * + * 所以:期望值 = 在**本相位**下不被跳过的文件数,而不是套件总数。 + * 并且它和下面的 skip 判定用**同一个谓词**(同一个 `sideOf`),免得两处各写一份条件而漂移 —— + * "同一个事实有两份实现"正是这一路反复出现的形状。 + */ +const phaseWillRun = SUITE.map(([f]) => f) + .filter(f => !(PHASE === 'install' && sideOf(f) === SIDE.SOURCE)); const skippedByPhase = []; for (const [file, flags, expected] of SUITE) { if (PHASE === 'install' && sideOf(file) === SIDE.SOURCE) { @@ -741,18 +756,21 @@ try { * 于是它**能被行为判据抓住**,而不是只能靠猜文本。 * **这是"文本判据 → 行为判据",不是"可能错 → 不可能错"。** */ - if (records.length !== SUITE.length) { + if (records.length !== phaseWillRun.length) { const seen = new Map(); for (const r of records) seen.set(r.file, (seen.get(r.file) || 0) + 1); const dup = [...seen].filter(([, n]) => n > 1); - probs.push(`跑出的记录数 ${records.length} ≠ 套件文件数 ${SUITE.length} —— ` + probs.push(`跑出的记录数 ${records.length} ≠ **本相位实际会跑**的文件数 ${phaseWillRun.length}` + + `(套件共 ${SUITE.length} 条,本相位跳过 ${skippedByPhase.length} 条构建相位判据)—— ` + `每个文件**恰好一条**记录(这是"每个文件只该计一次"的**行为**版本;` + `原来那条是扫源码文本的,已删,理由见上)。\n` + (dup.length > 0 - ? ` 重复的文件:${dup.map(([f, n]) => `${f}×${n}`).join('、')}\n` + ? ` 重复的文件:${dup.map(([f, n]) => `${f}×${n}`).join('、')}` + + `⇒ **push 了两次**(不是漏跑)。\n` : ` 没有重复文件 ⇒ 是**漏了**或**多跑了**,不是 push 两次:检查 SUITE 与循环。\n`) - + ` 修法:records.push 每个文件只该执行一次(它在循环体里、红绿分支**之外**)。` - + `注意"搬动累加块"这个动作历史上做过三次,都把旧路径留在了原地。`); + + ` ⚠️ 期望值用 \`phaseWillRun\`(本相位不被跳过的那些)而**不是** \`SUITE.length\`:` + + `install 相位会跳过构建相位判据,拿套件总数比就是**假红**` + + `(这条命令正是 \`install.sh --check\` 跑的那条)。`); } if (ranFilesCount > SUITE.length) { probs.push(`ran=${ranFilesCount} > files=${SUITE.length} —— 自报条数的文件比文件总数还多,` @@ -781,12 +799,16 @@ try { * * ★ 本条的判据形状**在 pi 2026-09-15 的建议后换过**:原来是"扫自己的源码, * 数四个计数器各有几处写"(文本判据,长出了五轮毛病); - * 现在是"**记录数 == 文件数**"(行为判据,见上面 `reportedRecords`)。 + * 现在是"**记录数 == 本相位实际会跑的文件数**"(行为判据,见上面 `reportedRecords`)。 * 报文如实说它现在由谁判 —— 改了判据却留着旧文案,就是又一次"说法与实现不一致"。 + * (**这句话我这轮又踩了一次**:改成 `phaseWillRun` 之后报文还写着"== 文件数", + * 而 install 相位 `files=25` 里只有 23 条会被跑 ⇒ 报文本身就成了假话。) */ console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},` + `fail 是 checks 的子集;四个计数器由 ${records.length} 条记录**投影**而来 —— ` + - `判的是"记录数 == 文件数"这个**行为**,不是"源码里 \`+=\` 有几处")`); + `判的是"记录数 == **本相位实际会跑的** ${phaseWillRun.length} 条"` + + `${skippedByPhase.length ? `(套件 ${SUITE.length} 条里本相位跳过 ${skippedByPhase.length} 条)` : ''}` + + `这个**行为**,不是"源码里 \`+=\` 有几处")`); } } catch (e) { console.error(`✗ 套件自检 5 跑不了:${e.message || e}`);