收口移动壳能力诊断日志

移动壳 HostBridge 能力失败日志只记录稳定阶段标签

更新 badge notification haptics network share navigation 失败测试断言

扩展移动壳配置门禁禁止能力日志打印原生 error 对象

补充 Expo 与 Tauri 宿主壳方案中的移动诊断日志边界
This commit is contained in:
2026-06-20 23:09:49 +08:00
parent 568010b31b
commit 05d278f8f7
15 changed files with 39 additions and 26 deletions
@@ -161,6 +161,31 @@ const bridgeSourceFiles = fs
const hostBridgeSource = bridgeSourceFiles
.map((file) => fs.readFileSync(file, 'utf8'))
.join('\n');
function assertLabelOnlyMobileHostBridgeDiagnostics(source, label, message) {
if (source.includes(`console.warn(\`${message} \${label}\`, error)`)) {
throw new Error(`${label} diagnostics must not log native error objects`);
}
if (!source.includes(`console.warn(\`${message} \${label}\`)`)) {
throw new Error(`${label} diagnostics must use stable label-only logging`);
}
}
for (const [source, label, message] of [
[badgeSource, 'mobile app badge', 'mobile app badge failed for'],
[clipboardSource, 'mobile clipboard', 'mobile clipboard failed for'],
[hapticsSource, 'mobile haptics', 'mobile haptics failed for'],
[
hostBridgeNavigationSource,
'mobile HostBridge navigation',
'mobile HostBridge navigation failed for',
],
[hostBridgeNetworkSource, 'mobile network', 'mobile network failed for'],
[notificationsSource, 'mobile notification', 'mobile notification failed for'],
[shareSource, 'mobile share', 'mobile share failed for'],
]) {
assertLabelOnlyMobileHostBridgeDiagnostics(source, label, message);
}
const urlPath = new URL('../src/shell/url.ts', import.meta.url);
const urlSource = fs.readFileSync(urlPath, 'utf8');
const urlTestPath = new URL('../src/shell/url.test.ts', import.meta.url);
@@ -177,7 +177,6 @@ describe('mobile app badge helper', () => {
expect(Notifications.setBadgeCountAsync).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for permission.current',
error,
);
warnSpy.mockRestore();
});
@@ -204,7 +203,6 @@ describe('mobile app badge helper', () => {
expect(Notifications.setBadgeCountAsync).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for permission.request',
error,
);
warnSpy.mockRestore();
});
@@ -225,7 +223,6 @@ describe('mobile app badge helper', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for update.rejected',
expect.any(Error),
);
warnSpy.mockRestore();
});
@@ -247,7 +244,6 @@ describe('mobile app badge helper', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for update.set_count',
error,
);
warnSpy.mockRestore();
});
+2 -2
View File
@@ -18,8 +18,8 @@ function hasBadgePermission(permission: NotificationPermissionStatus) {
return permission.ios?.allowsBadge === true;
}
function logMobileBadgeFailure(label: string, error: unknown) {
console.warn(`mobile app badge failed for ${label}`, error);
function logMobileBadgeFailure(label: string, _error: unknown) {
console.warn(`mobile app badge failed for ${label}`);
}
async function ensureMobileBadgePermission() {
@@ -618,7 +618,6 @@ describe('handleMobileHostBridgeMessage', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for update.set_count',
nativeError,
);
} finally {
warnSpy.mockRestore();
@@ -972,7 +971,6 @@ describe('handleMobileHostBridgeMessage', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile app badge failed for update.rejected',
expect.any(Error),
);
} finally {
warnSpy.mockRestore();
@@ -80,7 +80,6 @@ describe('mobile haptics helpers', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile haptics failed for impact.dispatch',
nativeError,
);
} finally {
warnSpy.mockRestore();
+2 -2
View File
@@ -17,8 +17,8 @@ function toExpoImpactStyle(style: HostBridgeHapticsImpactStyle) {
: Haptics.ImpactFeedbackStyle.Light;
}
function logMobileHapticsFailure(label: string, error: unknown) {
console.warn(`mobile haptics failed for ${label}`, error);
function logMobileHapticsFailure(label: string, _error: unknown) {
console.warn(`mobile haptics failed for ${label}`);
}
export async function runMobileHapticsImpact(rawStyle: unknown) {
@@ -127,7 +127,6 @@ describe('mobile HostBridge navigation helpers', () => {
expect(Linking.openURL).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile HostBridge navigation failed for external.open',
expect.any(Error),
);
vi.mocked(Linking.canOpenURL).mockResolvedValueOnce(true);
@@ -20,8 +20,8 @@ import {
unsupported,
} from './protocol';
function logMobileHostBridgeNavigationFailure(label: string, error: unknown) {
console.warn(`mobile HostBridge navigation failed for ${label}`, error);
function logMobileHostBridgeNavigationFailure(label: string, _error: unknown) {
console.warn(`mobile HostBridge navigation failed for ${label}`);
}
export async function openMobileHostBridgeExternalUrl(
@@ -91,7 +91,6 @@ describe('mobile HostBridge network helper', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile network failed for status.query',
nativeError,
);
} finally {
warnSpy.mockRestore();
+2 -2
View File
@@ -2,8 +2,8 @@ import { getMobileNetworkStatus } from '../shell/network';
import { type HostBridgeRequest } from '../../../../packages/shared/src/contracts/hostBridge';
import { failure, ok } from './protocol';
function logMobileNetworkFailure(label: string, error: unknown) {
console.warn(`mobile network failed for ${label}`, error);
function logMobileNetworkFailure(label: string, _error: unknown) {
console.warn(`mobile network failed for ${label}`);
}
export async function getMobileHostBridgeNetworkStatus(
@@ -169,7 +169,6 @@ describe('mobile local notification helpers', () => {
expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile notification failed for permission.current',
error,
);
warnSpy.mockRestore();
});
@@ -193,7 +192,6 @@ describe('mobile local notification helpers', () => {
expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile notification failed for permission.request',
error,
);
warnSpy.mockRestore();
});
@@ -252,7 +250,6 @@ describe('mobile local notification helpers', () => {
expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
'mobile notification failed for delivery.schedule',
error,
);
warnSpy.mockRestore();
});
@@ -270,7 +267,6 @@ describe('mobile local notification helpers', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile notification failed for delivery.schedule',
error,
);
warnSpy.mockRestore();
});
@@ -20,8 +20,8 @@ Notifications.setNotificationHandler({
}),
});
function logMobileNotificationFailure(label: string, error: unknown) {
console.warn(`mobile notification failed for ${label}`, error);
function logMobileNotificationFailure(label: string, _error: unknown) {
console.warn(`mobile notification failed for ${label}`);
}
function hasNotificationPermission(
@@ -205,7 +205,6 @@ describe('mobile share helpers', () => {
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile share failed for open.share',
nativeError,
);
});
+2 -2
View File
@@ -10,8 +10,8 @@ import { invalidRequest, ok } from './protocol';
let currentShareTarget: ShareOpenPayload | null = null;
function logMobileShareFailure(label: string, error: unknown) {
console.warn(`mobile share failed for ${label}`, error);
function logMobileShareFailure(label: string, _error: unknown) {
console.warn(`mobile share failed for ${label}`);
}
export function setMobileHostBridgeShareTarget(request: HostBridgeRequest) {
@@ -521,6 +521,8 @@ GameBridge 禁止:
2026-06-18 追加:登录 / 支付能力伪声明进入门禁。`auth.requestLogin``payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo 与 Tauri 壳在没有真实 SDK、渠道流程和后端契约前不得声明这些 capability,也不得把它们写入 `hostCapabilities`;请求到达壳层时必须返回明确 `unsupported_method`,让 H5 fallback 承接。两端壳测试会直接覆盖这两个 method,避免后续半接入时返回伪成功。
2026-06-20 追加:移动壳 HostBridge 能力失败诊断统一只记录稳定标签。Expo 壳的 badge、clipboard、haptics、navigation、network、notification 和 share 等 HostBridge 能力在捕获原生 SDK / WebView / 系统 API 异常时,只能向控制台输出 `mobile ... failed for <stage>` 这类固定阶段标签,不把原生 error 对象、native stack、平台错误码、文件路径或系统返回结构作为日志参数输出;HostBridge response 继续返回共享契约里的稳定 `host_error` / `cancelled` / `unsupported_capability` 等错误。`apps/mobile-shell/scripts/check-config.mjs` 会反查这些能力模块的 label-only 日志形式,避免后续为了排查方便把原生错误对象重新带进可分发移动壳日志。
2026-06-18 追加:微信 / Expo / Tauri 三端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时 等脚手架或替身词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs``apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现;移动壳 EAS / Expo config / Metro export smoke 脚本也属于可分发验收入口,必须纳入移动壳单端结构清单和替身词扫描。根级 `npm run check:native-shells` 会统一扫描 `miniprogram``apps/mobile-shell``apps/desktop-shell`、H5 HostBridge transport 和共享 HostBridge 契约生产源码,防止把临时替身或占位文案带进可分发壳。
2026-06-19 追加:H5 HostBridge 真实调用链扫描改为自动发现。根级 `npm run check:native-shells` 会从 `src/` 生产文件里收集直接导入并调用真实宿主能力 facade 的文件,以及 `useHostLifecycleActive``useHostNetworkOnline``platformProfileHostClipboard` 等薄 wrapper 的消费者;新增 `reloadHostWebView``showHostLocalNotification``setHostAppBadgeCount``importHostTextFile``importHostImageFile``captureHostImageFile``subscribeHostImageDrop``readHostClipboardText``openHostExternalUrl` 等 H5 调用点时,不再手工维护单个扫描文件清单。登录与支付外链属于敏感跳转路径,`src/services/authService.ts``src/services/payment/paymentRedirect.ts` 额外列入必扫清单,防止后续绕回裸浏览器跳转却脱离 HostBridge 调用链门禁。H5 业务文件允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍会拒绝 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。