From d967b9cea0e8cb244bd6b8f3de7f03c9129a8f17 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 10:22:42 +0800 Subject: [PATCH] =?UTF-8?q?=E9=94=81=E5=AE=9A=E7=A7=BB=E5=8A=A8=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E4=BA=AB=E4=B8=8D=E5=8F=AF=E7=94=A8=E8=BE=B9?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动文件导出测试覆盖文本图片音频分享不可用时不写缓存 移动壳配置检查反查文件导出不可用不伪成功断言 共享决策日志补充移动文件导出分享不可用边界 --- apps/mobile-shell/scripts/check-config.mjs | 3 ++ .../src/host-bridge/files.test.ts | 33 +++++++++++++++---- .../shared-memory/decision-log.md | 1 + 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 482ba4c92..91718f6b1 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2042,6 +2042,9 @@ for (const snippet of [ 'describe(\'mobile HostBridge file actions\'', 'exportTextFile({', 'exportImageFile({', + 'rejects every export before cache writes when system sharing is unavailable', + 'const exportCases = [', + 'expect(writtenFiles).toHaveLength(0)', 'importTextFile()', 'importDocumentFile()', 'importAudioFile()', diff --git a/apps/mobile-shell/src/host-bridge/files.test.ts b/apps/mobile-shell/src/host-bridge/files.test.ts index 5ac14912a..dd80582af 100644 --- a/apps/mobile-shell/src/host-bridge/files.test.ts +++ b/apps/mobile-shell/src/host-bridge/files.test.ts @@ -141,18 +141,37 @@ describe('mobile HostBridge file actions', () => { }); }); - test('rejects exports when system sharing is unavailable', async () => { + test('rejects every export before cache writes when system sharing is unavailable', async () => { shareAvailableMock.mockResolvedValue(false); - await expect( - exportImageFile({ + const exportCases = [ + () => + exportTextFile({ + content: '泥巴AI', + fileName: '创作记录', + mimeType: 'text/plain', + }), + () => + exportImageFile({ base64Data: PNG_BASE64, fileName: '分享卡', mimeType: 'image/png', - }), - ).rejects.toMatchObject({ - code: 'unsupported_capability', - }); + }), + () => + exportAudioFile({ + base64Data: MP3_BASE64, + fileName: '声浪', + mimeType: 'audio/mpeg', + }), + ]; + + for (const exportFile of exportCases) { + await expect(exportFile()).rejects.toMatchObject({ + code: 'unsupported_capability', + }); + } + + expect(writtenFiles).toHaveLength(0); expect(shareAsyncMock).not.toHaveBeenCalled(); }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0f991493a..a9abdf111 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -41,6 +41,7 @@ - 2026-06-19 桌面壳文件桥接执行边界:Tauri `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.importAudio` / `file.exportAudio` 的系统文件对话框过滤器、用户取消语义、路径转换、异步读写编排和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/files.rs`;MIME、大小、base64、文件名清洗、本地副本读写和 HostBridge payload 组装统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs`;`dispatch.rs` 只负责按 method 委托 `export_desktop_host_bridge_*_file(...)` / `import_desktop_host_bridge_*_file(...)`。桌面壳配置检查会拒绝分发层直接调用 `.dialog()`、`blocking_save_file` / `blocking_pick_file`、文件 payload helper 或落盘 helper,避免文件访问边界重新散落。 - 2026-06-20 移动壳文件桥接载荷边界:Expo `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.captureImage` / `file.importAudio` / `file.exportAudio` 的 DocumentPicker、ImagePicker、File、Sharing 系统交互、用户取消语义、缓存读写编排和 HostBridge 响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`;MIME、大小、base64、文件名清洗、图片 / 音频 bytes 匹配和 picker 结果到 HostBridge payload 的组装统一收口在 `apps/mobile-shell/src/host-bridge/filePayloads.ts`。移动壳单端配置检查和根级 `npm run check:native-shells` 会把 `filePayloads.ts` 纳入结构清单与 HostBridge 源码扫描,避免文件载荷边界重新散落到分发层或 shell 层。 - 2026-06-20 移动文件动作单测边界:`apps/mobile-shell/src/host-bridge/files.test.ts` 直接覆盖 Expo 文件动作 helper 的文本导出、系统分享不可用、文本 / 文档 / 音频导入、用户取消、图片相册导入、相机权限拒绝和音频二进制导出;单端配置检查会反查这些动作测试存在,根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免系统文件交互只靠完整 HostBridge bridge 流程间接覆盖。 +- 2026-06-20 移动文件导出分享不可用边界:Expo `file.exportText` / `file.exportImage` / `file.exportAudio` 在 `Sharing.isAvailableAsync()` 返回 false 时必须直接返回 `unsupported_capability`,不得写入 Expo cache,也不得调用 `Sharing.shareAsync` 或伪造 saved 成功;`apps/mobile-shell/src/host-bridge/files.test.ts` 用三类导出参数化覆盖该顺序,配置检查反查“不写缓存”断言。 - 2026-06-20 移动文件载荷单测边界:`apps/mobile-shell/src/host-bridge/filePayloads.test.ts` 直接覆盖移动壳文件载荷 helper 的 base64、UTF-8 byte、MIME / 扩展名归一、图片 / 音频 bytes 匹配、导出文件名补扩展、导入大小门禁和 ImagePicker payload 转换;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,防止后续只靠完整 HostBridge bridge 流程间接覆盖文件安全边界。 - 2026-06-20 移动本地通知单测边界:`apps/mobile-shell/src/host-bridge/notifications.test.ts` 直接覆盖 Expo `notification.showLocal` 的已授权 / iOS provisional 权限复用、alert-only 权限请求、权限拒绝失败、iOS 即时调度、Android 固定 channel、共享 payload 归一和结构化 `delivered_to_system` 成功响应;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免移动通知边界只靠完整 HostBridge bridge 流程间接覆盖。 - 2026-06-20 桌面能力清单单测边界:Tauri `capabilities.rs` 必须用 Rust 单测同时覆盖桌面 runtime capability 清单顺序、无重复、真实桌面能力完整包含,并显式排除 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact` 等未接入能力;桌面单端配置检查会反查该测试边界,避免只靠方案文档或共享 profile 发现桌面壳能力伪声明。