diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index a711c4b..2d6a4f8 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -27,7 +27,7 @@ import { dirname, join, relative } from 'node:path'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { code, prose } from './lib/read.mjs'; +import { code, prose, stripComments } from './lib/read.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const RELECTRON = join(HERE, '..'); // test/ 的上一级就是 client/electron @@ -129,3 +129,82 @@ test('★ 用到 code/prose/bytes 就必须 import(不许靠运行时才发现 assert.deepEqual(problems, [], `这些判据会以 ReferenceError 的形式红,看起来像"判据失败了",其实是"判据写错了":\n ${problems.join('\n ')}`); }); + +/** + * ★ 判据**必须读自己那棵树**,不许把仓库根硬编码成绝对路径。 + * + * pi 2026-09-15 实测出的形状(这次长在**判据自己**身上,正是我们前几轮一直在消的那个): + * `harmony-arkts.test.mjs` 里写着 `const ROOT = '/home/program/agentmail'`。 + * 把带违规的提交检出到别的目录再跑,它**读的仍是 `/home/program/agentmail`** ⇒ + * **在一个 import 顺序明显违规的检出上 3/3 全绿**。 + * + * 两层后果,第二层最糟: + * ① 它**永远无法验证任何别的 checkout / CI / 镜像** —— 换个目录不是"红", + * 是 `readdirSync` 直接抛(broken),而 broken 证明不了任何判据成立或不成立; + * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿**。 + * **"规则进来了,对象没进来"** —— 判据的逻辑对,对象错。 + * + * 判据做法:扫判据目录里**真代码**(`code()` 剥注释,否则本文件自己的说明就会误报), + * 找形如 `const X = '/绝对路径'` 的仓库根声明。修法照邻居:`join(HERE, '..', '..', '..')`。 + */ +test('★ 判据不许把仓库根硬编码成绝对路径(必须从本文件位置推)', () => { + /* + * 例外:**工具链/SDK 的绝对路径是合法的** —— 那些东西本来就不在仓库里, + * 推不出来(`TOOLCHAIN_ROOT = '/opt/huawei/command-line-tools'`)。 + * 所以按**名字**放行含 `TOOLCHAIN`/`SDK`/`HAP` 的常量,而不是按值的白名单 —— + * 值白名单会逼着下一个人为了过判据去改那个路径的写法。 + * 另一半保证:仓库**内部**的路径一律不许硬编码,那才是"读错树"的来源。 + */ + const TOOLCHAIN_OK = /TOOLCHAIN|_SDK|SDK_|HAP_|EMULATOR|HDC/i; + const problems = []; + for (const f of criteriaFiles()) { + const src = code(f); + // 只看**真的在赋值绝对路径**的那些行;注释已被剥掉,不会拿说明文字误报 + for (const m of src.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*'(\/[^']*)'/g)) { + const [, name, val] = m; + const looksLikeRepo = new RegExp(`(^|/)${relative(RELECTRON, f).split('/')[0]}|agentmail`, 'i').test(val) + || /PROJECT|REPO|WORKSPACE/i.test(name); + if (looksLikeRepo && !TOOLCHAIN_OK.test(name)) { + problems.push(`${relative(RELECTRON, f)}:\`${name} = '${val}'\` —— 这是**仓库内**的路径,` + + `必须从 \`import.meta.url\` 推(\`join(dirname(fileURLToPath(import.meta.url)), '..', …)\`),` + + `否则这个判据读的不是它自己那棵树`); + } + } + } + assert.deepEqual(problems, [], + `这些判据被钉死在一条**仓库内**的绝对路径上 —— 在别的检出/CI/镜像里,` + + `它们要么读错树报假绿,要么直接抛(broken):\n ${problems.join('\n ')}`); +}); + +/** + * ★ `stripComments` 必须**保持行号不变**。 + * + * 块注释自带换行,若整块抹成 `''`,它之后**所有行号整体前移** —— + * 而全仓判据都在用 `文件:行号` 定位(grep、编辑器跳转、`git show` 核对)。 + * 实测(我自己的 `harmony-arkts` 报违规时):报出 64/47,**真实文件是 80/63**, + * 读者第一步就得先猜"这是剥过的还是没剥的"。 + * + * 判据做法:造一个含多行块注释的样本,断言剥完**行数不变**、且行号仍然对得上; + * 再断言注释内容确实被去掉了(别为了保行号把注释留下)。 + */ +test('★ stripComments 必须保持行号(否则判据报的行号全是错的)', () => { + const sample = [ + '/*', + ' * 多行块注释', + ' * 第二行', + ' */', + 'const a = 1; // 行尾注释', + '/* 单行块注释 */', + 'const b = 2;', + ].join('\n'); + const out = stripComments(sample); + assert.equal(out.split('\n').length, sample.split('\n').length, + 'stripComments 改变了行数 —— 它之后所有行号都会错位'); + assert.ok(!out.includes('多行块注释') && !out.includes('行尾注释'), + 'stripComments 没把注释去掉'); + // 行号对得上:第 5 行仍应是 `const a = 1;` + assert.match(out.split('\n')[4], /const a = 1;/, + '剥完之后第 5 行不再是原来的第 5 行'); + assert.match(out.split('\n')[6], /const b = 2;/, + '单行块注释所在的那一行之后,行号错位了'); +}); diff --git a/client/electron/test/harmony-admin.test.mjs b/client/electron/test/harmony-admin.test.mjs index a6dd862..4dfcc30 100644 --- a/client/electron/test/harmony-admin.test.mjs +++ b/client/electron/test/harmony-admin.test.mjs @@ -18,13 +18,27 @@ * 本文件里判的标识符(`listUsers`/`#` 色值/`radiusCard`…)在解释性注释里大量出现, * 用原文读会产生假绿。这是 `lib/read.mjs` 存在的理由,也是我踩过两次的坑。 */ -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { code, prose } from './lib/read.mjs'; -const ROOT = '/home/program/agentmail'; +/* + * ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。 + * + * 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果: + * 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**, + * 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。 + * 两层后果,第二层最糟: + * ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛); + * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状, + * 这次长在判据自己身上。**"规则进来了,对象没进来"**。 + * 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。 + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); const ETS = join(ROOT, 'client/harmony/entry/src/main/ets'); const ADMIN_TS = join(ETS, 'model/AdminUsers.ts'); const ADMIN_PAGE = join(ETS, 'pages/AdminUsersPage.ets'); diff --git a/client/electron/test/harmony-arkts.test.mjs b/client/electron/test/harmony-arkts.test.mjs index 52d74ee..c22dae0 100644 --- a/client/electron/test/harmony-arkts.test.mjs +++ b/client/electron/test/harmony-arkts.test.mjs @@ -24,11 +24,25 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { code } from './lib/read.mjs'; // 与其它鸿蒙判据同口径(见 `harmony-admin.test.mjs` 的文件头) -const ROOT = '/home/program/agentmail'; +/* + * ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。 + * + * 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果: + * 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**, + * 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。 + * 两层后果,第二层最糟: + * ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛); + * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状, + * 这次长在判据自己身上。**"规则进来了,对象没进来"**。 + * 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。 + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); const ETS_ROOT = join(ROOT, 'client/harmony/entry/src/main/ets'); function allEts(dir = ETS_ROOT) { diff --git a/client/electron/test/harmony-imageprep.test.mjs b/client/electron/test/harmony-imageprep.test.mjs index d5a7bbd..173c3e9 100644 --- a/client/electron/test/harmony-imageprep.test.mjs +++ b/client/electron/test/harmony-imageprep.test.mjs @@ -11,13 +11,27 @@ * ⚠️ **本判据不能证明** "压出来的图能看"、"真机上 picker 能打开"、 * "服务端真的收下了" —— 那几条见文件末的未验清单。 */ -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { code, prose } from './lib/read.mjs'; -const ROOT = '/home/program/agentmail'; +/* + * ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。 + * + * 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果: + * 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**, + * 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。 + * 两层后果,第二层最糟: + * ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛); + * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状, + * 这次长在判据自己身上。**"规则进来了,对象没进来"**。 + * 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。 + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); const ETS = join(ROOT, 'client/harmony/entry/src/main/ets'); const PREP_TS = join(ETS, 'model/ImagePrep.ts'); const PICKER = join(ETS, 'common/BackgroundPicker.ets'); diff --git a/client/electron/test/harmony-presets.test.mjs b/client/electron/test/harmony-presets.test.mjs index 1ad7f8f..9057ef1 100644 --- a/client/electron/test/harmony-presets.test.mjs +++ b/client/electron/test/harmony-presets.test.mjs @@ -15,12 +15,26 @@ * `harmony-logic.test.mjs` 同一个先例。因此它**不需要设备**, * 也**不该**登记进 `static` 欠账(那会虚增余额)。 */ -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; -const ROOT = '/home/program/agentmail'; +/* + * ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。 + * + * 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果: + * 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**, + * 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。 + * 两层后果,第二层最糟: + * ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛); + * ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状, + * 这次长在判据自己身上。**"规则进来了,对象没进来"**。 + * 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。 + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(HERE, '..', '..', '..'); const WALLPAPER_TS = join(ROOT, 'client/harmony/entry/src/main/ets/model/Wallpaper.ts'); // 直接执行 `.ets` 树里的**纯逻辑** `.ts`(有 `@ohos` 依赖的文件不能这样跑) diff --git a/client/electron/test/lib/read.mjs b/client/electron/test/lib/read.mjs index 794ed59..4e547f3 100644 --- a/client/electron/test/lib/read.mjs +++ b/client/electron/test/lib/read.mjs @@ -18,11 +18,21 @@ */ import { readFileSync } from 'node:fs'; -/** 剥掉注释与字符串字面量里的注释样式文本之外的东西:只用于"代码里有什么" */ +/** + * 剥掉注释:只用于"代码里有什么"。 + * + * ★ **行号必须保持不变** —— 这一条是硬要求,不是风格问题。 + * 原来块注释是用 `''` 直接抹掉的,而块注释**自带换行**,抹掉它就把后面所有行的行号 + * 整体前移。后果实测(我自己的 `harmony-arkts` 判据报违规时): + * 报出"最后一个 import 在第 64 行、第 47 行已是语句",而**真实文件里是第 80 / 63 行** —— + * 全仓的判据都在用 `文件:行号` 定位(`grep -n`、编辑器跳转),**报出来的行号必须能直接用**, + * 否则读者第一步就得先猜"这是剥过的还是没剥的"。 + * 修法:块注释里的每个换行都**换成等价数量的空行**(而不是整块删掉)。 + */ export function stripComments(src) { return src - .replace(/\/\*[\s\S]*?\*\//g, '') // 块注释 - .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https:// 这类) + .replace(/\/\*[\s\S]*?\*\//g, (m) => '\n'.repeat((m.match(/\n/g) || []).length)) + .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https:// 这类;它不含换行,行号天然不变) } /** 读文件并**剥掉注释** —— 判"代码里有什么"用这个 */ diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 6a83494..9916d36 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -85,7 +85,7 @@ const SUITE = [ ['test/debt-visibility.test.mjs', [], 1], ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2], // 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口 - ['test/criteria-hygiene.test.mjs', [], 3], + ['test/criteria-hygiene.test.mjs', [], 5], // 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin / // 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用) ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 22],