跨端: fix(推送客户端): 登记标记绑定账号(token 换账号是"转移"不是并存 ⇒ 别把登记状态缓存成长期结论)

pi 邮件 `2518e1a3` 的服务端事实:注销只认注册者本人、token 字符串不是凭证,
**同一个 token 换账号登录是"转移"而不是并存** ⇒ "我登记过没有"的答案**随账号而变**。

原来的 `shouldReportToken(lastReported, current)` 只按 token 存标记 ⇒ 换账号后
(同一 token)会**错误地跳过上报**,而那个账号其实还没登记过这个 token。
改成 `reportMarker(accountKey, token) = accountKey|token` + `shouldReportToken(lastMarker, accountKey, token)`:
**账号变了标记必然不同** —— 于是"换账号必然重新上报"是**性质**,不靠人记得 reset。

判据 13 条全绿(新增"换账号后必然重新上报 / 切回来也要重新上报",并断言两次 marker 不相等)。
This commit is contained in:
2026-09-15 11:49:04 +08:00
parent 30886271ea
commit ab31690348
3 changed files with 79 additions and 12 deletions

View File

@ -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 比 —— 比全文是"看起来更严、其实永远为假"的写法', () => {

View File

@ -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);