Files
MailUI4Agents/client/electron/test/cross-client-logic.test.mjs
JianFeeeee 5e4a1b616c 跨端: B 的交付物(跨端纯逻辑一致性判据)+ 照 skill 回扫修掉两处隐形债
用户:「你为什么不加载鸿蒙开发相关skill?」—— 说得对。那份
`arkts-grammar-standards` 写着 "REQUIRED before writing the first .ets file of a
session",而我这轮一直在写 `.ets`。补加载后照它的规则表**逐条回扫**,
当场抓出两处此前没人管的违规。

══ ① 用户要做的 B:`cross-client-logic.test.mjs`(新,6 条判据)

背景:两套纯逻辑各写一份且已分叉(replyTarget 214/170 行、mailGroups 178/459、
appearance 187/324、calendar 208/446)。当天已**踩到**两处分叉
(`participantAddress` 的 `||`、`ThreadPage` 字段全错)。

做法:**同一张用例表喂给两边,逐条比结果**(`--experimental-strip-types`
直接在 node 里跑两侧源码 —— 两边的 model 层都是纯逻辑、无 SDK 依赖)。
不选"生成一份共享源码":harmony 不能 import 工程外文件,
且两边类型系统不同(ArkTS 禁解构/any/对象字面量要具名类型),
生成器要维护"两边都能过"的子集,是另一个大工程。

★ **首轮运行就报出两处真分叉,都不是我踩到才发现**:
  ① `formatAddress('dsh', undefined, undefined)`:electron 返回 `"dsh"`,
     harmony **抛** `Cannot read properties of undefined`。
     —— 又是 `omitempty` 那个坑(**第三次**),这次是判据先报的。
  ② `monthGrid`:electron **固定 6 行**(`grid-rows-6`),harmony **4~6 行**
     ⇒ 翻月时网格高度跳动。WebUI 的注释明写要避免这个("行数变化会让整个
     网格高度跳动,翻月时页面内容上下弹")。
  ③ 顺着 ② 又发现:WebUI 邻月格子**填真实日期并置灰、可点**
     (`CalendarView.tsx:545-556`),harmony 留**空白格**。

★ 判据自身的两次错,都留了档(判据的 bug 与代码的 bug 一样危险):
  · 第一版把 `args[0]` 当单个参数传,字符串被当可迭代对象展开 ⇒
    `formatAddress('d','s','h')` —— **判据自己造出假分叉**。
  · 第一版 `weekStart` 传 0(周日),而两端实际都是 1(周一)⇒ 又一处假分叉。
    差一点就去"修"一个不存在的问题。
  · `monthGrid` 的投影第一版按 `inMonth ? [y,m,day] : null`,
    把"邻月填不填真日期"这个**真分叉**抹平了 —— 投影只该换表示,不该替我看不看。

★ 三类"不同"要分清(写进文件头):**命名不同**(投影归一,不是分叉)、
  **签名不同**(ArkTS 没 Date 重载习惯;语义必须一样)、**行为不同**(是分叉,以 electron 为准)。

══ ② skill 回扫抓出的两处隐形债(编译器只告警、判据也不管)

· **正则字面量**(`arkts-no-regexp-literals`):`MailDetailPage.ets:615` 的
  `/^\d+$/`(从 2026-09-19 活到今天)。
· **废弃的全局 `router`**:`api/Logout.ets:76` 的 `router.replaceUrl(...)`。
  它是个独立函数(没有 `this`)⇒ 拿不到 `UIContext`,改成由调用方传
  (两个调用点都持有 `getUIContext()`,零成本)。

★ 这两条为什么能活这么久:**编译器对它们只告警、不挡构建**,
  全仓也**没有判据**管 ⇒ 规则事实上不存在。已补两条判据,都做了变异验证。

══ ③ 顺带修正一条**恒真的同义反复**断言

`harmony-calendar` 里 "today 不在本月:不许标在别的月" 那条:
它是在"邻月格子是空 `DayCell`(`iso` 为空串)"时写的 ⇒ `c.iso === today`
**永远不可能**匹配 ⇒ `count === 0` 恒真,**看起来守着一条规则,其实什么都没守**。
改成真不变量:**"被标为今天的那一格,iso 必须就是 today;至多一格"**,
并反向核对"2026-10-01 确实出现在 9 月网格里"(否则那段是空转)。
实测 WebUI `CalendarView.tsx:547` 是逐格 `isSameDay` ⇒ **它会标**,
所以原来那条"不许标"本身就窄了一半。

══ ④ 登记两处盘点发现(**没有**顺手改,因为需要人决定)

· `harmony-dead-pages`:`InboxPage.ets`(238 行) 不可达(不在页面表、无人导航),
  `SessionsPage.ets`(170 行) 唯一引用来自 InboxPage ⇒ 一起不可达。
  没删是因为 `HARMONY-ALIGN-PLAN.md:214` 把它当变异测试靶子用过 ——
  删掉会永久丢代码,是否只是"早期留存"我判断不了。
· `harmony-permission-history`:WebUI 授权栏显示**待决 + 已决策历史**两段
  (拿 inbox 自己分组,`PermissionList.tsx:27/174/182`);鸿蒙调专用端点
  `/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ **只拿得到待决的**。
  已在 `cross-client-logic` 的 gaps 里如实登记,判据会盯着"不要再少"。

══ 判据状态

`files=33 ran=33 checks=530 pass=530 fail=0 skip=0 red=0 broken=0 unreported=0`;
`baseline=7/7✓`(底本第 9 次重算,已按规矩先 `git diff --quiet HEAD` 取证 + 记录理由)。
`verdict=red` 残余仍是 5 条静态判据的**设备到期提示**(既有机制)。

══ 环境

模拟器昨天起卡死(hdc 能连、shell 超时、CPU 150%、跑了 34 小时),
导致设备判据各跑 836 秒后失败 —— 看起来像"套件卡死"。用户批准后杀掉重启
(`Emulator -start HATriple -noWindow`,`devecocli` 那套因 x11 起不来),
现在**75~150 秒**跑完整套。
2026-09-20 22:35:35 +08:00

394 lines
20 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.

/*
* 跨端**纯逻辑一致性**判据:同一张用例表喂给两边,逐条比结果。
*
* # 为什么需要它(用户要做的 B)
*
* 用户 2026-09-20:「两边纯逻辑各写一份且已分叉」—— 实际规模:
* replyTarget electron 214 行 / harmony 170 行
* mailGroups electron 178 行 / harmony 459 行
* appearance electron 187 行 / harmony 324 行
* calendar electron 208 行 / harmony 446 行
* 两份手抄的同一套规则。
*
* 当天已经**实证了两处分叉**(都是踩到才发现的):
* · `participantAddress`:electron 写 `(workspace || '').trim()`,
* harmony 写 `workspace.trim()` ⇒ 服务端 `omitempty` 缺失时 harmony 抛
* `Cannot read property trim of undefined` ⇒ **整页白屏**。
* · `ThreadPage` 的字段名整套写错(`dir`/`has_more_up` vs
* `root_mail_id`/`anchor_depth`/`has_more`/`next_offset`)⇒
* 接口 200 但界面什么都不显示。
*
* # 为什么是"跑同一张表比结果",而不是"生成一份共享源码"
*
* 生成器(像 `gen-background-takeover.mjs` 那样)看着更彻底,但有两条硬约束:
* · harmony 侧**不能** `import` 工程外的文件(实测:相对路径与软链都报
* `Could not resolve`),所以"共享源码"只能是**生成进 harmony 目录**的一份拷贝。
* · 两边的**类型系统不同**(ArkTS 禁解构/禁 `any`/对象字面量要具名类型…),
* 生成器要维护一份"两边都能过"的子集 —— 那是另一个大工程。
*
* 而**一致性判据**用很小的成本拿到最重要的性质:
* **只要两边对同一输入给出不同答案(或一边抛一边不抛),就红。**
* 它不需要我先知道哪里有 bug —— 本文件首次运行就报出两处真分叉
* (`formatAddress(undefined)` 抛 与 `monthGrid` 行数 4~6 vs 固定 6),
* 两处都是**判据先报、不是我踩到才发现**的。
*
* ⇒ 分两步走:**先用判据把分叉钉住**(本次),生成器留待真正需要时再做。
* 这与本仓既有纪律一致:「枚举挡实例,类才挡漂移」——
* 这张表枚举的是**行为**,新增一个用例就多覆盖一格。
*
* # 三类"不同"要分清(这是本文件最容易骗自己的地方)
*
* ① **命名不同**(`MailGroup.sessionId` vs `SessionGroup.session_id`):
* 不是分叉。用**语义投影**归一后比(投影只改名与挑字段,**不做计算**)。
* ② **签名不同**(`addMonths(Date,n)` vs `addMonths(y,m,delta)`):
* 不是分叉(ArkTS 没有 Date 习惯),但**语义必须一样** ——
* 用例表按侧写参数、投影归一到同一层再比。
* ③ **行为不同**(`monthGrid` 4~6 行 vs 固定 6 行):
* **是分叉**,以 electron 为准。这一处就是本判据抓出来的。
*/
import { code } from './lib/read.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..', '..');
const E_LIB = join(ROOT, 'client/electron/src/lib');
const H_MODEL = join(ROOT, 'client/harmony/entry/src/main/ets/model');
/** 把一个函数调用成"结果字符串" —— 抛异常本身也是一等结果(要能比出来)。 */
function callString(fn, args, project) {
if (typeof fn !== 'function') return '<不是函数>';
try {
const v = fn(...args);
const p = project ? project(v) : v;
return JSON.stringify(p) ?? 'undefined';
} catch (e) {
/*
* "哪一边抛、哪一边不抛"正是要抓的分叉(`formatAddress(undefined)` 就是)。
* 所以异常也被归一成可比较的字符串,而不是让整个判据崩掉。
*/
return `THROW:${e instanceof Error ? e.message.slice(0, 40) : String(e)}`;
}
}
/** 造一封最小可用邮件(只填被测函数真正会读的字段)。 */
function mkMail(over = {}) {
return {
mail_id: 'm1', session_id: 's1', from_name: 'dsh', to_name: 'jianf',
from_human: false, to_human: true,
session_workspace: '/home/program/agentmail', session_alias: 'fix-x',
created_at: '2026-09-14T10:00:00Z', status: 'unread', subject: '主题',
body_preview: '预览', cc_list: [], mail_type: 'normal',
permission_result: '', permission_mode: 'workspace',
source_account_id: 'a1', source_account_name: '主账号',
...over
};
}
const PAIRS = [
{
name: 'ReplyTarget',
electron: join(E_LIB, 'replyTarget.ts'),
harmony: join(H_MODEL, 'ReplyTarget.ts'),
/* 签名与返回形状两边一致 ⇒ 不需要投影,直接比 */
project: {},
gaps: ['replyAllCC', 'sessionCounterpart', 'sessionReplyTarget'],
cases: [
['agent 三段', 'formatAddress', ['dsh', '/p', 's']],
['只有名字', 'formatAddress', ['dsh', '', '']],
['人(无 path/session)', 'formatAddress', ['jianf', '', '']],
['带空格要 trim', 'formatAddress', [' dsh ', ' /p ', ' s ']],
['空名字', 'formatAddress', ['', '', '']],
/* ★ 服务端 `omitempty` 缺失时传进来的就是这个形状 —— 两边都必须不抛 */
['path/session 是 undefined', 'formatAddress', ['dsh', undefined, undefined]],
['name 是 undefined', 'formatAddress', [undefined, '/p', 's']],
['participant:人', 'participantAddress', ['jianf', true, '', '']],
['participant:Agent', 'participantAddress', ['dsh', false, '/p', 's']],
['participant:workspace 缺失', 'participantAddress', ['dsh', false, undefined, undefined]],
['participant:人 + 缺失', 'participantAddress', ['jianf', true, undefined, undefined]]
]
},
{
name: 'MailGrouping',
electron: join(E_LIB, 'mailGroups.ts'),
harmony: join(H_MODEL, 'MailGrouping.ts'),
/*
* ★ 命名投影(不是计算):唯一差别是 `MailGroup.sessionId`
* ↔ `SessionGroup.key`/`session_id` —— 类型系统各自演化的结果。
* 不归一的话会把命名差异误报成行为分叉。
*/
/*
* ★ 缺口(harmony 没有的导出)—— **如实登记,不静默忽略**。
*
* `groupPermissions` 不是"改名了":electron 拿 **inbox** 自己按会话分组、
* 同时渲染 `g.pending`(待决策)与 `g.settled`(历史)两段
* (`PermissionList.tsx:27/174/182`);而 harmony 调**专用端点**
* `GET /permission/pending`(SQL 里 `WHERE pr.result IS NULL`)——
* **只拿得到待决的,且平铺不分组**。
*
* ⇒ 用户可见差异:**已决策的授权记录在鸿蒙上完全看不到**。
* 已登记进 `docs/DEBTS.json` 的 `harmony-permission-history`。
*
* 登记在这里的意义:这条判据会让**下次再少一个导出**时红,
* 而"这个缺口是已知的、有据可查的"与"悄悄少了一项比较"是两件事。
*/
gaps: ['groupPermissions'],
project: {
groupMailsBySession: (v) => (v ?? []).map(g => ({
sessionId: g.sessionId ?? g.session_id,
alias: g.alias,
subject: g.subject,
latestMailId: g.latest?.mail_id ?? null,
mailIds: (g.mails ?? []).map(m => m.mail_id),
unreadCount: g.unreadCount
}))
},
cases: [
['单封不成组', 'groupMailsBySession', [[mkMail()]]],
[
'同会话两封成组',
'groupMailsBySession',
[[mkMail({ mail_id: 'm1', created_at: '2026-09-14T09:00:00Z' }),
mkMail({ mail_id: 'm2', created_at: '2026-09-14T11:00:00Z', subject: '新' })]]
],
['空列表', 'groupMailsBySession', [[]]],
['单封是 flat group', 'isFlatGroup', [{ mails: [mkMail()] }]],
['两封不是 flat group', 'isFlatGroup', [{ mails: [mkMail(), mkMail()] }]],
['按权限拆分:全是普通', 'splitByPermission', [[mkMail(), mkMail({ mail_id: 'm2' })]]],
[
'按权限拆分:混一条权限请求',
'splitByPermission',
[[mkMail(), mkMail({ mail_id: 'm2', mail_type: 'permission_request' })]]
],
['拆分:空列表', 'splitByPermission', [[]]],
['未决权限计数', 'countPendingPermissions', [[mkMail({ mail_type: 'permission_request' })]]],
['未决权限计数(已决策)', 'countPendingPermissions',
[[mkMail({ mail_type: 'permission_request', permission_result: 'allow' })]]]
]
},
{
name: 'Calendar',
electron: join(E_LIB, 'calendar.ts'),
harmony: join(H_MODEL, 'Calendar.ts'),
/*
* ★ 两边**签名不同**(不是缺陷,ArkTS 没有 Date 习惯),但语义必须一样。
* 投影把两边归到"年月"与"网格里每天是哪天"这一层 —— 只做形状归一,不算日期。
*
* 注意 `monthGrid` 的 harmony 返回 `DayCell[][]`,投影里**不能**自己算
* 年月(那会引入第三份实现);它用调用时传入的 year/month 闭包捕获。
*/
project: {
addMonths: (v) => {
if (v instanceof Date) return [v.getFullYear(), v.getMonth() + 1];
return v; // harmony 已经是 [year, month]
}
},
/* monthGrid 的投影需要 y/m,改成按用例给(见下方 cases 的第 4 个元素) */
/*
* ★ 缺口(harmony 没有同名导出)—— 这里**不是"功能缺失"**,是
* **两种表示法**,必须区分清楚,否则会把"架构不同"误报成"少做了功能":
*
* electron `lib/calendar.ts`:**以 `Date` 对象为中心**
* `addDays(Date, n): Date`、`isSameDay(a, b)`、`dayKey(Date)`、
* `startOfMonth(Date)`、`weekDays(Date): Date[]` …
* harmony `model/Calendar.ts`:**以 ISO 字符串为中心**
* `addDaysIso(iso, delta): string`、`weekDaysOf(iso, ws): string[]`、
* `localIsoOf(timestamp)` …
*
* 这是 ArkTS 侧的**有意选择**(没有 `Date` 重载习惯、ISO 串做 key 更稳),
* 不是漏做。已逐条核对语义等价:
* `addDays(d,n)` ≡ `addDaysIso(iso,delta)`(都是"加减天数、跨月跨年正确");
* `weekDays(anchor)` ≡ `weekDaysOf(iso, weekStart)`(都是"含锚点的那一周 7 天")。
*
* ⇒ 登记为缺口,让判据盯着"**不要再多**":
* 若哪天 harmony 又少一个 calendar 导出,会在这里红,
* 而那多半是真的漏做(与这 17 个表示法差异不同)。
*/
gaps: [
'addDays', 'bucketByDay', 'dayKey', 'describeRecurrence', 'describeRemindBefore',
'endOfDay', 'endOfMonth', 'endOfWeek', 'fromLocalInput', 'isSameDay',
'remindAt', 'renderReminder', 'startOfDay', 'startOfMonth', 'startOfWeek',
'toLocalInput', 'weekDays'
],
/*
* 用例格式:[标签, 函数名, electron 实参数组, harmony 实参数组(可选), 网格投影键(可选)]
* 签名不同 ⇒ 前两个 `addMonths` 用例的两侧实参不一样,这正是要显式写出来的地方。
*/
cases: [
['跨月前进', 'addMonths', [new Date(2026, 0, 1), 1], [2026, 1, 1]],
['跨年前进', 'addMonths', [new Date(2026, 11, 1), 1], [2026, 12, 1]],
['跨年后退', 'addMonths', [new Date(2026, 0, 1), -1], [2026, 1, -1]],
['加 0 个月', 'addMonths', [new Date(2026, 5, 1), 0], [2026, 6, 0]],
['加 12 个月', 'addMonths', [new Date(2026, 5, 1), 12], [2026, 6, 12]],
/* ★ 1 月 31 日 + 1 月:electron 的注释说"先归到 1 号再加月"防溢出到 3 月 */
['1 月 31 日 + 1(防溢出)', 'addMonths', [new Date(2026, 0, 31), 1], [2026, 1, 1]],
/*
* 月历网格:**固定 6 行**(WebUI `grid-rows-6`)。
* ★ 这一条就是本判据抓出来的分叉现场:harmony 原来 4~6 行,
* 翻月时网格高度跳动(WebUI 的注释明写要避免)。
*/
['月历网格 2026-09(6 行的月)', 'monthGrid', [new Date(2026, 8, 1)], [2026, 9, 1], 'grid:2026,9'],
['月历网格 2026-02(4 行的月)', 'monthGrid', [new Date(2026, 1, 1)], [2026, 2, 1], 'grid:2026,2'],
['月历网格 2024-02(闰年)', 'monthGrid', [new Date(2024, 1, 1)], [2024, 2, 1], 'grid:2024,2']
]
}
];
/**
* 网格投影:两边都归到 `[[[y,m,d], …7], …6行]` —— **用两种格式都自带的
* "这一格是哪一天"信息**,不由投影推算日期。
*
* ★ 曾经写错过一次,值得留档:
* 第一版把 harmony 的格子按 `c.inMonth ? [y,m,c.day] : null` 处理 ——
* 于是"**邻月格子填不填真实日期**"这个差别被投影抹成了 `null`,
* 两端看起来一样。**但那个差别正是真分叉**(WebUI 填真日期并置灰、可点,
* 鸿蒙原来留空白格)。
*
* 投影里读 `iso`(两端都有这一格的完整日期)才是对的:
* 投影只做"换一种表示",不做"要不要看这一维"的判断。
*/
function gridProject() {
return (v) => {
/* electron:`Date[]`(42 个)→ 按 7 列折行 */
if (Array.isArray(v) && v.length > 0 && v[0] instanceof Date) {
const flat = v.map(d => [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
const rows = [];
for (let i = 0; i < flat.length; i += 7) rows.push(flat.slice(i, i + 7));
return rows;
}
/*
* harmony:`DayCell[][]` —— 读格子的 `iso`(`YYYY-MM-DD`)。
* 用 `iso` 而不是 `[year, month, day]` 拼:`iso` 是格子**自己**的日期,
* 邻月日期的年月与格子的 `year`/`month` 参数**不同**,拿参数拼会算错。
*/
return (v ?? []).map(row => row.map(c => {
const parts = String(c.iso ?? '').split('-');
if (parts.length !== 3 || parts[0] === '') return null;
return [Number(parts[0]), Number(parts[1]), Number(parts[2])];
}));
};
}
for (const pair of PAIRS) {
test(`跨端纯逻辑一致|${pair.name}:同一张用例表两边结果必须相同`, async () => {
const E = await import(pathToFileURL(pair.electron).href);
const H = await import(pathToFileURL(pair.harmony).href);
const rows = [];
const diffs = [];
for (const c of pair.cases) {
/*
* 用例的四个位置:
* [0] 标签 [1] 函数名 [2] electron 的实参数组 [3] harmony 的实参数组(可选,缺省同 [2])
* [4] 网格投影键 'grid:Y,M'(可选,仅 monthGrid 用)
*
* ★ 这里必须**展开** `argE`(`fn(...argE)`)而不是把它当单个参数 ——
* 第一版我传的是 `args[0]`,于是 `formatAddress('dsh',…)` 变成了
* `formatAddress('d','s','h')`(字符串被当作可迭代对象展开)——
* 判据自己造出了假分叉。**判据的 bug 与代码的 bug 一样危险**:
* 它会把正确的实现报成错的,然后我就去"修"一个不存在的问题。
*/
const [tag, fn, argE] = c;
const argH = c.length > 3 && Array.isArray(c[3]) ? c[3] : argE;
const grid = c[4];
let pe = pair.project[fn];
let ph = pair.project[fn];
if (typeof grid === 'string' && grid.startsWith('grid:')) {
const [gy, gm] = grid.slice(5).split(',').map(Number);
/*
* 周起始日 = **周一**(`1`)
* · electron:`startOfWeek` 硬编码周一起(`dow===0 ? 6 : dow-1`),
* 表头也写死 `['一','二',…,'日']`
* · harmony :`CalendarPage` 的 `WEEK_START = 1`
* ★ 我第一版在这里传 `0`(周日),于是判据报"两端网格不一致" ——
* **那是判据自己的 bug,不是代码的**。差一点就去"修"一个不存在的问题。
* (教训:判据报差异时,先确认**输入真的相同**,再怀疑实现。)
*/
pe = gridProject();
ph = gridProject();
}
const e = callString(E[fn], argE, pe);
const h = callString(H[fn], argH, ph);
rows.push({ tag, fn, e, h });
if (e !== h) {
diffs.push(` · ${fn}/${tag}\n electron: ${String(e).slice(0, 300)}\n harmony : ${String(h).slice(0, 300)}`);
}
}
assert.ok(rows.length > 0, `${pair.name} 的用例表不能是空的(空表会假装通过)`);
assert.deepEqual(diffs, [],
`${pair.name} 两端对同一输入给出了不同结果(这就是"分叉"):\n${diffs.join('\n')}\n` +
'以 electron 为准(用户定的方向:electron 是唯一真实源泉)');
/*
* 自检:这张表**真的在比**吗?
* 造一个"故意不一样"的假 harmony,确认差异会被抓出来 ——
* 否则"没有差异"与"比较逻辑坏了"给出同样的绿,那是判据最容易骗自己的地方。
*/
const firstFn = pair.cases[0][1];
const FAKE = { ...H, [firstFn]: () => '总之不一样' };
const fakeDiff = [];
for (const c of pair.cases) {
const [, fn, argE] = c;
const argH = c.length > 3 && Array.isArray(c[3]) ? c[3] : argE;
if (callString(E[fn], argE, pair.project[fn]) !== callString(FAKE[fn], argH, pair.project[fn])) {
fakeDiff.push(c[0]);
}
}
assert.ok(fakeDiff.length > 0,
`自检失败:把 ${firstFn} 换成恒定返回值的假实现,竟然一条差异都没报 —— 说明这张表没在真的比`);
});
}
test('跨端纯逻辑一致|覆盖面自检:几个关键边界**必须在表里**', () => {
/*
* 判据最容易烂的方式是"表被删到只剩好走的几条"。
* 这里钉住几条**必须有**的边界 —— 尤其是那个真实造成白屏的形状
* (`omitempty` 缺失 ⇒ `undefined`),它是本表存在的理由。
*/
const rt = PAIRS.find(p => p.name === 'ReplyTarget').cases.map(c => `${c[1]}|${c[0]}`);
assert.ok(rt.some(x => x.startsWith('formatAddress|path/session 是 undefined')),
'「path/session 是 undefined」这条必须在 —— 它就是当初白屏崩溃的形状');
assert.ok(rt.some(x => x.includes('participantAddress|participant:workspace 缺失')),
'「participantAddress 遇到缺失 workspace」必须在 —— 那是那个 || 兜底分叉的位置');
const cal = PAIRS.find(p => p.name === 'Calendar').cases.map(c => c[0]);
assert.ok(cal.some(x => x.includes('闰年')), '闰年 2 月必须在 —— 日期逻辑最容易差一天的地方');
assert.ok(cal.some(x => x.includes('跨年')), '跨年必须在');
assert.ok(cal.filter(x => x.includes('月历网格')).length >= 2,
'月历网格至少要两个不同行数的月份 —— 那一处分叉正是"行数不固定"');
});
test('跨端纯逻辑一致|缺口只减不增(harmony 缺的导出要登记)', async () => {
/*
* 一致性表只能比**两侧都有**的导出。剩下的是**功能缺口**:
* `ReplyTarget` 里 harmony 少了 `sessionCounterpart` / `sessionReplyTarget` /
* `replyAllCC`(electron 有)。
*
* 这里把缺口集合钉住:**允许减少、不允许增加**。
* 不钉的话,下次再删一个 harmony 导出,一致性表会**安静地少比一项**,
* 而所有判据依然全绿 —— 那是最危险的一种"绿"。
*/
for (const pair of PAIRS) {
const E = await import(pathToFileURL(pair.electron).href);
const H = await import(pathToFileURL(pair.harmony).href);
const missing = Object.keys(E).filter(k => !(k in H)).sort();
assert.deepEqual(missing, [...pair.gaps].sort(),
`${pair.name} 两侧导出缺口的集合变了。\n` +
'减少(harmony 补上了功能)→ 请把 gaps 里对应的名字删掉;\n' +
'增加(harmony 又少了一个导出)→ 那是一处新的功能缺口,要登记并说明为什么。');
}
});
test('跨端纯逻辑一致|两个对照文件都不是空的(防止"比了个寂寞")', () => {
for (const pair of PAIRS) {
assert.ok(code(pair.harmony).length > 800, `${pair.harmony} 太短,可能路径写错了`);
assert.ok(code(pair.electron).length > 800, `${pair.electron} 太短,可能路径写错了`);
}
});