锁定移动壳运行时能力回包

移动 HostBridge runtime 单测精确断言共享能力 profile

移动壳配置门禁反查 runtime 能力 profile 断言

补充原生壳方案文档和共享决策记录
This commit is contained in:
2026-06-20 20:28:59 +08:00
parent e563ae0a81
commit 4623709a39
4 changed files with 11 additions and 11 deletions
@@ -2672,8 +2672,12 @@ for (const snippet of [
'reports iOS runtime with host version, bridge version and iOS capabilities',
'reports Android runtime without iOS-only app badge capability',
'wraps runtime metadata in the HostBridge response shape',
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
'HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES',
'hostVersion: MOBILE_SHELL_HOST_VERSION',
'bridgeVersion: HOST_BRIDGE_VERSION',
'expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES)',
'expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES)',
"not.toContain('app.setBadgeCount')",
"expect(getMobileRuntimePlatform()).toBe('android')",
]) {
@@ -2,6 +2,8 @@ import { Platform } from 'react-native';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
type HostBridgeRequest,
@@ -50,15 +52,7 @@ describe('mobile HostBridge runtime helper', () => {
bridgeVersion: HOST_BRIDGE_VERSION,
}),
);
expect(runtime.capabilities).toEqual(
expect.arrayContaining([
'host.getRuntime',
'appearance.getColorScheme',
'network.status',
'app.setBadgeCount',
'file.captureImage',
]),
);
expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES);
});
test('reports Android runtime without iOS-only app badge capability', () => {
@@ -75,8 +69,7 @@ describe('mobile HostBridge runtime helper', () => {
bridgeVersion: HOST_BRIDGE_VERSION,
}),
);
expect(runtime.capabilities).toContain('host.getRuntime');
expect(runtime.capabilities).toContain('file.captureImage');
expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES);
expect(runtime.capabilities).not.toContain('app.setBadgeCount');
});
@@ -171,6 +171,7 @@
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod``normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误,Tauri 壳的 `failed(...)` 出口也必须先校验同一错误码白名单;未知原生错误对象或非法错误码统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。
- 2026-06-20 桌面 HostBridge command facade 单测边界:Tauri 唯一 `host_bridge_request` command 必须先通过 `prepare_host_bridge_request(...)` 做 envelope、method 和 request id 校验,再进入 `HostBridgeReplayState` reserve / wait / execute`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs` 的单测必须覆盖非法 envelope 在 replay 前返回 `invalid_request` 且不会占用对应 request id 的 replay slot,桌面配置检查会反查该测试存在。
- 2026-06-20 桌面 HostBridge replay 内部失败边界:Tauri `HostBridgeReplayState` 的 cache lock、slot lock 和 condvar wait 异常不得 panic,也不得把 Rust 内部错误细节回传给 H5;桌面壳只写 `desktop host bridge replay failed for ...` stderr 观测日志,并统一返回 `host_error: desktop host bridge request failed`。桌面配置检查反查 `reserve(...)``Result` 出口、稳定错误响应和 poison lock 单测。
- 2026-06-20 移动 HostBridge runtime 能力回包边界:Expo `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES``HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,并使用与 `platform` 字段一致的归一平台值选择 profile;移动壳 runtime 单测和配置检查反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 能力 query 或共享 profile 分叉。
- 2026-06-20 移动壳协议 helper 单测边界:`apps/mobile-shell/src/host-bridge/protocol.test.ts` 直接覆盖 Expo 移动壳 HostBridge JSON 解析、envelope 和 request id 校验、未知 method 拒绝、ok / failure 响应包装、unsupported / invalid_request 错误构造,以及 native helper 错误归一时只透传共享错误码与字符串 message,不泄露非法错误码、nativeStack 或其它私有字段;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免协议边界只靠完整 bridge 流程间接覆盖。
- 2026-06-20 移动扫码 overlay 单测边界:`apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx` 直接覆盖移动扫码 overlay 的相机权限请求、二维码扫码成功、权限拒绝失败和关闭取消;单端配置检查会反查该组件测试存在,根级 `npm run check:native-shells` 会把该测试文件列入移动 shell 层结构清单,避免扫码 UI 容器只靠 `ShellApp.test.tsx` 的完整 HostBridge 流程间接覆盖。
- 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 methodTauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。
@@ -495,6 +495,8 @@ GameBridge 禁止:
2026-06-20 追加:Tauri 桌面壳 HostBridge replay 的内部锁、等待和缓存状态异常只写入桌面壳 stderr 观测日志,H5 侧统一收到 `host_error` 与固定文案 `desktop host bridge request failed``HostBridgeReplayState::reserve(...)` 返回 `Result``complete(...)` / `wait_for_response(...)` 也不得用 `expect` 让宿主进程 panic。桌面配置检查会反查 replay 失败日志、稳定错误响应和 poison lock 单测,避免后续把内部 mutex / condvar 细节或 Rust panic 泄露到 H5 调用链。
2026-06-20 追加:Expo 移动壳 `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约中的 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES``HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,且使用同一个已归一的平台值选择 profile;移动壳 runtime 单测和配置检查会反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 或共享 profile 分叉。
2026-06-18 追加:HostBridge method 白名单进入跨壳门禁。`packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳的 HostBridge 分发 case 不得处理共享契约外 methodTauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。两端配置检查会在 `npm run check:native-shells` 中拒绝 method 白名单漂移,新增宿主能力必须先更新共享契约,再落壳实现。
2026-06-19 追加:HostBridge event 白名单进入跨壳门禁。`packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_EVENTS` 是宿主注入事件名的唯一来源,当前只包含 `app.lifecycle``network.statusChanged``navigation.canGoBack``file.imageDropped`;事件名必须存在于 capability 白名单,各宿主壳只声明自身真实发射的事件 capability。Expo 壳事件注入函数使用 `HostBridgeEventName`Tauri 壳 `shell/events.rs` 镜像同一清单并在脚本生成前拒绝未知事件,H5 `nativeAppHostBridge` 只分发 `isHostBridgeEventName()` 认可的事件。H5 `subscribeHostAppLifecycle()``subscribeHostNetworkStatusChange()``subscribeHostNavigationCanGoBack()``subscribeHostImageDrop()` 必须同时校验 `host.events` 与对应事件 capability,缺任一能力时不绑定事件监听;根级 `npm run check:native-shells` 会反查这四个订阅 facade 都通过 `canUseNativeHostEventCapability(...)` 进入同一双能力门控,并拒绝新增事件后漏补 H5 facade 覆盖。