收口原生壳能力来源

新增共享 HostBridge capability profile,区分 Expo 移动通用、iOS 额外和 Tauri 桌面能力

移动壳能力清单改为引用共享 profile,入口 URL 类型支持 readonly 能力数组

两端配置检查和原生壳总门禁改为反查共享 profile,桌面 Rust 清单保持运行时镜像

补充 HostBridge profile 单测和 H5 readonly 能力合并类型兼容

同步 Expo/Tauri 壳方案、HostBridge 协议文档和项目决策记录
This commit is contained in:
2026-06-19 02:02:15 +08:00
parent a54210b0d1
commit 75cd4f1abc
12 changed files with 192 additions and 68 deletions
@@ -929,6 +929,10 @@ const sharedMethods = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_METHODS',
);
const sharedDesktopCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES',
);
const sharedHostBridgeProtocol = extractTsStringConst(
sharedContractSource,
'HOST_BRIDGE_PROTOCOL',
@@ -1154,6 +1158,12 @@ if (unknownDesktopCapabilities.length > 0) {
);
}
assertSameList(
desktopCapabilities,
sharedDesktopCapabilities,
'desktop shell HostBridge capability profile',
);
for (const capability of sdkBackedCapabilities) {
if (desktopCapabilities.includes(capability)) {
throw new Error(
+26 -8
View File
@@ -481,6 +481,14 @@ const sharedMethods = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_METHODS',
);
const sharedMobileBaseCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
);
const sharedMobileIosCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES',
);
const sharedHostBridgeProtocol = extractStringConstExport(
sharedContractSource,
'HOST_BRIDGE_PROTOCOL',
@@ -498,14 +506,8 @@ const sharedPublicWebUrl = extractStringConstExport(
'HOST_BRIDGE_PUBLIC_WEB_URL',
);
const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource);
const mobileCapabilities = extractStringArrayExport(
hostBridgeSource,
'MOBILE_HOST_CAPABILITIES',
);
const iosMobileCapabilities = extractStringArrayExport(
hostBridgeSource,
'IOS_MOBILE_HOST_CAPABILITIES',
);
const mobileCapabilities = sharedMobileBaseCapabilities;
const iosMobileCapabilities = sharedMobileIosCapabilities;
const mobileCapabilitySet = new Set(mobileCapabilities);
const iosMobileCapabilitySet = new Set(iosMobileCapabilities);
const sdkBackedCapabilities = ['auth.requestLogin', 'payment.request'];
@@ -553,6 +555,22 @@ for (const localBoundary of forbiddenLocalPayloadBoundaryDeclarations) {
}
}
for (const profileSource of [
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
'HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES',
]) {
if (!hostBridgeSource.includes(profileSource)) {
throw new Error(`mobile shell must use shared HostBridge profile ${profileSource}`);
}
}
if (
/export const MOBILE_HOST_CAPABILITIES[^=]*= \[/.test(hostBridgeSource) ||
/export const IOS_MOBILE_HOST_CAPABILITIES[^=]*= \[/.test(hostBridgeSource)
) {
throw new Error('mobile shell must not redeclare HostBridge capability profiles');
}
const unknownHandledMobileMethods = handledMobileMethods.filter(
(method) => !sharedMethods.includes(method),
);
@@ -1,37 +1,15 @@
import { Platform } from 'react-native';
import type { HostBridgeCapability } from '../../../../packages/shared/src/contracts/hostBridge';
import {
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
} from '../../../../packages/shared/src/contracts/hostBridge';
export const MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'navigation.canGoBack',
'app.reloadWebView',
'app.openExternalUrl',
'network.status',
'network.statusChanged',
'clipboard.writeText',
'clipboard.readText',
'file.exportText',
'file.importText',
'file.exportImage',
'file.importImage',
'file.captureImage',
'file.importAudio',
'file.exportAudio',
'haptics.impact',
'notification.showLocal',
];
export const MOBILE_HOST_CAPABILITIES =
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES;
export const IOS_MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [
...MOBILE_HOST_CAPABILITIES,
'app.setBadgeCount',
];
export const IOS_MOBILE_HOST_CAPABILITIES =
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES;
export function resolveMobileHostCapabilities(platform = Platform.OS) {
return platform === 'ios'
+1 -1
View File
@@ -9,7 +9,7 @@ import {
export type MobileShellUrlOptions = {
platform: Extract<NativeHostPlatform, 'ios' | 'android'>;
hostVersion: string;
capabilities: HostBridgeCapability[];
capabilities: readonly HostBridgeCapability[];
};
export const DEFAULT_MOBILE_SHELL_WEB_URL = HOST_BRIDGE_PUBLIC_WEB_URL;
@@ -2438,6 +2438,13 @@
- 影响范围:`apps/mobile-shell/scripts/check-config.mjs``apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run check:native-shells``npm run mobile-shell:typecheck``npm run desktop-shell:typecheck``npm run typecheck``npm run check:encoding``git diff --check`
## 2026-06-19 原生壳 capability profile 来源收口
- 背景:Expo 移动壳、Tauri 桌面壳和方案文档都需要维护真实 capability 子集;如果移动端源码、桌面 Rust 镜像和文档各自手写完整清单,后续新增能力时容易出现入口 URL、`host.getRuntime` 回包、文档和门禁漂移。
- 决策:`packages/shared/src/contracts/hostBridge.ts` 中的 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES``HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES``HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES` 是原生壳 capability profile 来源。Expo 移动壳只通过 `apps/mobile-shell/src/host-bridge/capabilities.ts` 引用共享 profile 并选择平台差异;Tauri 桌面壳 `capabilities.rs` 仍保留 Rust 运行时镜像,但 `apps/desktop-shell/scripts/check-config.mjs``npm run check:native-shells` 必须反查共享桌面 profile。新增 native capability 必须先进入共享白名单和对应平台 profile,再补真实壳实现、H5 fallback、测试和文档。
- 影响范围:`packages/shared/src/contracts/hostBridge.ts``apps/mobile-shell/src/host-bridge/capabilities.ts``apps/mobile-shell/scripts/check-config.mjs``apps/desktop-shell/scripts/check-config.mjs``scripts/check-native-shells.mjs`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run check:native-shells``npm run mobile-shell:typecheck``npm run desktop-shell:typecheck``npm run test -- packages/shared/src/contracts/hostBridge.test.ts``npm run check:encoding``git diff --check`
## 2026-06-18 原生壳本地生成物边界
- 背景:`npm run check:native-shells` 会生成 Expo `.expo/` 日志、Expo export smoke 临时目录、Tauri schema、Tauri 自动生成权限和 Rust `target/` 产物。这些文件是本机工具输出,不是生产源码;如果进入生产壳敏感词扫描或被误提交,会让门禁受工具版本、构建日志或自动生成格式影响。
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
# 宿主壳能力统一协议
更新时间:`2026-06-18`
更新时间:`2026-06-19`
## 背景
@@ -37,7 +37,7 @@ AI H5 sandbox
-> parent HostBridge adapter
```
桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol``webView``payment``shareGrid``subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js``payment.js``shareGrid.js``subscribeMessage.js``miniprogram/shell/webView.js``payment.js``shareGrid.js``subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 承接能力清单与 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接能力清单`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs``url.rs``navigation.rs``network.rs``lifecycle.rs``file_drop.rs``events.rs``deep_link.rs``tray.rs``webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。
桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol``webView``payment``shareGrid``subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js``payment.js``shareGrid.js``subscribeMessage.js``miniprogram/shell/webView.js``payment.js``shareGrid.js``subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs``url.rs``navigation.rs``network.rs``lifecycle.rs``file_drop.rs``events.rs``deep_link.rs``tray.rs``webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。
## 首批能力
@@ -72,7 +72,7 @@ AI H5 sandbox
2. `authService` 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。
3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。
4. 后续新增 `native_app` adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 `host.getRuntime` 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时长期隐藏真实可用能力。
5. 每次新增或调整 native capability 后,必须运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;排查单端问题时再单独运行 `npm run mobile-shell:typecheck``npm run mobile-shell:test``npm run mobile-shell:config``npm run mobile-shell:export``npm run desktop-shell:typecheck``npm run desktop-shell:test``npm run desktop-shell:build -- --no-bundle`
5. 每次新增或调整 native capability 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应 Expo / Tauri capability profile,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;排查单端问题时再单独运行 `npm run mobile-shell:typecheck``npm run mobile-shell:test``npm run mobile-shell:config``npm run mobile-shell:export``npm run desktop-shell:typecheck``npm run desktop-shell:test``npm run desktop-shell:build -- --no-bundle`
## 验收
@@ -81,7 +81,7 @@ AI H5 sandbox
- 小程序支付仍跳转 `/pages/wechat-pay/index` 并保留支付结果 hash 回灌确认。
- 小程序订阅授权仍跳转 `/pages/subscribe-message/index`,且返回不阻断生成主链路。
- 普通浏览器分享、H5 支付和 Native 二维码支付不受影响。
- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport 和已接入的 H5 直接调用链文件。
- 原生壳统一验收入口 `npm run check:native-shells` 通过,能力白名单、共享 capability profile、壳 runtime 回包、URL `hostCapabilities`、H5 fallback、三端桥接层结构、两端壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport 和已接入的 H5 直接调用链文件。
## 后续
@@ -1,9 +1,14 @@
import { describe, expect, test } from 'vitest';
import {
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_TAURI_COMMAND,
HOST_BRIDGE_CAPABILITIES,
isHostBridgeMethod,
isHostBridgeCapability,
normalizeHostBridgeBadgeCount,
@@ -90,6 +95,44 @@ describe('HostBridge shared contract helpers', () => {
expect(isHostBridgeCapability(null)).toBe(false);
});
test('原生壳 capability profile 来自共享白名单', () => {
const profiles = [
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
];
for (const profile of profiles) {
expect(profile.every((capability) =>
HOST_BRIDGE_CAPABILITIES.includes(capability),
)).toBe(true);
expect(profile).not.toContain('auth.requestLogin');
expect(profile).not.toContain('payment.request');
}
});
test('移动和桌面 profile 保留真实平台差异', () => {
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES).toEqual([
'app.setBadgeCount',
]);
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES).toEqual([
...HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
...HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
]);
expect(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES).toContain(
'file.captureImage',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain('app.setTitle');
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain(
'file.imageDropped',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
'file.captureImage',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
'haptics.impact',
);
});
test('归一化 HostBridge request id', () => {
expect(normalizeHostBridgeRequestId(' request-1 ')).toBe('request-1');
expect(normalizeHostBridgeRequestId('')).toBeNull();
+63 -1
View File
@@ -71,12 +71,74 @@ export function isHostBridgeCapability(
);
}
export const HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES: readonly HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'navigation.canGoBack',
'app.reloadWebView',
'app.openExternalUrl',
'network.status',
'network.statusChanged',
'clipboard.writeText',
'clipboard.readText',
'file.exportText',
'file.importText',
'file.exportImage',
'file.importImage',
'file.captureImage',
'file.importAudio',
'file.exportAudio',
'haptics.impact',
'notification.showLocal',
];
export const HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES: readonly HostBridgeCapability[] = [
'app.setBadgeCount',
];
export const HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES: readonly HostBridgeCapability[] = [
...HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
...HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
];
export const HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES: readonly HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'navigation.canGoBack',
'app.reloadWebView',
'app.openExternalUrl',
'app.setTitle',
'app.setBadgeCount',
'network.status',
'network.statusChanged',
'clipboard.writeText',
'clipboard.readText',
'file.exportText',
'file.importText',
'file.exportImage',
'file.importImage',
'file.importAudio',
'file.exportAudio',
'file.imageDropped',
'notification.showLocal',
];
export type HostBridgeRuntimeResult = {
shell: NativeHostShell;
platform: NativeHostPlatform;
hostVersion: string | null;
bridgeVersion: number;
capabilities: HostBridgeCapability[];
capabilities: readonly HostBridgeCapability[];
};
export type HostBridgeRequest<Payload = unknown> = {
+16 -12
View File
@@ -421,31 +421,35 @@ function assertNativeShellCapabilityPlan() {
'decision log document',
);
const mobileCapabilitySource = fs.readFileSync(
'apps/mobile-shell/src/host-bridge/capabilities.ts',
'utf8',
);
const desktopCapabilitySource = fs.readFileSync(
'apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs',
'utf8',
);
const sharedContractSource = fs.readFileSync(sharedHostBridgeContractPath, 'utf8');
const sharedMethods = extractTsStringArray(sharedContractSource, 'HOST_BRIDGE_METHODS');
const mobileCapabilities = extractTsStringArray(
mobileCapabilitySource,
'MOBILE_HOST_CAPABILITIES',
);
const sharedMethods = extractTsStringArray(
fs.readFileSync(sharedHostBridgeContractPath, 'utf8'),
'HOST_BRIDGE_METHODS',
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
);
const iosMobileCapabilities = extractTsStringArray(
mobileCapabilitySource,
'IOS_MOBILE_HOST_CAPABILITIES',
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES',
);
const iosExtraCapabilities = iosMobileCapabilities.filter(
(capability) => !mobileCapabilities.includes(capability),
);
const sharedDesktopCapabilities = extractTsStringArray(
sharedContractSource,
'HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES',
);
const desktopCapabilities = extractRustCapabilities(desktopCapabilitySource);
assertSameList(
desktopCapabilities,
sharedDesktopCapabilities,
'desktop shell runtime capability profile',
);
assertSameList(
extractDocumentMethodTable(planSource),
[
+1 -1
View File
@@ -162,7 +162,7 @@ function normalizeHostCapabilities(values: unknown): HostBridgeCapability[] {
}
function mergeHostCapabilities(
...capabilityLists: Array<HostBridgeCapability[] | null | undefined>
...capabilityLists: Array<readonly HostBridgeCapability[] | null | undefined>
) {
return Array.from(
new Set(capabilityLists.flatMap((capabilities) => capabilities ?? [])),
@@ -179,11 +179,13 @@ describe('nativeAppHostBridge', () => {
test('Tauri invoke 超时后的迟到回包不改变已拒绝结果', async () => {
vi.useFakeTimers();
let resolveInvoke:
| ((response: HostBridgeResponse<{ handled: boolean }>) => void)
| null = null;
let resolveInvoke: (
response: HostBridgeResponse<{ handled: boolean }>,
) => void = () => {
throw new Error('Tauri invoke promise was not created');
};
const invoke = vi.fn(
() =>
(_command: string, _args?: Record<string, unknown>) =>
new Promise<HostBridgeResponse<{ handled: boolean }>>((resolve) => {
resolveInvoke = resolve;
}),
@@ -206,7 +208,7 @@ describe('nativeAppHostBridge', () => {
const request = (invoke.mock.calls[0]?.[1] as { request: HostBridgeRequest })
.request;
resolveInvoke?.({
resolveInvoke({
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: request.id,