From ab31690348bf3a5b82a1dfe8b5e27e76a1b4fe7c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 11:49:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20fix(=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF):=20=E7=99=BB=E8=AE=B0=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E7=BB=91=E5=AE=9A=E8=B4=A6=E5=8F=B7=EF=BC=88token=20?= =?UTF-8?q?=E6=8D=A2=E8=B4=A6=E5=8F=B7=E6=98=AF"=E8=BD=AC=E7=A7=BB"?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E5=B9=B6=E5=AD=98=20=E2=87=92=20=E5=88=AB?= =?UTF-8?q?=E6=8A=8A=E7=99=BB=E8=AE=B0=E7=8A=B6=E6=80=81=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=88=90=E9=95=BF=E6=9C=9F=E7=BB=93=E8=AE=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 邮件 `2518e1a3` 的服务端事实:注销只认注册者本人、token 字符串不是凭证, **同一个 token 换账号登录是"转移"而不是并存** ⇒ "我登记过没有"的答案**随账号而变**。 原来的 `shouldReportToken(lastReported, current)` 只按 token 存标记 ⇒ 换账号后 (同一 token)会**错误地跳过上报**,而那个账号其实还没登记过这个 token。 改成 `reportMarker(accountKey, token) = accountKey|token` + `shouldReportToken(lastMarker, accountKey, token)`: **账号变了标记必然不同** —— 于是"换账号必然重新上报"是**性质**,不靠人记得 reset。 判据 13 条全绿(新增"换账号后必然重新上报 / 切回来也要重新上报",并断言两次 marker 不相等)。 --- client/electron/test/harmony-push.test.mjs | 26 +++++++++--- client/electron/test/run-all.mjs | 41 +++++++++++++++++-- .../entry/src/main/ets/model/PushContract.ts | 24 +++++++++-- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/client/electron/test/harmony-push.test.mjs b/client/electron/test/harmony-push.test.mjs index 492cb00..0318858 100644 --- a/client/electron/test/harmony-push.test.mjs +++ b/client/electron/test/harmony-push.test.mjs @@ -13,11 +13,27 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const HARMONY = join(HERE, '..', '..', 'harmony', 'entry', 'src', 'main', 'ets', 'model'); const C = await import(pathToFileURL(join(HARMONY, 'PushContract.ts')).href); -test('★ 上报决策:空 token 不上报;与上次相同不上报;变了才上报', () => { - assert.equal(C.shouldReportToken('', ''), false, '取不到 token 就不许上报(静默跳过)'); - assert.equal(C.shouldReportToken('abc', 'abc'), false, '没变就不许重复上报(否则每次启动打一次接口)'); - assert.equal(C.shouldReportToken('abc', 'xyz'), true, '变了要上报'); - assert.equal(C.shouldReportToken('', 'xyz'), true, '第一次拿到要上报'); +test('★ 上报决策:空 token 不上报;同账号同 token 不上报;变了才上报', () => { + assert.equal(C.shouldReportToken('', 'acct1', ''), false, '取不到 token 就不许上报(静默跳过)'); + const m = C.reportMarker('acct1', 'abc'); + assert.equal(C.shouldReportToken(m, 'acct1', 'abc'), false, '没变就不许重复上报(否则每次启动打一次接口)'); + assert.equal(C.shouldReportToken(m, 'acct1', 'xyz'), true, 'token 变了要上报'); + assert.equal(C.shouldReportToken('', 'acct1', 'xyz'), true, '第一次拿到要上报'); +}); + +/* +★ 账号必须绑进标记(pi 邮件 `2518e1a3` 的服务端事实):同一个 token **换账号登录是"转移"不是并存**, +所以"我登记过没有"的答案**随账号而变**。只按 token 存标记 ⇒ 换账号后**错误跳过上报**, +而那个账号其实没登记过。这条判据钉的是"换账号必然重新上报"这个性质。 +*/ +test('★ 换账号后必然重新上报(token 转移不是并存 —— 别把登记状态缓存成长期结论)', () => { + const token = 'AAAABBBBCCCC123456'; + const m1 = C.reportMarker('acct1', token); + assert.equal(C.shouldReportToken(m1, 'acct1', token), false, '同账号同 token:不重复上报'); + assert.equal(C.shouldReportToken(m1, 'acct2', token), true, + '换账号 + 同一个 token ⇒ 必须重新上报(服务端是转移,acct2 其实没登记过)'); + assert.notEqual(m1, C.reportMarker('acct2', token), '标记的形状就保证账号变了必然不同(不靠人记得 reset)'); + assert.equal(C.shouldReportToken(C.reportMarker('acct2', token), 'acct1', token), true, '切回来也要重新上报'); }); test('★ "登记过没有"只按 provider + tail 比 —— 比全文是"看起来更严、其实永远为假"的写法', () => { diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 76dcc76..a19cfb1 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -79,7 +79,7 @@ const SUITE = [ ['test/build-stamp.test.mjs', [], 7], ['test/packaging.test.mjs', [], 5], ['test/align-refs.test.mjs', [], 3], - ['test/harmony-push.test.mjs', ['--experimental-strip-types', '--no-warnings'], 12], + ['test/harmony-push.test.mjs', ['--experimental-strip-types', '--no-warnings'], 13], ['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 10], ['test/debt-visibility.test.mjs', [], 1], ['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 2], @@ -178,6 +178,20 @@ if (shapeless.length) { } const reds = []; +/* + * 判据**条数**的累计(pi 2026-09-15)。 + * + * 为什么要有它:套件此前**不打总数** —— 于是每次要"一共多少条、过了多少条", + * 都得在套件外面用 `node --test test/*.test.mjs` 那样的 **glob 调用法**绕出去。 + * 而 glob 那套正是让"写了判据却没接线"逃掉的那个调用法(那次 `SUITE` 里少两个文件, + * 绕出去的 glob 反而把它们跑绿了)。⇒ 权威来源必须由**套件自己**给出: + * 它只累加**它自己实际跑过并解析到自报条数**的那些文件,不猜、不扫目录。 + */ +let totalTests = 0; +/** 逐文件自报的**失败**条数之和(`fail=` 那一格)。`pass + fail == checks` 是恒等式 */ +let totalFail = 0; +/** 实际跑过(并解析到自报条数)的文件数 —— 与 `SUITE.length` 分开报,差额看得见 */ +let ranFilesCount = 0; /* * ─── 相位(pi 2026-09-14 裁定)─── * @@ -269,10 +283,14 @@ for (const [file, flags, expected] of SUITE) { */ const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out); const nodeTest = /^# pass (\d+)/m.exec(out); + const nodeFail = /^# fail (\d+)/m.exec(out); const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null); // 累计总数(见汇总末尾的 `RESULT files=…`)—— 解析成功就加,无论该文件红绿 - if (ran !== null) totalTests += ran; - if (ran === null) { + if (ran !== null) { + totalTests += ran; + ranFilesCount += 1; + totalFail += marker ? Number(marker[2]) : (nodeFail ? Number(nodeFail[1]) : 0); + } if (ran === null) { /* * 报错**自带修法**(pi 2026-09-14):这条契约的受众不只是读过规范的人 —— * 并发写 WebUI 的 agent 新加判据时不会打开 CRITERIA.md,看到红的第一反应 @@ -601,6 +619,21 @@ console.log(`RESULT phase=${PHASE} static=${STATIC_ONLY.length}${debtsLine} prob : '(只能验形态的判据:到期前提成立就自动变红)') + mutantsLine); console.log(`\n========== 判据汇总 ==========`); +/* + * 判据**条数**的权威来源(pi 2026-09-15)。 + * 格式与其它 `RESULT` 行同族:机器可读、一行、**套件自己打的**。 + * + * ⚠️ 单位要写清:`checks` 是**逐文件自报条数之和** —— 也就是套件在每跑完一个文件时 + * 真正解析过、并据此判"有没有少跑"的那个数。它**混两种单位**: + * `check()` 文件自报 `RESULT pass=N`(N 条判据),`node:test` 文件自报 `# pass N` + * (N 个 test 用例)。两者都是"条",但**不是同一种条**,所以这里叫 `checks` 而不叫 + * `tests`,免得下一个人把它跟 `node --test test/*.test.mjs` 的 glob 口径直接比 —— + * 那个口径只跑标准 `test/*.test.mjs`,**不跑套件里的构建相位**,也不含本次相位跳过。 + * `pass + fail == checks` 恒成立(fail 是那些文件里的红条数)。 + */ +const totalsLine = (verdict) => `RESULT files=${SUITE.length} ran=${ranFilesCount} ` + + `checks=${totalTests} pass=${totalTests - totalFail} fail=${totalFail} ` + + `broken=${brokens.length} verdict=${verdict}`; if (reds.length === 0 && brokens.length === 0) { // 报**本相位实际跑了**几条,不报清单长度 —— 上面刚写完"不能声称自己做过的事", // 这里如果打 SUITE.length 就是在同一行里犯同一个错(2 条构建相位判据被跳过了)。 @@ -608,6 +641,7 @@ if (reds.length === 0 && brokens.length === 0) { console.log(`全部通过(${ranFiles.length} 个判据文件:` + `${ranFiles.map(f => f.replace('test/', '').replace('.test.mjs', '')).join('、')}` + `${skippedByPhase.length ? `;另有 ${skippedByPhase.length} 条属构建相位,本相位不判` : ''})`); + console.log(totalsLine('green')); process.exit(0); } // broken 先报:它比红更严重(红是"判据说不成立",broken 是"判据没说话") @@ -619,4 +653,5 @@ if (reds.length) { console.error(`红的判据(${reds.length}/${SUITE.length}):`); for (const r of reds) console.error(` - ${r}`); } +console.log(totalsLine('red')); process.exit(1); diff --git a/client/harmony/entry/src/main/ets/model/PushContract.ts b/client/harmony/entry/src/main/ets/model/PushContract.ts index db01d14..01ccd1b 100644 --- a/client/harmony/entry/src/main/ets/model/PushContract.ts +++ b/client/harmony/entry/src/main/ets/model/PushContract.ts @@ -40,12 +40,28 @@ export interface PushNotificationData { action: string; } -/** 上报决策:只有"有 token,且与上次上报的不同"才上报 —— 避免每次启动都打一次接口 */ -export function shouldReportToken(lastReported: string, current: string): boolean { - if (current.length === 0) { +/** + * 上报标记:**必须把账号绑进去**。 + * + * 服务端事实(pi 邮件 `2518e1a3`):注销只认**注册者本人**,token 字符串不是凭证; + * **同一个 token 换账号登录是"转移",不是并存**。所以"我登记过没有"这个问题的答案 + * **会随账号而变** —— 若把标记只按 token 存(`lastReported === token` 就跳过上报), + * 换了账号之后我会**错误地跳过上报**,而那个账号其实还没登记过这个 token。 + * ⇒ 标记的形状就是 `accountKey|token`:**账号变了,标记必然不同**,不必靠人记得 reset。 + */ +export function reportMarker(accountKey: string, token: string): string { + return `${accountKey}|${token}`; +} + +/** + * 上报决策:只有"有 token,且**本账号**上次上报的不是它"才上报 —— 避免每次启动都打一次接口。 + * 账号切换后必然重新上报(见 `reportMarker` 的理由),这是**性质**,不是特判。 + */ +export function shouldReportToken(lastMarker: string, accountKey: string, token: string): boolean { + if (token.length === 0) { return false; } - return lastReported !== current; + return lastMarker !== reportMarker(accountKey, token); } /** 取尾 6 位(与服务端 `token_tail` 同口径;token 短于 6 位时取全文) */