diff --git a/docs/dev-conventions.html b/docs/dev-conventions.html
new file mode 100644
index 0000000..1464f21
--- /dev/null
+++ b/docs/dev-conventions.html
@@ -0,0 +1,293 @@
+
+
+
+
+
+
Pangolin 开发规范 · 可测试性五支柱
+
「怎么写才好测」——开发规范是可测试性的前置条件 · 与测试框架咬合
+
+
+
核心立场:测试做不做得起来,根子在代码
有没有遵守可测试的结构约定。哪里没遵守,哪里就测不了。这份规范把决定「能不能自动化测」的约定收成
5 条支柱,每条回答三件事:
怎么写 → 所以测得了 → 由哪层守门。它与
测试框架架构说明 一一咬合:规范定义接缝,测试在接缝下刀。
+
+
+
+
五支柱速览
+
写法采用通用原则 + Pangolin 实例双层——原则可复制到任何多端项目,实例替换即可。
+
+
+
支柱 1 · 接缝即接口
+
所有跨进程 / 外部依赖(原生隧道、HTTP、子进程、存储、时钟、随机)藏在接口 / 可注入构造之后;业务逻辑里禁止直接 new 或调静态单例。分层单向依赖(token→实现→组件/页面),生成物 *.gen.dart 勿手改。
+
测试红利L2 集成/交互能换假替身,无需真依赖即可驱动真实路径。
+
验证钩子每个外部依赖都存在 Fake/Mock + 一条 flow 测试;依赖方向 lint(dart import / go internal 包边界)。
+
正 / 反例✅ VpnBridge 接口 → 可换 VpnBridgeMock;AuthApi(baseUrl, http.Client) 可注入。❌ NETunnelProviderManager 直接散在 UI / 业务里 new。
+
+
+
+
支柱 2 · 契约单源
+
每段跨边界数据(pangolin/vpn/stats EventChannel、HTTP JSON、gRPC UsageEntry proto、设计 token)有唯一定义源,两端从它生成或校验,禁止两端各写各的。
+
测试红利L0 契约快照能守住「字段形状」,接缝两端永不漂移。
+
验证钩子契约快照测试(字段 diff=0)+ codegen 漂移检查(colors_and_type.css→*.gen.dart、proto→桩,生成物与源一致)。
+
针对这条正是「mac 实时统计恒为 0」的根因解药——两端各写 stats、无单源、无守门。
+
+
+
+
支柱 3 · 纯逻辑与 IO 分离
+
计算 / 换算 / 状态机是纯函数,不混时间、网络、IO、随机;这些都当「可注入依赖」从外面传,IO 收进薄壳(functional core / imperative shell)。纯核心天然确定 → 不 flaky。
+
测试红利L1 单元无依赖、可重复、毫秒级;确定性是 E2E 不 flaky 的前提。
+
验证钩子覆盖率阈值;纯函数测试零 mock;时钟/随机可注入。
+
正例bytes→速率/时长 格式化、配额计算、时间 Go 端算好传 ?(不用 MySQL NOW())、golden 字体锁定。
+
+
+
+
支柱 4 · 错误是有类型的值
+
错误带类型 / 码、可断言;禁止吞异常、禁止只打日志不返回。失败路径和成功路径一样要可观测、可测。
+
测试红利测试能断言失败分支(401、超额、连接失败),而不是只测 happy path。
+
验证钩子错误路径用例;lint 禁空 catch {} / 裸吞;错误类型可枚举。
+
正例AuthApiException(statusCode, messageZh, messageEn)——带码、带双语、可断言。
+
+
+
+
支柱 5 · 可观测即可断言
+
关键状态 / 事件经稳定通道暴露(status / stats stream、结构化日志、/healthz、/v1/usage),让测试读数值断言,而非截屏猜。
+
测试红利L4 端到端能断言:bytes 增长、限流触发、延迟 >0、状态流转。
+
验证钩子每个关键状态有可程序读取的出口;E2E 冒烟据此断言。
+
正例VpnStatusStreamHandler / stats EventChannel / /v1/usage 数值出口。
+
+
+
支柱 ↔ 测试层 咬合矩阵
+
每条支柱直接解锁测试架构里的某些层。这张图就是规范与测试的咬合证明:
+
+
图 1 · 五支柱解锁五测试层(● 主要解锁 ○ 间接帮助)
+
+
+
读法:补齐支柱 2(契约单源)就点亮 L0;做好支柱 1 才有 L2 的假桥;支柱 5 是 L4 端到端能断言的唯一前提。规范不是道德要求,是测试能力的开关。
+
+
反例 → 真实 bug → 本可由哪条支柱防住
+
规范的说服力来自真实的坑。下表把项目里遇到/潜在的问题,回溯到被违反的支柱:
+
+| 现象 / 真实 bug | 根因(违反了什么) | 本可由哪条支柱防住 |
+
+
+ | mac 实时统计恒为 0 |
+ 原生侧产出、Dart 侧消费 stats,两端各写、无单源;触发条件一变即静默失效,无任何测试守门。 |
+ 支柱 2 契约单源 + 支柱 5 可观测 |
+
+
+ 延迟显示 stale / 拿不到 Android·Windows urltest |
+ 延迟值没有稳定出口、没有契约校验,靠各端自行解析、刷新时机不一致。 |
+ 支柱 5 可观测 + 支柱 2 契约单源 |
+
+
+ | 记账若用 MySQL 专属时间函数 |
+ 时间/方言混进 SQL,无法在 SQLite 真库测、不可移植。 |
+ 支柱 3 纯逻辑/IO 分离 时间 Go 端算、方言走 dialect |
+
+
+ 连接交互测不了 (通用风险) |
+ 连接逻辑直接依赖 NETunnelProviderManager / 真子进程,不连真隧道就无法驱动。 |
+ 支柱 1 接缝即接口 藏到 VpnBridge 后面 |
+
+
+ 失败路径不可测 / UI 卡死 (通用风险) |
+ 连接失败只打日志不返回、空 catch 吞异常,调用方拿不到可断言的错误。 |
+ 支柱 4 错误是有类型的值 |
+
+
+
+
+
执行机制 · 四道闸
+
规范只靠文档一定漂。执行 = 尽量把每条往机器自动守门推,推不动的用脚手架降低犯错成本 + 评审清单兜底。这不是一个动作,是从「写之前」卡到「push」的四道闸——越早拦,成本越低。
+
+
图 2 · 四道闸:从「写之前」预防到「push」硬闸,外加评审兜底
+
+
+
+- 闸 1 · 脚手架(预防):让对的写法成默认路径——新模块生成器自带「接口 + Fake 骨架」,token/proto 一律走 codegen。不主动绕,自然合规。
+- 闸 2 · lint/analyze(即时):
flutter analyze + go vet + 自定义规则(空 catch、import 方向、业务层禁直接 new 外部依赖),编辑器当场红线。
+- 闸 3 · pre-commit(本地把关):format + 快检 + 红线词扫描 + codegen 漂移检查(生成物与源不一致即拦,逼你别手改
*.gen.dart)。
+- 闸 4 · CI 硬闸(守门,失败挡 merge):全量测试 + 覆盖率阈值 + 契约快照 diff=0 + 可移植 SQL 扫描 + golden 像素闸 +(按需)E2E。不可绕过的最终防线。
+
+
诚实区分「能不能机器强制」——能强制的硬卡 CI,强制不了的靠脚手架 + 评审兜底:
+
+| 支柱 | 主执行手段 | 强制度 | 现状 |
+
+| 1 接缝即接口 | import 方向 lint + 脚手架 + 评审清单 | 半自动 | 待补 |
+| 2 契约单源 | 契约快照 diff=0 + codegen 漂移检查 | 强制 | 待补(OpenAPI 已有) |
+| 3 纯逻辑/IO 分离 | 覆盖率阈值 + 可移植 SQL 扫描 | 强制 | 部分已有 |
+| 4 错误是值 | lint 禁空 catch + 失败路径用例 | 半自动 | 待补 |
+| 5 可观测 | 关键状态出口评审 + L4 冒烟据出口断言 | 半自动 | 随 L4 |
+
+
+
这个仓库的落点:.gitea/workflows/ci.yml(shellcheck、OpenAPI 校验、红线词扫描、flutter analyze + test)= 闸 4 骨架已在。缺:契约快照、golden 进 CI、可移植 SQL 扫描、pre-commit hook、自定义 lint、PR checklist。
+
+
可复用:通用原则 vs 本项目实例
+
换到你别的多端项目时,五条原则不变,只替换右列实例:
+
+| 通用原则 | Pangolin 实例(换项目时替换) |
+
+| 接缝即接口 | VpnBridge / KernelProcess / TokenStore / AuthApi(http.Client) |
+| 契约单源 | stats EventChannel · /v1/* JSON · UsageEntry proto · colors_and_type.css |
+| 纯逻辑/IO 分离 | 速率/时长格式化 · 配额计算 · 时间 Go 端算 · dialect 方言层 |
+| 错误是值 | AuthApiException · 控制面错误码 |
+| 可观测 | status/stats stream · 结构化日志 · /v1/usage · /healthz |
+
+
+
+
本规范为「怎么写」的真相源;「怎么验证」见 测试框架架构说明。两份配套维护,新增接缝时同步更新两边。
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
index 392f71b..1128303 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -76,8 +76,23 @@
KillSwitch 设计与跨平台能力矩阵 HTML
断网保护 L0–L3 分级模型 + 各平台能力天花板 / 当前实现矩阵。KillSwitch 决策依据。
diff --git a/docs/test-architecture.html b/docs/test-architecture.html
new file mode 100644
index 0000000..a4b4bfc
--- /dev/null
+++ b/docs/test-architecture.html
@@ -0,0 +1,431 @@
+
+
+
+
+
+Pangolin 统一测试框架 · 架构说明
+
+
+
+
+
+
Pangolin 统一测试框架 · 架构说明
+
分层方法论 + 脚手架模板 + 单一编排入口 · 可自动化 / 可量化 / 可自定义
+
+
+
定性(方案 A):不存在一个能同时覆盖「界面渲染 / 交互驱动 / 后端记账 / 真链路端到端」的单一工具——这四者运行时根本不同。所以「一套测试框架」= 一套
分层方法论 + 一组
可复用脚手架模板(假桥、Provider 覆盖、golden+字体锁、Go 真库套、E2E compose 骨架)+ 一个
统一编排入口。跨多端项目的复用方式是
模板复制后按接缝适配,而非全项目 import 同一个包。
+
+
+
+
设计原则
+
+- 测试金字塔:底层用例多、快、稳、便宜;越往上越接近真实、越慢、越脆。绝大多数断言压在底部,顶部只留薄薄一层冒烟。
+- 契约接缝(contract seam)优先:系统由若干跨进程/跨语言的接缝拼成(原生→Dart、Dart→控制面、agent→控制面)。每段接缝先有契约快照守住「字段形状」,再分层在接缝两侧各自下刀。
+- 量化优先:每层都产出可比对的数字(覆盖率、像素不一致比例、数值容差、时延 ms、通过率),而非「人眼看着对」。
+- 单一编排:一个入口(
make test / test.sh)按 profile 跑对应层,输出一份合并报告。
+
+
前不久的「mac 实时统计恒为 0」就是典型反例:原生侧产出统计、Dart 侧消费统计,两端的 pangolin/vpn/stats 契约没有任何自动化测试守门,触发条件一变(隧道已连接才启动 app)就静默失效。L0 契约层防的正是这类 bug。
+
+
① 分层测试金字塔
+
+
图 1 · 五层金字塔:L0 契约为地基,L1→L4 自底向上
+
+
+
+
② 契约接缝数据流
+
统计/记账数据沿一条跨进程链路流动。每段接缝标注其契约,以及哪一层测试在此下刀——这张图就是「该测什么、在哪测」的地图。
+
+
图 2 · 契约接缝:从节点数据面一路回到客户端 UI
+
+
+
实时上下行走 ①(libbox→EventChannel→Dart);累计流量/周柱走 ④→③→DB→②(节点统计→agent→控制面入库→客户端拉取)。两条流共用同一张接缝图,测试各自归位。
+
+
③ 四类诉求 → 层 → 工具 → 量化指标
+
把你提出的四类测试诉求逐条落到层、工具、量化指标与现状:
+
+| 诉求 | 层 | 工具 | 量化指标 | 现状 |
+
+
+ ① 界面设计一致 每端一致 |
+ L3 |
+ Flutter golden(字体锁定,host 无关→天然每端一致);design-distill pixelmatch 对照 HTML 原型真相源 |
+ 像素不一致比例 vs 阈值 0.05;golden 通过/失败 |
+ 素材齐 · CI 没跑 |
+
+
+ ② 前端交互 连接 / 登陆 / 登出 |
+ L2 |
+ flutter_test 进程内驱动整树 + VpnBridgeMock + http MockClient + ProviderScope 覆盖 |
+ 流程通过/失败;覆盖率 lcov |
+ 素材齐 · 待组装 |
+
+
+ ③ 数值型 统计 / 实时速度 / 时延 |
+ L1 + L4 |
+ L1:格式化/换算纯逻辑单测;L4:连真节点拉 stats 断言 bytes 增长、延迟 >0 |
+ 实测 vs 期望容差;时延 ms;速率 B/s >0 |
+ L1 可补 L4 缺 |
+
+
+ ④ 后端网络 按租户流量 / 限流 |
+ L2 + L4 |
+ L2:SQLite 真库 + testcontainers MySQL(已覆盖并发/重放/跨日/配额);L4:全链路真流量计数 |
+ 记账数值断言;限流是否触发;覆盖率 go cover |
+ L2 强 L4 缺 |
+
+
+
+
+
④ 逐层详解
+
+
+
L0 · 契约 / 静态闸
+
守住接缝的「字段形状」
+
+- 测什么:跨进程契约的结构——
pangolin/vpn/stats EventChannel 字段、gRPC UsageEntry proto、/v1/me·/v1/usage JSON 形态;静态:analyze/vet、OpenAPI 结构、红线词。
+- 工具:契约快照测试(snapshot)+ 已有 OpenAPI 校验 +
ci/scan-redline.sh。
+- 量化:字段 diff = 0;扫描命中 = 0。
+- 现状/缺口:OpenAPI/红线已有 契约快照需补。这是最被低估、性价比最高的一层。
+- 可自定义:新增一段接缝 = 加一份快照基线。
+
+
+
+
+
L1 · 单元
+
纯逻辑,最快最稳
+
+- 测什么:数值换算(bytes→速率/时长格式化)、状态机(ConnectionController)、记账纯函数(GB 卡控、dp_uuid 解析)。
+- 工具:
flutter_test;go test + testify。
+- 量化:覆盖率(lcov /
go test -cover)。
+- 现状:已有(前端 controller、后端 usage 包多场景)。
+
+
+
+
+
L2 · 集成 / 交互
+
带假替身的真实路径
+
+- 前端交互:
pumpWidget(真 App 树) 驱动 登陆→重启续登→点连接→状态流转→登出,把原生隧道换 VpnBridgeMock、服务端换 MockClient,ProviderScope 覆盖注入。
+- 后端 DB:SQLite 内存真库 + testcontainers MySQL/Redis(已覆盖并发去重/重放/跨日分桶/配额)。
+- 后端 HTTP/gRPC:
httptest.Server(鉴权路径需补);gRPC bufconn + 自签 CA/CRL(已强)。
+- 量化:通过/失败 + 覆盖率 + 数值断言。
+- 现状:后端强 前端 flow 待组装 · HTTP 鉴权偏薄。
+
+
+
+
+
L3 · 视觉一致性
+
每端渲染一致 + 对照设计真相源
+
+- 测什么:mobile/tablet/desktop × light/dark × zh/en 的渲染一致;以及实现 vs HTML 原型的像素差。
+- 工具:Flutter golden(
flutter_test_config.dart 字体锁定→host 无关,所以「每端效果一致」天然成立);design-distill shoot-prototype.mjs + diff.mjs(pixelmatch)。
+- 量化:像素不一致比例 vs 阈值
0.05。
+- 现状:20+ golden 基准已有 CI 未接 · pixel 闸未自动化。
+- 可自定义:新增屏 = 加一张基线;阈值按屏可调。
+
+
+
+
+
L4 · 端到端冒烟
+
真链路:统计真不真 / 流量真不真
+
+- 测什么:起真控制面 + agent + sing-box + DB,发真流量,断言按租户/按设备 bytes 入库、限流触发、实时数值 >0。
+- 工具:docker-compose + 探针驱动脚本(见图 5)。
+- 量化:实测 vs 期望容差、限流是否触发、连上耗时、时延 ms。
+- 现状:完全缺——最大缺口。
local_test.sh 只构建启动、deploy.sh 只部署,均无连通性自检。
+- 边界(建议):先做 (a) 后端全链路(可全自动、进一台 Linux runner);(b) desktop 真连(需常驻 Mac)与 (c) iOS/Android 真机 作本地按需后续。
+
+
+
+
⑤ 编排与 CI 流水线
+
一个入口按 profile 分流:CI 每 push 只跑快而稳的 fast;视觉与 E2E 按需触发。
+
+
图 4 · 单一入口 → 三 profile 泳道
+
+
+
+
⑥ L4 端到端冒烟拓扑
+
+
图 5 · docker-compose 真链路 + 测试驱动器(推荐边界 a)
+
+
+
+
⑦ 量化指标总表
+
+| 维度 | 指标 | 工具 / 口径 |
+
+| 覆盖率 | 行/分支 % | flutter test --coverage + go test -coverprofile → 合并 lcov |
+| 视觉 | 不一致像素比例 vs 阈值 0.05 | pixelmatch(diff.mjs);golden 通过/失败 |
+| 数值精度 | 实测 vs 期望容差 | L4 断言 bytes/速率/累计落在容差带内 |
+| 时延 | 毫秒 ms | urltest 延迟 >0 且在合理区间 |
+| 稳定性 | 通过率 / flaky 率 | 多次重跑统计;flaky 用例隔离 |
+
+
+
+
⑧ 可复用与可自定义
+
跨项目模板(复制即用,按接缝微调):
+
+- 假桥模式(
VpnBridge 接口 + Mock)→ 任何「原生/外部依赖」都可照此切假。
+- ProviderScope/DI 覆盖注入 → 任何状态层都可注入假数据。
+- golden + 字体锁定(
flutter_test_config.dart)→ 视觉回归通用骨架。
+- Go 内存真库套(
run_sqlite_test.sh)+ testcontainers 集成套 → 任何 DB 项目通用。
+- E2E docker-compose 骨架 + 探针驱动器 → 换服务清单即复用。
+- 单一编排器 + profile 分流 + 合并报告 → 框架的「壳」,跨项目不变。
+
+
每项目按接缝定制:契约字段、API 路径、compose 服务清单、golden 屏清单、数值容差阈值。
+
+
⑨ 落地路线(建议)
+
+| 序 | 动作 | 性价比 | 起点 |
+
+| 1 | 补 L0 契约快照 + 把现有 golden 接进 CI | 最高(防回归) | 素材已有,组装即可 |
+| 2 | 组装 L2 前端 flow 测试(登陆/续登/连接/登出) | 高 | Mock/覆盖素材全齐 |
+| 3 | 建 L4 后端全链路冒烟 (a) | 中(覆盖"真不真") | 需新建 compose + 驱动器 |
+| 4 | 补 L1 数值单测 + L2 HTTP 鉴权测试 | 中 | 增量补齐 |
+| 5 | (b) desktop 真连 / (c) 真机——本地按需 | 低(贵·脆) | 需常驻 Mac / 真机 |
+
+
+
本文档仅为架构说明(设计真相源)。具体落地拆解(契约快照实现、前端 flow 用例、L4 冒烟脚本)走各自实现计划(writing-plans)。
+
+
+
+