Files
MailUI4Agents/client/electron/test/harmony-deviceprobe.test.mjs
JianFeeeee f2cddf41d2 修复: 包名从 com.agentmail.harmony 改成 com.jianf.agentmail 后,有一条行为判据**永远走"设备忙"跳过**
被发现的方式值得记:那条判据从没红过(它从不执行断言),是**"跳过也要有界"**
那条闹钟把 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。
2026-09-18 08:39:21 +08:00

145 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)。
危险形状不是"读不到",而是**读到别人的界面却当成我们的**:断言可能通过、也可能红,
**两者都不是在讲我们的界面** —— 前者是假绿。
★ 本判据喂的是**真机样本**(`fixtures/aa-dump-l-real.txt`,`hdc … shell "aa dump -l"` 实采),
不是我构造的形状。原因:我第一版按**猜的**格式写(`bundleName: com.x`),
真机上根本没有那种写法 ⇒ 闸会永远返回 `unverified`(安全但没法用);
而改成"取第一个 `bundle name`" ⇒ 真机多 mission、对方可能排在前面 ⇒ **取到别人的**(假绿)。
pi 独立复现过后者。所以样本必须是真机的。
*/
const HERE = dirname(fileURLToPath(import.meta.url));
const MODEL = join(HERE, '..', '..', 'harmony', 'entry', 'src', 'main', 'ets', 'model');
const C = await import(pathToFileURL(join(MODEL, 'DeviceProbe.ts')).href);
const OURS = 'com.jianf.agentmail';
const THEIRS = 'com.example.homeagent';
const real = prose(join(HERE, 'fixtures', 'aa-dump-l-real.txt'));
test('★ 真机样本:两个 mission 并存,前台是**对方**(这份样本就是争用发生的那一刻)', () => {
const ms = C.parseMissions(real);
assert.equal(ms.length, 2, '真机 dump 里有两个 mission(我们 + 对方)');
assert.deepEqual(ms.map((m) => m.bundle), [THEIRS, OURS], '对方排在前面 —— 所以"取第一个 bundle name"必然取错');
assert.equal(C.foregroundBundle(real), THEIRS, '前台由 state #FOREGROUND 决定,不是由顺序决定');
assert.equal(C.foregroundVerdict(real, OURS), C.VERDICT_OTHER, '对方在前台 ⇒ other,绝不算我们的');
assert.equal(C.mayAssertOn(real, OURS), false, '拿不到独占 ⇒ 不许断言');
assert.ok(C.unverifiedReason(C.VERDICT_OTHER).includes('未验'));
});
test('★ 真样本②(12:08 实采):**我们真的在前台** ⇒ ours + 允许断言(严格正例)', () => {
// 此前这条正例是**对调状态合成**的,而且合成得自相矛盾(state FG 却 app state BG)——
// pi 抓到这点:那种样本不是合法 dump,正例等于建在非法输入上。现在换成**真机实采**的。
const oursReal = prose(join(HERE, 'fixtures', 'aa-dump-l-ours-foreground.txt'));
assert.equal(C.parseMissions(oursReal).length, 2, '真样本里两个 mission 都在(对方 + 我们)');
assert.equal(C.foregroundVerdict(oursReal, OURS), C.VERDICT_OURS);
assert.equal(C.mayAssertOn(oursReal, OURS), true, '★ 没有这条,"闸能放行"这个性质就没人验(失效方式是永远未验)');
assert.equal(C.foregroundBundle(oursReal), OURS);
});
test('★ 自相矛盾的 dump(state FG 但 app state BG)⇒ unverified,不许挑一个信', () => {
const contradictory = prose(join(HERE, 'fixtures', 'aa-dump-l-ours-foreground.txt'))
.split('\n').map((l) => l.includes('app state #FOREGROUND') && l.trim().startsWith('app state') ? l.replace('#FOREGROUND', '#BACKGROUND') : l).join('\n');
assert.equal(C.foregroundVerdict(contradictory, OURS), C.VERDICT_UNVERIFIED,
'同一块里 state 与 app state 打架 ⇒ 这份 dump 自己就不自洽 ⇒ 拿不准,而不是"挑一个信"');
});
test('★ 空 dump / 截断 / 格式变了 ⇒ unverified(缺证据 ≠ 没有那个现象)', () => {
assert.equal(C.foregroundVerdict('', OURS), C.VERDICT_UNVERIFIED);
assert.equal(C.foregroundVerdict('User ID #100\n current mission lists:{\n', OURS), C.VERDICT_UNVERIFIED);
assert.equal(C.mayAssertOn('{不是 dump', OURS), false);
assert.ok(C.unverifiedReason(C.VERDICT_UNVERIFIED).includes('缺证据'), '状态词必须点明"缺证据≠没有现象"');
});
test('★ 有 mission 但全都没有 FOREGROUND 状态 ⇒ unverified(不是"没我们就算别人")', () => {
const noFg = real.split('\n').map((l) => l.replace('#FOREGROUND', '#BACKGROUND')).join('\n');
assert.equal(C.foregroundBundle(noFg), '', '没有 FOREGROUND ⇒ 取不到前台');
assert.equal(C.foregroundVerdict(noFg, OURS), C.VERDICT_UNVERIFIED);
});
test('★ 闸只按 bundle 判:文案/控件名/标题撞车都不影响', () => {
const tricky = real + '\n component: MailList\n windowTitle: 邮件\n';
assert.equal(C.foregroundVerdict(tricky, OURS), C.VERDICT_OTHER, '加上与我们要判控件同名同文案的东西,也不许变成 ours');
});
test('★ 第二份真样本:单 mission(**我们根本不在列表里**)⇒ other,不是 unverified', () => {
// 由第一份真样本删掉我们那块得到;形状与 pi 12:03 那份活 dump 一致(他信里也是单 mission)
const single = prose(join(HERE, 'fixtures', 'aa-dump-l-single-theirs.txt'));
assert.equal(C.parseMissions(single).length, 1);
assert.equal(C.foregroundVerdict(single, OURS), C.VERDICT_OTHER,
'有 FOREGROUND 且不是我们 ⇒ other。若这里报 unverified,就把"别人占着"和"没读到"混成一种了');
assert.equal(C.mayAssertOn(single, OURS), false);
});
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}`);
});