/** * 判据目录自身的卫生:**读文本必须走 `test/lib/read.mjs` 的两个具名入口**。 * * # 为什么这条判据存在(pi 2026-09-14 §4) * * 规范里写着"判代码读剥离版(`code`)、判理由/文档读原文(`prose`)", * 我 P5 写过一次、当天又踩了一次:那条断言读的是**原文**,而它要找的标识符 * 恰好出现在一段解释性注释里 → 误报。**第二次犯规说明问题不在记性,在形态**: * 靠人记得执行的规范一定会有下一次。 * * 所以把"用哪个读取器"从**记忆**变成**代码里的一个词**,并且可被检查: * - `code(path)` —— 剥掉注释;判"代码里有没有这个调用/这个值"; * - `prose(path)` —— 原文;判"注释/文档里写了什么"; * - `bytes(path)` —— 二进制(安装包等)。 * * # 判据 * * 判据目录(`test/**` 里跑的判据 + `run-all.mjs`)中**不得出现裸 `readFileSync`**, * 唯一例外是 `test/lib/read.mjs` 自己。`test/manual/**` 是人工脚本、不是判据,不在范围内。 * * 附两条自检:读取器本身要真的剥注释(否则 `code` 退化成 `prose` 这条判据就废了)、 * 以及探测器要能认出裸调用(否则"都没有"与"探测器坏了"结果一样)。 */ import assert from 'node:assert/strict'; import { readdirSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { code, prose } from './lib/read.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const RELECTRON = join(HERE, '..'); // test/ 的上一级就是 client/electron const SELF = join(HERE, 'lib', 'read.mjs'); /** 判据文件清单:`test/**` 下会跑的判据 + 编排器;不含 lib/ 与 manual/ */ function criteriaFiles(dir = HERE, out = []) { for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) { if (e.name === 'lib' || e.name === 'manual' || e.name === 'node_modules') continue; criteriaFiles(p, out); } else if (/\.(test\.mjs|test\.ts|test\.tsx|mjs)$/.test(e.name) && !e.name.endsWith('.d.ts')) { out.push(p); } } return out; } /** 探测器:一段源码里有没有裸 readFileSync */ const BARE = /\breadFileSync\s*\(/; test('探测器自检 + 读取器自检', () => { // ① 探测器能认出裸调用(否则"都没有"与"探测器坏了"分不开) assert.equal(BARE.test("const s = " + "readFile" + "Sync(p, 'utf8');"), true); assert.equal(BARE.test('const s = prose(p);'), false); // ② code 真的剥注释、prose 不剥 —— 这条是整套用法的地基: // 若 code 退化成 prose,那么"读剥离版"的规范就变成一句空话,而且没人会发现。 const probe = join(RELECTRON, 'test', '_reader_probe.tmp.ts'); // 注:这个探针文本**故意拼接**而不是写字面量 —— 否则本判据自己会被自己判红 // (它扫的就是"文本里有没有这个写法",判据文件也在扫描范围内)。 writeFileSync(probe, "const REAL = 1; // " + "readFile" + "Sync( 注释里的假调用\n/* allowed-once */\n"); try { assert.ok(!code(probe).includes('allowed-once'), 'code() 必须剥掉块注释'); assert.ok(!code(probe).includes('假调用'), 'code() 必须剥掉行注释'); assert.ok(code(probe).includes('REAL'), 'code() 要保留真代码'); assert.ok(prose(probe).includes('allowed-once') && prose(probe).includes('假调用'), 'prose() 必须保留注释'); } finally { unlinkSync(probe); } }); test('★ 判据目录里不得出现裸 readFileSync(必须走 code/prose/bytes)', () => { const offenders = []; for (const f of criteriaFiles()) { if (f === SELF) continue; // 读取器的实现自己当然要用它 const src = prose(f); // 扫的是"文本里有没有这个写法",所以读原文 if (BARE.test(src)) { const line = src.split('\n').findIndex(l => BARE.test(l)) + 1; offenders.push(`${relative(RELECTRON, f)}:${line}`); } } assert.deepEqual(offenders, [], `这些判据文件里还在裸用 readFileSync:\n ${offenders.join('\n ')}\n` + " 改用 test/lib/read.mjs 的具名入口:\n" + " · code(path) —— 剥掉注释。判「代码里有没有这个调用/这个值」时用它(默认选它);\n" + " · prose(path) —— 原文。判「注释/文档里写了什么」时用它;\n" + " · bytes(path) —— 二进制(安装包等)。\n" + " 为什么不能裸用:读原文去判代码,会被解释性注释骗(同一个坑已经踩过两次)。"); });