// 通知开关判据 —— 钉「接入系统通知」**必须是可配置项**(用户强调多次,非可选)。 // // 用户原话:「还有我强调多少遍了,接入系统通知是一个可配置项,因为用户可以自由的部署自己的 // agentmail」。 // // 自部署常态:服务器没有配置华为推送凭证。关着 = 不取 token、不请求系统通知权限、 // 不上报、不打网关 —— 零开销、不弹任何东西、不阻塞启动。这条判据钉的就是这套"开关必须存在 // 且默认关、关着要走零路径"的契约。 // // 四条: // ① 默认关(契约常量 + PushService.isEnabled 读不到即 false); // ② reportToken 在**最前** gate(关 ⇒ getToken 都不调 —— 否则零开销就不成立); // ③ SettingsPage 有 Toggle 且接了回调(用户能看见开关); // ④ EntryAbility 不因开关关而报错(gate 在 PushService 内部 ⇒ onCreate 不用改)。 import { code, prose, stripComments } from './lib/read.mjs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..', '..'); const HARMONY_ETS = join(ROOT, 'client/harmony/entry/src/main/ets'); const read = p => prose(join(HARMONY_ETS, p)); const C = await import(join(HARMONY_ETS, 'model/PushContract.ts')); test('① 契约默认开:DEFAULT_PUSH_ENABLED = true(用户要能默认收到通知)', () => { assert.equal(C.DEFAULT_PUSH_ENABLED, true, '默认必须开 —— 接入系统通知是"服务端没配凭证时优雅降级",不是"默认关掉推送"(2026-09-16 用户实测收不到通知的根因)'); // 标签:关 ⇒ 'off'、开 ⇒ 'on'(口径稳定,给设置页用) assert.equal(C.pushEnabledLabel(false), 'off'); assert.equal(C.pushEnabledLabel(true), 'on'); }); test('② PushService.isEnabled 读不到即开(默认开落点) + setEnabled 存在', () => { const svc = read('api/PushService.ets'); const code_ = stripComments(svc); assert.match(code_, /static isEnabled\(context: common\.Context\): boolean \{/, '要有静态 isEnabled(ability 阶段 onCreate 要读它,不能是实例方法)'); assert.match(code_, /getSync\(SETTING_KEY, DEFAULT_PUSH_ENABLED\)/, '读不到时用 DEFAULT_PUSH_ENABLED(默认开)—— 只有显式存了 false 才关'); assert.match(code_, /static setEnabled\(context: common\.Context, enabled: boolean\): void \{/, '要有静态 setEnabled(设置页 Toggle 回调用)'); }); test('③ reportToken 在最前 gate(关 ⇒ getToken 都不调 ⇒ 零开销)', () => { const svc = read('api/PushService.ets'); const code_ = stripComments(svc); /* * 取 reportToken 的正文(从方法签名到下一个 async/方法声明), * 判"开关检查在 getToken 之前" —— 这是"零开销"的关键: * 关时根本不碰 Push Kit(不取 token、不触网络/权限路径)。 */ const lines = code_.split('\n'); const start = lines.findIndex(l => /async reportToken\(/.test(l)); assert.ok(start > 0, '要能找到 reportToken'); let stop = lines.length; for (let i = start + 1; i < lines.length; i++) { if (/^\s*(async |private |static )\w/.test(lines[i]) || /^}\s*$/.test(lines[i])) { // 粗略:下一段方法或闭合 — 取 200 行作截断 stop = Math.min(i + 1, start + 200); break; } } const body = lines.slice(start, stop).join('\n'); const gateAt = body.indexOf('PushService.isEnabled'); const getTokenAt = body.indexOf('this.getToken()'); assert.ok(gateAt > 0, 'reportToken 里要有 isEnabled gate'); assert.ok(getTokenAt > 0, 'reportToken 里要有 getToken'); assert.ok(gateAt < getTokenAt, '开关 gate 必须在 getToken 之前 —— 否则关时仍会取 token(零开销不成立)'); }); test('④ SettingsPage 有 Toggle 且接了 togglePush 回调(用户能看见开关)', () => { const settings = read('pages/SettingsPage.ets'); assert.match(settings, /Toggle\(\{ type: ToggleType\.Switch, isOn: this\.pushEnabled \}\)/, '设置页要有 Toggle(Switch) 绑定 pushEnabled'); assert.match(settings, /togglePush\(isOn\)/, 'Toggle 的 onChange 要调 togglePush'); assert.match(settings, /PushService\.isEnabled/, 'aboutToAppear 要读一次开关回填'); assert.match(settings, /PushService\.setEnabled/, 'togglePush 要写持久化'); // 状态提示(服务器未配置推送时如实告诉用户) assert.match(settings, /服务器未配置推送/, '服务器没配通道时要说清(否则用户开了以为有用)'); });