跨端: feat(推送客户端): 契约层 model/PushContract.ts + 6 条设备无关判据(静默失败/enabled:false 正常态/按 provider+tail 比/按 mail_id 去重)
pi 2026-09-14 推送契约的客户端半边。四条不变量里**三条是纯逻辑**,所以先落这三条, 平台调用(Push Kit 取 token、通知权限)留在下一步的 `PushService.ets` 里。 - `shouldReportToken`:取不到 token 不上报;与上次相同不重复上报(否则每次启动打一次接口); - `tokenTail` / `isRegistered`:GET **只回尾 6 位** ⇒ "登记过没有"必须按 `provider + tail` 比。 比全文是**看起来更严、其实永远为假**的写法(全文永不等于尾 6 位 ⇒ 每次启动重复上报), 所以有判据钉它,并在注释里写明"同尾 6 位即视为同一条 —— 这是服务端给的信息量的上界,不是我们的选择"; - `classifyRegister`:`ok-enabled` / `ok-disabled` / `silent-skip` —— **三种里没有一种是"提示失败"** (`enabled:false` 是自部署常态 ⇒ 不重试、不提示); - `parseNotificationData`:不满足约定形状就返回 undefined(坏 JSON 不抛、缺 mail_id/动作不符都忽略)—— 推送是可选通道,收到不认识的东西不许有任何副作用; - `NotificationLedger`:服务端**无幂等键**(至多一次、无重试/去重表)⇒ 重复保护落客户端;台账**有界**。 另有一条判据禁止契约层引入 `@ohos`/`@kit`(否则这些判据会退化成必须上设备)。 它第一次跑**咬到了解释这条规则的那行注释** ⇒ 改扫 `code()`(去注释),与前面扫描口径那次同族。 `npm test`(install 相位)全绿;余额 `debts=13`。
This commit is contained in:
73
client/electron/test/harmony-push.test.mjs
Normal file
73
client/electron/test/harmony-push.test.mjs
Normal file
@ -0,0 +1,73 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { code } from './lib/read.mjs';
|
||||
|
||||
/*
|
||||
推送客户端契约层(pi 2026-09-14)。四条不变量里**三条是纯逻辑**,所以不需要设备就能钉:
|
||||
静默失败、`enabled:false` 是正常态、按 `provider+tail` 比、通知按 `mail_id` 去重。
|
||||
*/
|
||||
|
||||
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('★ "登记过没有"只按 provider + tail 比 —— 比全文是"看起来更严、其实永远为假"的写法', () => {
|
||||
const token = 'AAAABBBBCCCC123456';
|
||||
const list = [{ provider: 'hms', token_tail: '123456' }];
|
||||
assert.equal(C.tokenTail(token), '123456', 'tail 取尾 6 位(与服务端同口径)');
|
||||
assert.equal(C.isRegistered(list, 'hms', token), true, '尾 6 位相同就算登记过');
|
||||
// 反证:为什么不能比全文 —— 服务端只回 tail,全文永远不等于 tail
|
||||
assert.notEqual(token, list[0].token_tail, 'GET 只回尾 6 位');
|
||||
assert.equal(C.isRegistered(list, 'apns', token), false, 'provider 维度必须参与比较');
|
||||
assert.equal(C.isRegistered(list, 'hms', 'ZZZZZZZZZZZZ123456'), true,
|
||||
'同尾 6 位即视为同一条 —— 这是服务端给的信息量的上界,不是我们的选择');
|
||||
assert.equal(C.tokenTail('abc'), 'abc', '短于 6 位时取全文(不补零、不截空)');
|
||||
});
|
||||
|
||||
test('★ 上报结果分类:三种里没有一种是"提示失败"(enabled:false 是正常态)', () => {
|
||||
assert.equal(C.classifyRegister(true, false), 'ok-enabled');
|
||||
assert.equal(C.classifyRegister(false, false), 'ok-disabled', 'enabled:false 是正常态 ⇒ 不重试、不提示');
|
||||
assert.equal(C.classifyRegister(true, true), 'silent-skip', '失败归 silent-skip,即使服务端 enabled=true');
|
||||
assert.equal(C.classifyRegister(false, true), 'silent-skip');
|
||||
});
|
||||
|
||||
test('★ 通知 data:不满足约定形状就静默忽略(不"尽力打开某个页面")', () => {
|
||||
const ok = C.parseNotificationData(JSON.stringify({ type: 'new_mail', mail_id: 'm1', session_id: 's1', action: 'open_mail' }));
|
||||
assert.equal(ok.mail_id, 'm1');
|
||||
assert.equal(ok.session_id, 's1');
|
||||
assert.equal(C.parseNotificationData(''), undefined, '空串');
|
||||
assert.equal(C.parseNotificationData('{不是 json'), undefined, '坏 JSON 不许抛(推送是可选通道)');
|
||||
assert.equal(C.parseNotificationData(JSON.stringify({ action: 'open_mail' })), undefined, '缺 mail_id');
|
||||
assert.equal(C.parseNotificationData(JSON.stringify({ mail_id: 'm1', action: 'other' })), undefined, '动作不是 open_mail');
|
||||
assert.equal(C.parseNotificationData(JSON.stringify({ mail_id: '', action: 'open_mail' })), undefined, '空 mail_id');
|
||||
});
|
||||
|
||||
test('★ 去重台账:服务端无幂等键 ⇒ 重复保护落客户端;且有界', () => {
|
||||
const led = new C.NotificationLedger(3);
|
||||
assert.equal(led.shouldHandle('m1'), true, '第一次该处理');
|
||||
assert.equal(led.shouldHandle('m1'), false, '重复必须丢弃(服务端至多一次、无幂等键)');
|
||||
assert.equal(led.shouldHandle('m2'), true);
|
||||
assert.equal(led.shouldHandle(''), false, '空 id 不处理');
|
||||
assert.equal(led.size(), 2);
|
||||
led.shouldHandle('m3'); led.shouldHandle('m4');
|
||||
assert.equal(led.size(), 3, '有界(常驻 pane 不许无限长)');
|
||||
assert.equal(led.shouldHandle('m1'), true, '最旧的被挤出后可再处理(有界台账的代价,明写在这里)');
|
||||
});
|
||||
|
||||
test('★ 契约层必须保持"无 @ohos 依赖"(否则这些判据跑不了,会退化成必须上设备)', () => {
|
||||
// 扫 code()(去注释):第一次跑这条时它咬到了**解释这条规则的那行注释** —— 与扫描口径同族的现成例子
|
||||
const src = code(join(HARMONY, 'PushContract.ts'));
|
||||
assert.ok(!/@ohos|@kit\./.test(src),
|
||||
'PushContract.ts 里出现了 @ohos/@kit 依赖 ⇒ 判据将无法用 node 直接跑。' +
|
||||
'**正确修法**:把平台调用留在 PushService.ets,纯决策留在这里(与 Calendar/Wallpaper 同模式)。' +
|
||||
'**最常见的错误修法**:把这条断言删掉,让契约层的判据跟着一起失效。');
|
||||
});
|
||||
@ -79,6 +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'], 6],
|
||||
['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],
|
||||
|
||||
Reference in New Issue
Block a user