补齐移动分发未声明能力测试

移动 dispatch 直接覆盖未声明 HostBridge method 返回 unsupported

原生壳结构门禁登记移动 dispatch 测试文件

共享决策日志补充移动 dispatch 单测边界
This commit is contained in:
2026-06-20 10:32:09 +08:00
parent d967b9cea0
commit 00f4af13c6
4 changed files with 138 additions and 3 deletions
+9 -3
View File
@@ -44,6 +44,11 @@ const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
const dispatchTestPath = new URL(
'../src/host-bridge/dispatch.test.ts',
import.meta.url,
);
const dispatchTestSource = fs.readFileSync(dispatchTestPath, 'utf8');
const filesPath = new URL('../src/host-bridge/files.ts', import.meta.url);
const filesSource = fs.readFileSync(filesPath, 'utf8');
const filesTestPath = new URL('../src/host-bridge/files.test.ts', import.meta.url);
@@ -955,11 +960,12 @@ for (const snippet of [
'const IOS_UNDECLARED_HOST_BRIDGE_METHODS = HOST_BRIDGE_METHODS.filter(',
'(method) => !HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES.includes(method)',
'test.each(IOS_UNDECLARED_HOST_BRIDGE_METHODS)',
"expect(failedResponse.error.code).toBe('unsupported_method')",
'dispatchMobileHostBridgeRequest(request(method))',
"expect(response.error.code).toBe('unsupported_method')",
]) {
if (!bridgeTestSource.includes(snippet)) {
if (!dispatchTestSource.includes(snippet)) {
throw new Error(
`mobile shell bridge tests must derive unsupported method coverage from shared profiles: ${snippet}`,
`mobile shell dispatch tests must derive unsupported method coverage from shared profiles: ${snippet}`,
);
}
}
@@ -0,0 +1,127 @@
import { describe, expect, test, vi } from 'vitest';
import {
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_METHODS,
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
type HostBridgeMethod,
type HostBridgeRequest,
} from '../../../../packages/shared/src/contracts/hostBridge';
import {
dispatchMobileHostBridgeRequest,
resetMobileHostBridgeDispatchForTest,
} from './dispatch';
vi.mock('expo-clipboard', () => ({
getStringAsync: vi.fn(),
setStringAsync: vi.fn(),
}));
vi.mock('expo-document-picker', () => ({
getDocumentAsync: vi.fn(),
}));
vi.mock('expo-file-system', () => ({
File: class TestFile {},
Paths: {
cache: 'file:///cache/',
},
}));
vi.mock('expo-haptics', () => ({
ImpactFeedbackStyle: {
Heavy: 'heavy',
Light: 'light',
Medium: 'medium',
},
impactAsync: vi.fn(),
}));
vi.mock('expo-image-picker', () => ({
PermissionStatus: {
DENIED: 'denied',
GRANTED: 'granted',
},
launchCameraAsync: vi.fn(),
launchImageLibraryAsync: vi.fn(),
requestCameraPermissionsAsync: vi.fn(),
requestMediaLibraryPermissionsAsync: vi.fn(),
}));
vi.mock('expo-linking', () => ({
canOpenURL: vi.fn(),
openURL: vi.fn(),
}));
vi.mock('expo-network', () => ({
getNetworkStateAsync: vi.fn(),
NetworkStateType: {
CELLULAR: 'CELLULAR',
ETHERNET: 'ETHERNET',
NONE: 'NONE',
WIFI: 'WIFI',
},
}));
vi.mock('expo-notifications', () => ({
AndroidImportance: {
DEFAULT: 'default',
},
getPermissionsAsync: vi.fn(),
IosAuthorizationStatus: {
PROVISIONAL: 3,
},
requestPermissionsAsync: vi.fn(),
scheduleNotificationAsync: vi.fn(),
setNotificationChannelAsync: vi.fn(),
setNotificationHandler: vi.fn(),
}));
vi.mock('expo-sharing', () => ({
isAvailableAsync: vi.fn(),
shareAsync: vi.fn(),
}));
vi.mock('react-native', () => ({
Appearance: {
getColorScheme: vi.fn(),
},
Platform: {
OS: 'android',
},
PushNotificationIOS: {
setApplicationIconBadgeNumber: vi.fn(),
},
}));
function request(method: HostBridgeMethod): HostBridgeRequest {
return {
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: `dispatch-${method}`,
method,
};
}
const IOS_UNDECLARED_HOST_BRIDGE_METHODS = HOST_BRIDGE_METHODS.filter(
(method) => !HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES.includes(method),
);
describe('mobile HostBridge dispatch', () => {
test.each(IOS_UNDECLARED_HOST_BRIDGE_METHODS)(
'%s 未被移动壳声明时明确返回 unsupported',
async (method) => {
resetMobileHostBridgeDispatchForTest();
const response = await dispatchMobileHostBridgeRequest(request(method));
expect(response.ok).toBe(false);
if (response.ok) {
throw new Error(`${method} unexpectedly succeeded`);
}
expect(response.error.code).toBe('unsupported_method');
expect(response.error.message).toContain(method);
},
);
});
@@ -51,6 +51,7 @@
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities``openHostShare``writeHostClipboardText``requestHostHapticsImpact``setHostAppTitle``exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。
- 2026-06-20 H5 原生能力门控收口:除 `host.getRuntime` 为了支持旧入口 URL 缺少 capability 时回读真实 runtime 可保留特殊判断外,H5 facade 中所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`,避免各能力复制门控规则;根级 `npm run check:native-shells` 会从共享 `HOST_BRIDGE_METHODS` 自动派生需门控的 request capability 清单,新增 method 时必须同步补齐 H5 facade 门控。
- 2026-06-20 移动壳未声明 method 覆盖:Expo 移动壳对未进入 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 的共享 request method 必须由测试从 `HOST_BRIDGE_METHODS` 自动派生覆盖,并在请求到达时返回明确 `unsupported_method`;平台差异能力如 Android 不声明的 `app.setBadgeCount` 保持独立 `unsupported_capability` 语义,不混入未声明 method 清单,移动壳配置检查必须反查 Android 角标请求失败测试仍存在。
- 2026-06-20 移动 dispatch 单测边界:`apps/mobile-shell/src/host-bridge/dispatch.test.ts` 直接从 `HOST_BRIDGE_METHODS``HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 派生未声明 method 清单,覆盖 `dispatchMobileHostBridgeRequest(...)` 返回 `unsupported_method`,避免只靠完整 `bridge.test.ts` 间接证明 `auth.requestLogin``payment.request` 等等待真实 SDK 的能力不会伪成功。
- 2026-06-20 移动壳能力清单单测边界:`apps/mobile-shell/src/host-bridge/capabilities.test.ts` 直接覆盖 Expo 移动壳 `MOBILE_HOST_CAPABILITIES` / `IOS_MOBILE_HOST_CAPABILITIES` 必须引用共享 `HOST_BRIDGE_EXPO_MOBILE_*` profileAndroid 只使用 base profile 且不声明 `app.setBadgeCount`,iOS 只额外声明真实角标能力,并且两端在真实 SDK / 渠道流程落地前不得声明 `auth.requestLogin``payment.request`。根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免移动壳 capability profile 本地复制或伪声明。
- 2026-06-20 壳文档能力清单反查:`npm run check:native-shells` 会同时反查移动壳 / 桌面壳主状态段落能力清单和后续完整能力清单;主状态段落按集合检查,允许按叙述需要调整顺序,但不得漏写或多写 capability,完整能力清单继续按共享 profile 顺序检查。
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。该回读请求的短超时由共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 声明,H5 facade 不得本地重声明。
+1
View File
@@ -259,6 +259,7 @@ const expectedMobileHostBridgeFiles = [
'capabilities.ts',
'clipboard.test.ts',
'clipboard.ts',
'dispatch.test.ts',
'dispatch.ts',
'filePayloads.test.ts',
'filePayloads.ts',