diff --git a/client/electron/test/harmony-logic.test.mjs b/client/electron/test/harmony-logic.test.mjs index 4b5bda3..13fcbe2 100644 --- a/client/electron/test/harmony-logic.test.mjs +++ b/client/electron/test/harmony-logic.test.mjs @@ -45,6 +45,22 @@ const page = code(join(HARMONY_ETS, 'pages/MainPage.ets')); * 已经用过一次(遮罩那段注释里引用了旧值),这里统一成常态。 */ const pageCode = page.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); +/* + * ★★ 2026-09-20:再加一份「取数/组装那层」的源码。 + * + * 用户裁定做 A(harmony 补 store 层)之后,**收件箱的取数与组装 + * (`sumUnreadTotals` / `partialLoadNotice` / `splitByPermission` / + * `groupMailsBySession` 的调用)从 `MainPage.ets` 搬进了 + * `common/MailStore.ets`**。 + * + * 于是本文件里几条"页面要用这个纯逻辑"的判据**锚错了文件** —— 它们读 + * `pageCode`,而那些调用现在在 store 里。这不是判据发现了 bug,是判据 + * 锚在了**实现位置**而不是**它声称的不变量**("这条纯逻辑真的被用上了")。 + * + * 所以这里引入 `compositionCode`:**两处合起来**看。 + * 不变量是"这个纯函数在数据流里被用了",用它的人搬去哪一层不该让判据红。 + */ +const compositionCode = pageCode + '\n' + code(join(HARMONY_ETS, 'common/MailStore.ets')); const webGroups = code(join(ROOT, 'client/electron/src/lib/mailGroups.ts')); const webCard = code(join(ROOT, 'client/electron/src/components/WorkCard.tsx')); @@ -144,14 +160,14 @@ test('未读数用服务端 total 相加(它是 CountUnread,权威),负 assert.equal(H.sumUnreadTotals([3, 4]), 7); assert.equal(H.sumUnreadTotals([0, -1, 2]), 2); assert.equal(H.sumUnreadTotals([]), 0); - assert.match(pageCode, /sumUnreadTotals\(/, '页面要用服务端未读数,而不是数这一页'); + assert.match(compositionCode, /sumUnreadTotals\(/, '数据流里要用服务端未读数,而不是数这一页'); }); test('只有"取满了这一页"才提示可能还有更多(服务端 total 是未读数,不是总封数)', () => { assert.equal(H.partialLoadNotice(12, 50), '', '没取满就别吓人'); assert.equal(H.partialLoadNotice(50, 50), '已加载 50 封(本页上限 50,可能还有更多)'); assert.equal(H.partialLoadNotice(0, 0), '', 'limit 不合法时不提示'); - assert.match(pageCode, /partialLoadNotice\(/, '页面要用这条提示'); + assert.match(compositionCode, /partialLoadNotice\(/, '数据流里要用这条提示'); }); test('★ 界面不再把"服务端未读数"当成"总封数"显示', () => { @@ -212,11 +228,32 @@ test('卡片上"最新一封是谁发的":人 vs Agent(决定人要不要接 // ───────────────────────── 两层的接合:页面确实调了被测逻辑 ───────────────────────── -test('页面把折叠逻辑真正接上了(不是"逻辑写好了没人用")', () => { - assert.match(pageCode, /import \{[\s\S]*groupMailsBySession[\s\S]*\} from '\.\.\/model\/MailGrouping'/, '页面要 import 折叠逻辑'); +test('折叠逻辑真正接上了(不是"逻辑写好了没人用")—— 取数侧 + 渲染侧都要在', () => { + /* + * ★★ 2026-09-20 拆成两半(用户裁定做 A:补 store 层之后本判据红了)。 + * + * 它原来把两件事混在一条里,都断言在 `pageCode`(`MainPage.ets`)上: + * · **取数侧**:"加载后要折叠"—— `this.groups = groupMailsBySession(inboxMails)` + * · **渲染侧**:"单封平铺 / 多封按展开态 / 组头可点" + * 而 A 步把取数侧搬进了 `common/MailStore.ets`。于是判据红的是**取数侧那一条**, + * 而它声称的不变量("折叠逻辑真的被用上了")其实**仍然成立**。 + * + * 这正是本仓反复出现的形状:**判据锚在实现位置上,而不是它声称的东西上**。 + * 一处实现搬家(哪怕搬得更对)就会假红 —— 而假红比漏报更坏, + * 它会让人去改本来对的代码。 + * + * 拆法按**归属**分: + * · 取数/组装 → `compositionCode`(页面 + store 合起来看) + * · 渲染 → 仍钉 `pageCode`(那是页面该管的事,不该被搬走) + */ + assert.match(compositionCode, + /import \{[\s\S]*groupMailsBySession[\s\S]*\} from '\.\.\/model\/MailGrouping'/, + '数据流那一层要 import 折叠逻辑'); // 加载后要折叠。变量是**筛过权限邮件之后**的那一批(权限邮件归授权栏, // 收件箱里不该出现它们 —— 见"权限邮件不进收件箱"那条)。 - assert.match(pageCode, /this\.groups = groupMailsBySession\(inboxMails\)/, '加载后要折叠(筛过权限邮件的那批)'); + assert.match(compositionCode, /groupMailsBySession\(split\.normal\)/, + '加载后要折叠(筛过权限邮件的那批)—— A 步之后这行在 MailStore 里'); + /* 渲染侧:下面三条仍必须落在页面上 —— 它们描述的是"用户看到什么" */ assert.match(pageCode, /if \(isFlatGroup\(g\)\)/, '单封的组要平铺渲染(这条就是"点开会多一次点击"的那个分支)'); assert.match(pageCode, /this\.isExpanded\(g\.key\)/, '多封的组要按展开状态渲染'); assert.match(pageCode, /toggleExpanded\(g\.key\)/, '组头要能点开(用户真正会点的那一层)'); @@ -574,10 +611,38 @@ test('通信页把三栏真的接上了:内部页签 + 徽标 + 悬浮加号 + // 收件箱那一栏必须**先分家再折叠**(否则权限邮件会混进收件箱,正是这条要防的) // 注意切到那一行**之后**再截断:原来截到 indexOf 处,正好把要断言的那一行切掉, // 判据于是报"再折叠筛过的那些"失败 —— 判据自己的切片边界错了(变异测试式的自省) - const gStart = sendCode.indexOf('this.groups = groupMailsBySession'); - const inboxLoad = sendCode.slice(sendCode.indexOf('async loadData'), gStart + 60); - assert.match(inboxLoad, /splitByPermission\(mergedMails\)/, '收件箱要先分家'); - assert.match(inboxLoad, /groupMailsBySession\(inboxMails\)/, '再折叠筛过的那些'); + /* + * ★★ 2026-09-20:这两条改读 store(A 步之后取数/組装在 `MailStore.ets`)。 + * + * 原来按 `sendCode`(MainPage)从 `async loadData` 切到 + * `this.groups = groupMailsBySession` 之间取片段 —— 那段现在在 store 里, + * 页面里的 `loadData` 只剩三行调用。判据于是假红(它声称的"先分家再折叠" + * 仍然成立,只是实现搬了家)。 + * + * 仍按**顺序**判(先 split 再 group)——那是这条不变量真正的形状, + * 与在哪个文件里无关。 + */ + const storeCode = code(join(HARMONY_ETS, 'common/MailStore.ets')); + /* + * ★★ 只查"两行都在"是不够的 —— 我第一版就是这么写的,而**变异测试没咬住**: + * 把顺序反过来(先 `groupMailsBySession(全部)` 再 `splitByPermission`)之后 + * 两行仍然都在、`indexOf` 也仍然一大一小(因为第二行的字面量变了, + * 而我当时匹配的是 `groupMailsBySession(split.normal)` ⇒ 它直接找不到, + * 却因为我把断言写成"两个都 >=0 且 splitAt < groupAt",反过来之后 + * `groupAt` 变成 -1 ⇒ 那次是**另一个断言**红的,不是顺序这条)。 + * + * 真正要钉的是:**`splitByPermission` 的结果被喂给 `groupMailsBySession`**。 + * 那就直接断言那个**数据流**形状 —— 而不是两行的相对位置。 + */ + assert.match(storeCode, /snap\.groups = groupMailsBySession\(split\.normal\)/, + '收件箱那一栏要折叠**分家之后**的普通邮件(`split.normal`)—— ' + + '喂 `mergedMails`(含权限请求)就是把权限邮件混进收件箱,这条判据防的正是它'); + assert.match(storeCode, /const split: MailSplit = splitByPermission\(mergedMails\)/, + '分家要在合并后的全量上做一次(`mergedMails`),而不是在某一账号的局部'); + const splitAt = storeCode.indexOf('splitByPermission(mergedMails)'); + const groupAt = storeCode.indexOf('groupMailsBySession(split.normal)'); + assert.ok(splitAt >= 0 && groupAt >= 0 && splitAt < groupAt, + '★ 先分家、后折叠(顺序反了会先折叠进权限邮件,再想筛也晚了)'); }); test('发件箱与授权栏走的是与 WebUI 相同的接口(路径、字段、备注都要对)', () => {