Skip to content

第 15 章:测试、发布与可迁移的设计启示

最后一章不再添加新子系统,而是回看项目如何测试并发不变量、构建三平台产物、发布更新元数据,并总结哪些设计值得复用、哪些地方仍有演进空间。

一、测试重点是状态不变量,不只是 happy path

本地快照有约 365 个 Rust #[test]/#[tokio::test] 标记。大量测试集中在:

  • Draft 并发、commit/rollback;
  • RunState 组合与 derived capability;
  • Service version 分类;
  • owner loss 的收敛阈值;
  • core readiness generation;
  • proxy guard 的 cancel-and-drain; -配置顺序、权威字段和幂等;
  • Mixed Port fallback 与文件恢复;
  • ProxyView 的确定性和歧义解析。

这些测试共同特点是:验证“永远不能发生什么”。

二、把副作用顺序提取成可注入 transition

manager/lifecycle.rs 中多个 helper 接收闭包:

text
run_controlled_stop_transition(stop_guard, clear_proxy, stop_core)
run_core_start_transition(start_core, is_ready, apply_proxy)
run_service_config_replacement_transition(...)

测试注入记录事件的闭包,就能断言:

  • clear 一定在 stop 前;
  • start 不 ready 不能 apply proxy;
  • macOS Service 使用 stop guard;
  • 失败在哪一步会跳过哪些后续动作。

这比 mock 整个 Tauri/Service 更简单。值得迁移的技巧是:把“副作用做什么”留在依赖里,把“副作用顺序”提取成小 orchestration function。

三、FakeEnv 让状态机成为纯逻辑

RunStateEnv 的 FakeEnv 可预设:

  • elevated;
  • service ready/mismatch/unreachable;
  • trusted evidence true/false/error;
  • privileged operation success/failure;
  • 多次 probe 回复序列。

测试不仅检查返回值,还检查:probe 次数、PAC availability、published snapshot、privileged action 列表和 generation。

这是状态机测试的高质量标准:验证输出与外部交互,而不是访问内部私有字段拼实现细节。

四、前端测试覆盖哪些风险

前端 Vitest 和 Node tests 主要覆盖:

  • event teardown;
  • query/provider 状态;
  • delay 去重/缓存;
  • update hook;
  • Proxy render/empty model;
  • record selection;
  • URI/traffic 等纯工具;
  • 开发模式 Service 控制脚本。

相较后端,复杂 UI 组件的行为测试仍较少。大型 Profile 编辑器、设置组合和虚拟列表更依赖代码结构与人工 QA,这是未来可加强区域。

五、Lint 与静态约束

5.1 Rust

workspace Clippy 配置非常严格:

  • correctness/suspicious deny;
  • await holding lock deny;
  • cognitive complexity deny;
  • panic/unimplemented deny;
  • unwrap/expect warn;
  • large future、needless collect 等性能 lint。

严格 lint 对并发桌面应用尤其有价值:await_holding_lock 和 large future 不只是风格,会直接影响死锁与 runtime 开销。

5.2 Frontend

组合使用 TypeScript、ESLint、Biome、Knip、Vitest:

  • 类型检查 IPC payload 和组件接口;
  • ESLint/React Compiler 规则检查 Hook;
  • Biome 统一格式;
  • Knip 找未使用导出/依赖;
  • i18n 脚本生成 key 类型并检查未使用项。

六、CI 被拆成独立质量门

仓库有 14 个 workflow,主要分为:

类型Workflow
前端质量frontend-check
Rust 格式/静态rustfmt、lint-clippy、cross_check
依赖安全cargo-audit
正式发布release
滚动构建autobuild
指定平台测试包dev
更新元数据updater
资产维护/通知clean-old-assets、telegram-notify

按变更路径跳过无关检查,减少大型三平台项目的 CI 成本;正式 tag 发布仍执行完整构建。

七、发布流水线的核心阶段

正式 Release 大致是:

text
tag 与 package version 一致性

创建/更新 draft release

Windows/macOS/Linux x64 构建
  + Linux ARM 构建
  + Windows fixed WebView2 构建

artifact attestation

上传安装包/portable

生成 updater JSON 与签名

发布 Release、Winget、通知

package.jsonsrc-tauri/Cargo.toml 和 Tauri config 都携带版本,release scripts 需要同步它们。CI 首先检查 tag 与版本,避免产物名称正确但应用内版本错误。

八、为什么需要 stable、autobuild 和 deploytest

  • Stable:面向普通用户,tag 驱动,可靠性优先;
  • AutoBuild:定时滚动,验证最新主干;
  • DeployTest:手动选择平台,验证发布链而不污染稳定通道。

三种通道复用大部分构建步骤,但 tag/channel/update endpoint 分开。发布通道是产品协议的一部分,不只是 CI 环境变量。

九、供应链与运行时安全

项目的安全面包括:

  • Tauri updater public key 验签;
  • GitHub artifact attestation;
  • cargo audit;
  • GPL 依赖与 deny 配置;
  • controller 优先本地 socket/pipe;
  • loopback singleton token;
  • Service owner credential/session proof;
  • Script 执行限制;
  • 备份敏感字段 AES-GCM;
  • URL 和错误日志脱敏。

安全不是一个 security/ 目录,而是贯穿更新、IPC、配置、脚本和日志。

十、最值得复用的七个设计

10.1 候选配置是一等状态

Draft 把“还没生效”编码进类型和 API,避免全局读者误读。

10.2 状态机发布完整快照

RunState 的 event 不要求前端按序重放 delta,丢事件可重读恢复。

10.3 generation 识别异步结果属于哪一代

用于核心 ready、Service owner、proxy guard 和 Profile activation,解决经典 ABA 问题。

10.4 先取消,再排空

Proxy guard 的 final operation 不只 invalidate future work,还等待在途操作结束。

10.5 数据变换与副作用编排分离

Enhance 尽量纯,生命周期 transition 可注入,使复杂顺序可测试。

10.6 事件关闭竞态要二次读取

先读后订阅之间的 gap 通过 onSubscribed revalidate 关闭。

10.7 高频链路需要端到端背压

共享 socket、批量、结构共享、Worker、虚拟列表共同工作,不能只在 UI 最后一层优化。

十一、仍可继续演进的地方

11.1 Rust/TypeScript IPC schema 自动生成

当前 events 在 TS 侧集中,但 Rust 与 TS 仍靠人工同步。可以从 Rust serde 类型生成 TypeScript,或维护共享 schema,减少命令名和 payload 漂移。

11.2 缩小大型文件

core/service.rsenhance/mod.rsconfig/profiles.rs 和部分 React 编辑器超过千行。它们内部已出现明确子概念,可进一步按 protocol/install/owner-monitor 或 editor model/view 拆分。

11.3 Query cache 单一实现

SWR 外再维护 mirror Map 解决同步 updater,但形成双缓存一致性责任。可以考虑注入自定义 SWR provider 或建立明确 external store,减少手工同步点。

11.4 文档化配置所有权表

AuthoritativeFields 已把关键控制面写进代码,但新增 Mihomo 字段时容易忘记是否属于应用。将“字段所有者、热加载能力、重启要求”生成成表,可同时驱动 UI、flags 和测试。

11.5 更完整的端到端故障注入

单元测试很好地固定局部不变量;下一步可在可控 fake service/sidecar 上测试“写文件一半失败、owner 切换、旧 RPC 晚到”的整条 Saga。

十二、如何继续阅读源码

完成 15 章后,建议选一个真实动作做反向追踪:

路线 A:打开 TUN

text
Setting component → useVerge → patch_verge_config
→ feat::patch_verge → UpdateFlags → enhance/use_tun
→ CoreManager update/restart → RunState reconciliation
→ tray/event/cache

路线 B:切换 Profile

text
Profile item → patch_profiles_config
→ Draft IProfiles → enhance → validate
→ core replacement → selection restore
→ profile/proxy events → AppDataProvider

路线 C:打开连接页

text
ConnectionsPage → useConnectionData
→ shared Mihomo WebSocket → throttle/parse
→ structural sharing external store
→ virtualized table

当你能不看目录说出这三条链路,基本就掌握了项目真正的架构。

本章源码索引

  • Cargo.toml:workspace lint 与 profile
  • .clippy.tomlrustfmt.tomldeny.toml:Rust 质量约束
  • eslint.config.tsbiome.jsonknip.jsonvitest.config.mts:前端质量约束
  • src-tauri/src/core/runstate/*:状态机密集测试示例
  • src-tauri/src/core/manager/lifecycle.rs:可注入 transition 测试
  • crates/clash-verge-draft/:并发与事务测试
  • .github/workflows/release.yml:正式发布
  • .github/workflows/autobuild.yml:滚动构建
  • scripts/release-version.mjspublish-version.mjsupdater.mjs:版本与更新元数据

原创文档 CC BY-SA 4.0 · 站点代码 MIT