第 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 接收闭包:
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 大致是:
tag 与 package version 一致性
↓
创建/更新 draft release
↓
Windows/macOS/Linux x64 构建
+ Linux ARM 构建
+ Windows fixed WebView2 构建
↓
artifact attestation
↓
上传安装包/portable
↓
生成 updater JSON 与签名
↓
发布 Release、Winget、通知package.json、src-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.rs、enhance/mod.rs、config/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
Setting component → useVerge → patch_verge_config
→ feat::patch_verge → UpdateFlags → enhance/use_tun
→ CoreManager update/restart → RunState reconciliation
→ tray/event/cache路线 B:切换 Profile
Profile item → patch_profiles_config
→ Draft IProfiles → enhance → validate
→ core replacement → selection restore
→ profile/proxy events → AppDataProvider路线 C:打开连接页
ConnectionsPage → useConnectionData
→ shared Mihomo WebSocket → throttle/parse
→ structural sharing external store
→ virtualized table当你能不看目录说出这三条链路,基本就掌握了项目真正的架构。
本章源码索引
Cargo.toml:workspace lint 与 profile.clippy.toml、rustfmt.toml、deny.toml:Rust 质量约束eslint.config.ts、biome.json、knip.json、vitest.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.mjs、publish-version.mjs、updater.mjs:版本与更新元数据