From f2cddf41d2226ddec8ba38dc95a8d9bd4a3c5fd2 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 18 Sep 2026 08:39:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E5=8C=85=E5=90=8D?= =?UTF-8?q?=E4=BB=8E=20`com.agentmail.harmony`=20=E6=94=B9=E6=88=90=20`com?= =?UTF-8?q?.jianf.agentmail`=20=E5=90=8E=EF=BC=8C=E6=9C=89=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E8=A1=8C=E4=B8=BA=E5=88=A4=E6=8D=AE**=E6=B0=B8?= =?UTF-8?q?=E8=BF=9C=E8=B5=B0"=E8=AE=BE=E5=A4=87=E5=BF=99"=E8=B7=B3?= =?UTF-8?q?=E8=BF=87**?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 被发现的方式值得记:那条判据从没红过(它从不执行断言),是**"跳过也要有界"** 那条闹钟把 42 轮连续跳过顶成红,才露出来的。这正是设界要抓的形状 —— 判据既不算红也不算绿 ⇒ 永远不必被升级。 ## 根因:包名有四处字面量,改名只改了三处 AGC 拒绝 `com.agentmail.harmony`(`harmony` 是包名保留字),于是改成 `com.jianf.agentmail`(见 `docs/ALIGN-REFS.json` 的 `agc.packageName` 与 `align-refs.test` 那条一致性判据)。但那个字面量在判据里是**各自抄的**: · `AppScope/app.json5` ← 唯一权威 · `harmony-deviceprobe.test.mjs:24` ← 改了(它判 AGC 匹配) · `harmony-nav.test.mjs:810` ← **没改** ⇒ 前台判定永不成立 · `lib/harmony-device.mjs:280` 注释 ← 没改(只是注释,但同样在传播旧名) 于是 `foregroundBundle(hdc) === 'com.agentmail.harmony'` 恒假,行为判据每次 都记一次"设备忙"。账本 `.tmp/harmony-busy-skips.json` 实测数到 **45**。 ## 修法 1. `lib/harmony-device.mjs` 新增 `ourBundle()` —— 从 `AppScope/app.json5` **唯一权威处**读包名(惰性、读不到返回 null,由调用方决定)。 2. `harmony-nav.test.mjs` 的比较改成 `fg !== ourBundle()`。 3. 清除 `.tmp/harmony-busy-skips.json` 里那 45 轮**假忙计数** —— 它们不是设备争用,是字面量漂移,留着会污染"跳过余额"的含义。 ## 判据(防止这一类漂移再发生) `harmony-deviceprobe.test.mjs` 新增 3 条(8 → 11,已同步 run-all 棘轮): · 判据目录里**不许再出现包名字面量**(除 lib/ 唯一读取处与本文件——它要拿 真机样本 `fixtures/aa-dump-l-*.txt` 里的包名做**逐字**对照,属具名例外) · 判据自检:ourBundle() 读得出值、且不带 `harmony`(AGC 保留字) · 变异自检:把一条消费方改回硬编码 ⇒ 必须判红(已验证) 且注释里的历史说明**不许误红**(`align-refs` 正靠注释解释为什么换名) 验证:harmony-deviceprobe 11/0、harmony-nav 18/0(行为那条**真跑过**、 不再是跳过)、criteria-hygiene 6/0。 --- .../test/harmony-deviceprobe.test.mjs | 59 +++++++++++++++++++ client/electron/test/harmony-nav.test.mjs | 11 +++- client/electron/test/lib/harmony-device.mjs | 30 +++++++++- client/electron/test/run-all.mjs | 4 +- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/client/electron/test/harmony-deviceprobe.test.mjs b/client/electron/test/harmony-deviceprobe.test.mjs index 955e7e9..22f6628 100644 --- a/client/electron/test/harmony-deviceprobe.test.mjs +++ b/client/electron/test/harmony-deviceprobe.test.mjs @@ -1,8 +1,10 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import { readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { prose } from './lib/read.mjs'; +import { ourBundle } from './lib/harmony-device.mjs'; /* 设备读回的**前置闸**(pi 邮件 `971c58fa` §4 / `bf583b0f` §2)。 @@ -83,3 +85,60 @@ test('★ 把 state #FOREGROUND 全部删掉 ⇒ unverified(缺证据,不猜 const noFg = real.split('\n').filter((l) => !l.includes('#FOREGROUND')).join('\n'); assert.equal(C.foregroundVerdict(noFg, OURS), C.VERDICT_UNVERIFIED); }); + +/* + * ★★ 包名只能有**一个**权威来源(2026-09-18 实测事故的回归锁)。 + * + * 事故:包名从 `com.agentmail.harmony` 改成 `com.jianf.agentmail`(AGC 拒 `harmony` 保留字), + * 而 `harmony-nav.test.mjs` 里的**比较字面量没跟着改** ⇒ + * 那条行为判据的"前台是我们的"永远不成立 ⇒ **永远走"设备忙"跳过**, + * 账本一路数到 42 轮才被设界抓红。 + * + * 值得记住的是它**怎么**暴露的:判据从没红过(它从不执行断言), + * 是"跳过也要有界"那条闹钟把它顶出来的 —— 否则它会一直"不算红也不算绿"地挂着。 + * + * 所以这条判的是**漂移本身**:判据目录里除 `lib/`(唯一读取处)与本文件(对照真机样本) + * 之外,**不许再出现包名字面量**。改包名只需改 `AppScope/app.json5`, + * 消费方一律走 `ourBundle()`。 + */ +test('★ 包名不许在判据里四处漂移:只有一个权威来源(我们刚被咬过一次)', () => { + const dir = join(HERE); + const files = readdirSync(dir).filter((f) => f.endsWith('.test.mjs')); + /* + * 本文件是**具名例外**,不是豁免:它拿真机样本(fixtures/aa-dump-l-*.txt)里的包名 + * 做对照,字面量与被测样本**必须逐字相同**,走 `ourBundle()` 反而会让样本与断言脱节。 + * 例外要**具名**(本仓纪律):写在这里,而不是把过滤条件写宽。 + */ + const SELF = 'harmony-deviceprobe.test.mjs'; + const offenders = []; + for (const f of files) { + if (f === SELF) continue; + const src = prose(join(dir, f)); + // 只认**代码里的字符串字面量**,注释里的历史说明不算(`align-refs` 就写了旧名来解释为什么换) + const noComment = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1'); + const hits = [...noComment.matchAll(/['"`](com\.[a-z0-9_.]+)['"`]/gi)].map((m) => m[1]); + if (hits.length) offenders.push(`${f}: ${[...new Set(hits)].join(', ')}`); + } + assert.deepEqual(offenders, [], + `判据里出现了包名字面量(应当走 ourBundle(),从 AppScope/app.json5 读):\n` + + ` ${offenders.join('\n ')}\n` + + ` **正确修法**:改成 \`ourBundle()\`;本文件是个例外(它要拿真机样本里的包名做对照)。\n` + + ` **最常见的错误修法**:改断言里的期望值 —— 那正是这次事故的形状(改一处忘一处)。`); +}); + +/* 变异自检:把一条消费方改回硬编码,上面那条必须判得出来。 */ +test('★ 判据自检:硬编码包名字面量必须判红', () => { + const sample = `if (fg !== 'com.agentmail.harmony') { t.skip(); }`; + const noComment = sample.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1'); + const hits = [...noComment.matchAll(/['"`](com\.[a-z0-9_.]+)['"`]/gi)].map((m) => m[1]); + assert.ok(hits.length > 0, '硬编码包名必须被抓到(否则上面那条是真空通过)'); + const commented = `// 历史说明:原来是 'com.agentmail.harmony'`; + assert.equal([...commented.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1') + .matchAll(/['"`](com\.[a-z0-9_.]+)['"`]/gi)].length, 0, '注释里的历史说明不该误红'); +}); + +test('★ 判据自检:ourBundle() 读得到值、且与 AppScope/app.json5 一致', () => { + const b = ourBundle(); + assert.ok(b && /^com\.[a-z0-9.]+$/.test(b), `ourBundle() 要读出合法包名,实际 ${b}`); + assert.ok(!/harmony/i.test(b), `包名带 harmony 是 AGC 保留字:${b}`); +}); diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index dfb3efb..be78944 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -13,7 +13,7 @@ import { readdirSync } from 'node:fs'; import { code, prose, stripComments } from './lib/read.mjs'; import { findHdc, hasTarget, foregroundBundle, dumpLayout, walk, noteBusySkip, noteRan, busyLimit, busyStreak, - noteBehavioralRan, behavioralRanAt } from './lib/harmony-device.mjs'; + noteBehavioralRan, behavioralRanAt, ourBundle } from './lib/harmony-device.mjs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; @@ -807,7 +807,14 @@ test('★ 行为(设备):底栏真渲染了可点的导航项(dumpLayout return t.skip('设备不在 —— 行为部分本次不跑(静态层仍把住源码契约)'); } const fg = foregroundBundle(hdc); - if (fg !== 'com.agentmail.harmony') { + /* + * ★ 包名从**唯一权威处**读(`ourBundle()` → AppScope/app.json5),不在这里写第二份。 + * + * 2026-09-18 实测:这里原先写死 `'com.agentmail.harmony'`,包名改成 + * `com.jianf.agentmail` 之后比较永远不成立 ⇒ 本条**永远走"设备忙"跳过"**, + * 账本一路数到 42 轮才被设界抓红。那 42 轮不是设备被占,是字面量漂移。 + */ + if (fg !== ourBundle()) { /* * ★ 设备在、但前台不是我们的 ⇒ 记一次"忙",**连续超 K 轮就变红**(pi 2026-09-18 §3)。 * diff --git a/client/electron/test/lib/harmony-device.mjs b/client/electron/test/lib/harmony-device.mjs index e183037..c7a06af 100644 --- a/client/electron/test/lib/harmony-device.mjs +++ b/client/electron/test/lib/harmony-device.mjs @@ -31,6 +31,34 @@ import { prose } from './read.mjs'; const TOOLCHAIN_HDC = '/opt/huawei/command-line-tools/sdk/default/openharmony/toolchains/hdc'; +/** + * 我们这个应用的 bundleName —— **从唯一权威处读**(`AppScope/app.json5`),不在这里写第二份。 + * + * ★ 2026-09-18 实测事故:包名从 `com.agentmail.harmony` 改成 `com.jianf.agentmail` + * (AGC 拒绝 `harmony` 作保留字,见 `align-refs.test` 与 `docs/ALIGN-REFS.json` 的 + * `agc.packageName`),而 `harmony-nav.test.mjs` 里的比较**仍是旧字面量** ⇒ + * 前台判定永远不成立 ⇒ 那条行为判据**永远走"设备忙"跳过"**,账本一路数到 **42 轮**。 + * + * 这正是 `noteBusySkip` 的设界要抓的形状:判据既不算红也不算绿 ⇒ 永远不必被升级。 + * 而它抓对了 —— 那 42 轮根本不是"设备被占",是**两处字面量漂移**。 + * + * 所以把包名收敛成一个来源:改包名只需改 `app.json5`,消费方自动跟上。 + * 另一处硬编码在 `harmony-deviceprobe.test.mjs`(那条判的是 AGC 匹配,用同一来源即可)。 + * + * 惰性求值(函数而非模块级常量):读盘失败时返回 null 由调用方决定怎么办, + * 且不把"import 期读文件"变成每个判据的隐式失败点。 + */ +export function ourBundle() { + try { + const app = prose(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', + 'harmony', 'AppScope', 'app.json5')); + const m = /"bundleName"\s*:\s*"([^"]+)"/.exec(app); + return m ? m[1] : null; + } catch { + return null; + } +} + /* * ─── 有界的"不抢前台"(pi 2026-09-18 §3)─── * @@ -277,7 +305,7 @@ export function boundsCenter(bounds) { * 在 (x, y) 上点一下(`uitest uiInput click`)。返回 true 表示"No Error"。 * * **这是写操作** —— 它改变前台应用的状态。按本模块的边界①,调用方有责任先 - * 确认前台是自己的应用(foregroundBundle === 'com.agentmail.harmony')再点, + * 确认前台是自己的应用(`foregroundBundle(hdc) === ourBundle()`)再点, * 否则会点到别人的会话正在用的界面上。 */ export function tap(hdc, x, y) { diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 14172ad..96e8076 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -83,7 +83,9 @@ const SUITE = [ ['test/build-stamp.test.mjs', [], 7], ['test/packaging.test.mjs', [], 5], ['test/align-refs.test.mjs', [], 3], - ['test/harmony-deviceprobe.test.mjs', ['--experimental-strip-types', '--no-warnings'], 8], + // 11 条里后 3 条是 2026-09-18 的**包名漂移**回归锁(判据里不许再出现包名字面量 —— + // 那次漂移让 `harmony-nav` 的行为判据永远走"设备忙"跳过,数到 42 轮才被闹钟顶出来)。 + ['test/harmony-deviceprobe.test.mjs', ['--experimental-strip-types', '--no-warnings'], 11], // 契约/形状 15 条 + ★接线 4 条 + session_id 值来源 1 条(2026-09-17)。前 15 条钉的是"点" // (逻辑对不对),后 5 条钉的是"边"(谁调用、谁消费、**传的是哪个值**)—— // "写好了但没人调用/没人读/传错了字段"编译一样通过(ArkTS 只编译可达模块,反向对照已复现过)。