Files
pangolin/docs/feature-test-coverage-checklist.html
T

345 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pangolin 功能 × 测试覆盖清单</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:1040px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 28px}
h2{font-size:21px;margin:46px 0 6px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16.5px;margin:26px 0 6px;color:var(--accent)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
a{color:var(--accent2);text-decoration:none}
a:hover{text-decoration:underline}
ul{margin:8px 0;padding-left:22px}
li{margin:5px 0}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
.small{color:var(--fg2);font-size:13px}
.note{color:var(--fg2);font-size:13px;border-left:3px solid var(--border);padding-left:12px;margin:10px 0}
.domain{color:var(--accent2);font-size:13px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;margin:30px 0 2px}
table{width:100%;border-collapse:collapse;margin:10px 0 18px;font-size:13px}
th,td{border:1px solid var(--border);padding:8px 10px;text-align:left;vertical-align:top}
th{background:var(--panel2);color:var(--fg);font-weight:600}
td{color:var(--fg2)}
td b,td strong{color:var(--fg)}
td.feat{color:var(--fg);font-weight:600;width:15%}
.tag{display:inline-block;font-size:10.5px;font-weight:700;padding:1px 7px;border-radius:999px;white-space:nowrap}
.t-ok{background:rgba(94,194,122,.16);color:var(--ok)}
.t-warn{background:rgba(224,184,79,.16);color:var(--warn)}
.t-bad{background:rgba(224,106,106,.16);color:var(--bad)}
.t-man{background:rgba(168,175,189,.16);color:var(--fg2)}
.kbd{font-family:var(--mono);font-size:.84em;color:#f0d9c4}
.legend{display:flex;flex-wrap:wrap;gap:14px;margin:14px 0 4px;font-size:13px}
.legend span{display:flex;align-items:center;gap:6px}
.ck{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin:12px 0}
.ck li{margin:7px 0;list-style:none}
.ck ul{padding-left:4px}
.ck .box{color:var(--accent);font-family:var(--mono);margin-right:8px}
.pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 9px;border-radius:999px;margin-right:6px;background:rgba(95,176,201,.16);color:var(--accent2)}
.crit{color:#f0d9c4;font-weight:600}
</style>
</head>
<body>
<div class="wrap">
<h1>Pangolin 功能 × 测试覆盖清单</h1>
<p class="sub">逐功能盘点:现有能力 → 已覆盖测试(怎么覆盖)→ 欠缺 → 只能人工。作为「验证五个终端是否正确」的对照工作表。</p>
<div class="lead">
<strong>怎么用:</strong>本清单分两部分——<b>A 共享逻辑 + 后端(控制面 + 数据面)</b>,跑一次自动化套件即对全平台生效;<b>B 平台隧道层</b>,五端各自不同,须逐端按底部验收清单打勾。每行的「覆盖」标签直接告诉你这条是<b>已自动化</b><b>部分/契约级</b><b>零覆盖</b> 还是<b>本质上只能人工</b>
<div class="small" style="margin-top:8px">↔ 配套:<a href="test-architecture.html">统一测试框架·架构说明</a>(分层方法论 + ⑩ 已知缺口) · <a href="dev-conventions.html">开发规范·可测试性五支柱</a> · <a href="vpn-test-plan.md">VPN 测试计划</a>(站点矩阵)</div>
</div>
<div class="legend">
<span><span class="tag t-ok">自动</span> CI 里有断言守门,回归即红</span>
<span><span class="tag t-warn">部分</span> 只到契约/单元,未覆盖端到端或边界</span>
<span><span class="tag t-bad"></span> 当前无任何测试</span>
<span><span class="tag t-man">人工</span> 本质上只能人工/真设备验证</span>
</div>
<p class="note">「覆盖」列给的是<b>当前状态</b>,不是目标;标 <span class="tag t-bad"></span>/<span class="tag t-warn">部分</span> 的「欠缺」列写清下一步补什么。标 <span class="tag t-man">人工</span> 的不是缺陷,是真设备/真出网/真链路这类自动化跑不动、必须人测的部分——集中收在 <a href="#manual">§A.4 / §B 验收清单</a></p>
<!-- ============================ PART A ============================ -->
<h2>A · 共享逻辑 + 后端(控制面 + 数据面)</h2>
<p>这部分代码<b>四端共用</b>Flutter 业务逻辑 / Go 控制面 / 数据面渲染),自动化跑一次即对五个终端同时生效。验证策略:<b>能自动化的全压到 CI,逐端不重复测</b></p>
<!-- A.1 控制面 -->
<h3 id="a1">A.1 控制面(Go 控制面 HTTP API + gRPC<code>server/internal/*</code></h3>
<p class="domain">账号 / 鉴权</p>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">注册 / 登录 / 刷新 <span class="tag t-ok">自动</span></td>
<td>邮箱注册、密码登录、refresh token 轮换、登出;<code>/v1/auth/{register,login,refresh,code}</code></td>
<td><code>auth/service_test · handler_test · token_test · integration_test</code>:真库(SQLite)跑完整 register→login→refresh 链;token 签发/校验/过期纯函数单测;<code>password_test</code> 哈希;<code>helpers_test</code></td>
<td>真邮件投递(SMTP)只 mock;账号枚举/暴力破解的真实速率压测仅 <code>ratelimit_test</code> 逻辑级</td></tr>
<tr><td class="feat">邮箱验证码 <span class="tag t-ok">自动</span></td>
<td>发码、校验、节流;<code>emailcheck</code></td>
<td><code>auth/emailcheck_test · ratelimit_test</code>:码生成/校验/节流窗口逻辑断言</td>
<td>真 SMTP 通道、到达率、垃圾箱判定 → <span class="tag t-man">人工</span></td></tr>
<tr><td class="feat">两步验证 TOTP <span class="tag t-ok">自动</span></td>
<td>TOTP 绑定/校验、备份码</td>
<td><code>totp/totp_test</code>(算法)+ <code>auth/totp_user_test</code>(绑定/校验流程)</td>
<td>真 Authenticator app 互操作 → <span class="tag t-man">人工</span>(一次性)</td></tr>
<tr><td class="feat">管理后台 <span class="tag t-ok">自动</span></td>
<td>admin 登录/会话、IP 白名单、配置、加密</td>
<td><code>admin/{auth,session,mw_ipallow,config,crypto,handlers,services_real}_test</code>:会话签发、IP allowlist 中间件、handler 行为</td>
<td>后台前端页面交互 → <span class="tag t-man">人工</span></td></tr>
</table>
<p class="domain">业务 / 计费</p>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">兑换码 / 套餐 <span class="tag t-ok">自动</span></td>
<td>码生成、兑换、套餐授予;<code>/v1/redeem · /v1/plans</code></td>
<td><code>codes/{generator,service}_test</code>:码生成唯一性、兑换幂等、过期/已用拒绝</td>
<td></td></tr>
<tr><td class="feat">支付 webhook <span class="tag t-warn">部分</span></td>
<td>第三方支付回调 → 充值/开通</td>
<td><code>codes/webhook_test</code>:回调签名校验 + 入账逻辑(mock 上游)</td>
<td>真支付渠道端到端(下单→回调→开通)仅 mock;真渠道沙箱 → <span class="tag t-man">人工</span></td></tr>
<tr><td class="feat">激励解锁配额 <span class="tag t-ok">自动</span></td>
<td>看广告解锁额度;<code>/v1/ads/unlock</code></td>
<td><code>usage/ads_test</code>:解锁额度计算/上限</td>
<td>真广告 SDK 回调 → <span class="tag t-man">人工</span></td></tr>
<tr><td class="feat">设备管理 <span class="tag t-ok">自动</span></td>
<td>设备注册/列举/解绑、上限;<code>/v1/me/devices</code></td>
<td><code>devices/service_test · devices_integration_test</code>:真库注册→列举→解绑;<b>注册回填 last_seen</b>(修过的真 bug</td>
<td></td></tr>
</table>
<p class="domain">节点 / 调度 / 供给</p>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">节点注册表 + 连接 <span class="tag t-ok">自动</span></td>
<td>列节点、连接/断开、ping<code>/v1/nodes · /connect · /disconnect</code></td>
<td><code>nodes/{grpc,hub,lifecycle}_test</code>:gRPC hub 收发、节点生命周期状态机</td>
<td>真节点延迟/可用性探测准确度 → 见数据面 §A.2</td></tr>
<tr><td class="feat">Agent mTLS 接入 <span class="tag t-ok">自动</span></td>
<td>CA 签发、CRL 吊销、节点身份、bootstrap token</td>
<td><code>mtls/{ca,crl,identity,bootstrap}_test</code> + <code>nodectl/bootstrap_token_test</code>:证书链签发/校验/吊销</td>
<td>真 mTLS 握手在 §A.2 e2e 覆盖一段</td></tr>
<tr><td class="feat">自动调度 / 故障替换 <span class="tag t-ok">自动</span></td>
<td>探测引擎、熔断、容量、自动替换编排、探针接入</td>
<td><code>scheduler/detect/engine_test · orchestrate/{breaker,capacity,config,replacer}_test · probe/{ingest,prober_agent}_test · wiring_{lifecycle,provision}_test</code>:健康判定→熔断→替换决策全链逻辑</td>
<td>真云厂商触发的端到端替换 → <span class="tag t-man">人工</span>(贵/慢)</td></tr>
<tr><td class="feat">云供给 provision <span class="tag t-warn">部分</span></td>
<td>cloud-init 渲染、provider 注册表、节点替换</td>
<td><code>provision/{cloudinit,replace,service}_test · providers/registry_test</code>fakes 注入)</td>
<td>真厂商 API 开机/销毁 → <span class="tag t-man">人工</span>(按需,烧钱)</td></tr>
<tr><td class="feat">告警 <span class="tag t-ok">自动</span></td>
<td>Telegram 通知、runbook</td>
<td><code>alert/{notifier,runbook}_test</code>:触发条件 + 去抖逻辑</td>
<td>真 Telegram 投递 → <span class="tag t-man">人工</span></td></tr>
</table>
<p class="domain">持久层 / 可移植性</p>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">双数据库 SQLite/MySQL <span class="tag t-ok">自动</span></td>
<td>裸 SQL + 方言层;迁移分两套;upsert/行锁中性记法</td>
<td><code>db/dialect_test · sqlite_smoke_test</code><code>store/{sqlite_stores,sqlite_migrate,sqlite_per_device,mysql,mysql_integration}_test</code>SQLite 实库 + MySQL testcontainers 真库跑同一套断言;<code>ci/scan-portable-sql.sh</code> 扫禁用 MySQL 专属构造</td>
<td>真 512MB VPS 上的并发/锁竞争压测 → <span class="tag t-man">人工</span></td></tr>
</table>
<!-- A.2 数据面 -->
<h3 id="a2">A.2 数据面(sing-box 配置渲染 + per-user 记账,<code>httpapi/clientconfig · agentd/*</code></h3>
<p>「连上了能不能真出网、记账准不准」的源头逻辑。<b>渲染/解析有测试,真链路靠 e2e 一段 + 人工兜底。</b></p>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">客户端配置渲染 <span class="tag t-ok">自动</span></td>
<td><code>BuildClientConfig</code> 服务端渲染原样下发:REALITY 出站、TUN 入站(strict_route 杀开关)、<span class="crit">DNS 劫持首条规则</span>、国内分流</td>
<td><code>httpapi/clientconfig_test</code>:断言 REALITY 公钥/short-id/端口、TUN auto_route/strict_route、<b>hijack-dns 规则排在 LAN 前</b>、split_cn 开关</td>
<td>渲染对 ≠ 内核吃得下:真 sing-box 加载该配置并连通 → §B 人工</td></tr>
<tr><td class="feat">节点配置渲染(agent 侧)<span class="tag t-ok">自动</span></td>
<td>agent 渲染 sing-box 服务端配置 + 凭证增删/轮换/吊销</td>
<td><code>agentd/singbox_test</code>Upsert/Revoke/Rotate/ApplyConfig 状态机;<code>derive_test</code> 数据口凭证派生;<code>hy2cert_test</code> 证书;<code>command_test</code></td>
<td><code>systemctl restart sing-box</code> 后端口真监听 → <span class="tag t-man">人工</span></td></tr>
<tr><td class="feat">per-user 流量采集 <span class="tag t-warn">部分</span></td>
<td><code>V2RayUsageSource</code>agent 读 sing-box v2ray_api StatsService per-user 计数器 → 聚合 → ReportUsage</td>
<td><b>新增</b> <code>agentd/usage_v2ray_test</code>:表驱动 <code>parseUserStat</code> + loopback 假 StatsService 跑真 <code>Collect()</code>,验聚合/方向不串/全0丢弃/<code>reset=true</code> 窗口语义</td>
<td>假 StatsService ≠ 真 sing-box:真出网流量经真 v2ray_api 的计数准确度 → <span class="tag t-man">人工</span></td></tr>
<tr><td class="feat">记账全链 e2e <span class="tag t-warn">部分</span></td>
<td>enroll → ReportUsage → 统计入库 → API 读出</td>
<td><code>server/test/e2e/smoke_test</code>:进程级 gRPC 全链路(agent enroll→上报→控制面统计真入库真读出),HTTP 段 + miniredis</td>
<td><code>/v1/usage/devices</code> 分设备断言(stats-overhaul 合并后补回);真节点真流量不在 e2e</td></tr>
<tr><td class="feat">REALITY 数据口连通 <span class="tag t-man">人工</span></td>
<td>客户端经 REALITY 连节点 443 真出网</td>
<td>—(握手是真协议真节点,自动化跑不动)</td>
<td>真握手成功 + 出网 → §B 每端验收清单第 1–2 步</td></tr>
</table>
<!-- A.3 客户端共享逻辑 -->
<h3 id="a3">A.3 客户端共享逻辑(Flutter <code>client/lib/*</code>,四端共用 Dart</h3>
<table>
<tr><th>功能</th><th>现有能力</th><th>已覆盖 + 怎么覆盖</th><th>欠缺 / 只能人工</th></tr>
<tr><td class="feat">API 客户端 + 契约 <span class="tag t-ok">自动</span></td>
<td><code>ApiClient</code> 取数、错误映射;<code>/v1/me · usage · usage/devices</code></td>
<td><code>unit/api_client_test</code><code>contract/{api_contract,stats_contract}_test</code> 冻结 wire 字段形状;Go 侧 <code>httpapi/contract_test · usage/contract_test · pb/agentv1/contract_test</code> 两侧对齐</td>
<td></td></tr>
<tr><td class="feat">登录 / 续登 / 登出流程 <span class="tag t-ok">自动</span></td>
<td><code>AuthNotifier</code> 状态机 + token 存储</td>
<td><code>unit/flow_auth_test</code>:真控制器 + MockClient 注入,跑登录→重启续登→登出</td>
<td><code>flutter_secure_storage</code> keychain 行为 → <span class="tag t-man">人工</span>(一次性)</td></tr>
<tr><td class="feat">连接状态机 <span class="tag t-ok">自动</span></td>
<td><code>connectionProvider</code> connecting→connected→errorping 探测</td>
<td><code>unit/{connection_controller,flow_connect}_test</code>:真控制器 + 假桥,验状态流转;<code>connect_passthrough_test</code></td>
<td>真隧道回调时序(连上才启动 app 等触发条件)→ §B</td></tr>
<tr><td class="feat">统计计算 + 上屏 <span class="tag t-ok">自动</span></td>
<td>月 GB/时长聚合、周柱、分设备归因;<code>stats_page</code></td>
<td><code>unit/{device_usage,format}_test</code> 解析/格式化;<b>新增</b> <code>widget/stats_page_test</code>:真 wire 形态喂 StatsPage 断言指标卡/周柱/分设备<b>数值真上屏</b></td>
<td>—(解析对 + 上屏对都已守)</td></tr>
<tr><td class="feat">配额 / 节点列表 <span class="tag t-ok">自动</span></td>
<td><code>quotaProvider · nodesProvider</code></td>
<td><code>unit/{quota_controller,nodes_provider}_test</code></td>
<td></td></tr>
<tr><td class="feat">UI 视觉一致性 <span class="tag t-ok">自动</span></td>
<td>各页面/组件像素还原(亮/暗 × 中/英 × 手机/平板/桌面)</td>
<td><code>golden/{components,auth_redesign,desktop_pages,tablet_pages}_test</code> Linux 权威基线;<code>widget/{cards,connect_button}_test</code><code>responsive/form_factor_test</code><code>fonts_test</code> 字体锁</td>
<td>真机不同 DPI/字号/深色模式实机观感 → <span class="tag t-man">人工</span>(抽查)</td></tr>
<tr><td class="feat">桥接口契约 <span class="tag t-warn">部分</span></td>
<td><code>VpnBridge</code> 抽象 + 桌面子进程实现 + mock</td>
<td><code>bridge/{vpn_bridge_mock,kernel_process,desktop_vpn_bridge_m4m5}_test</code>:内核进程查找/启停、mock 桥行为</td>
<td>原生 MethodChannel 两侧真实编解码 → §B(各端原生层)</td></tr>
<tr><td class="feat">交互组件(本轮)<span class="tag t-warn">部分</span></td>
<td>切换节点确认框(连接态防误触,节点行 + 智能选择卡同款);节点页搜索框配色(<code>PangolinFieldBox</code> 修双色缝);<code>showPangolinToast</code>(主显示区内、自适应宽、上限 80%、超长多行居中)</td>
<td><b>Windows 人工已验 2026-06-29</b>:确认框两处弹出 + 防误触正常;搜索框配色一致无缝;toast 常规文案显示正常(位置/明暗/动画)。<code>node_connect_confirm_test</code> 守确认框逻辑;toast 走真相源规格 <code>design/CONTRACT.md §2</code></td>
<td>toast <b>超长文本多行居中 + ≤80% 上限</b>边界<b>未验</b> <span class="tag t-bad">待测</span>;其余四端(mac/Android/iOS/iPad)同源未逐端抽查 → <span class="tag t-man">人工</span></td></tr>
</table>
<p class="domain">设计契约 · 五端 UI 同源</p>
<p>五端跑<b>同一个 Flutter 工程</b><code>client/lib</code> 共享 Dart),UI 不按平台分叉——颜色/样式/组件<b>不是「约定一致」,是物理上同一份代码</b>。下表是「五端同源」各维度的<b>单源 + 守门</b>状态。两处关键盲区:<span class="crit">① token 有漂移闸,logo/app-icon 资产没有;② 「生成物不漂移」有闸,但「screen 是否真用真相源」(依从性) 零闸、且所有闸无反向 case 自检</span></p>
<table>
<tr><th>维度</th><th>单源(真相源 → 生成/消费)</th><th>守门机制</th><th>欠缺 / 风险</th></tr>
<tr><td class="feat">颜色/间距/圆角/字体 <span class="tag t-ok">自动</span></td>
<td><code>design/colors_and_type.css</code>clay/sand 色板)→ <code>gen_flutter_tokens.mjs</code><code>pangolin_tokens.gen.dart</code>(勿手改)→ <code>pangolin_theme.dart</code> 语义层;业务禁硬编码 hex</td>
<td><b>CI <code>codegen-drift</code></b><code>ci/check-codegen-drift.sh</code>):重生成与提交版不一致即红,防「改 CSS 没重生成 / 手改生成物」</td>
<td>—(单源 + 自动闸,最规范的一块)</td></tr>
<tr><td class="feat">组件 <span class="tag t-ok">自动</span></td>
<td><code>client/lib/widgets/</code> 唯一实现;<code>design/</code> 禁放 Dart 副本(会漂移);规格在 <code>design/CONTRACT.md</code> + <code>design/preview/</code></td>
<td><b>golden 测试</b>:组件 + 各页面 × 明/暗 × 中/英 × 手机/平板/桌面,像素回归即红</td>
<td></td></tr>
<tr><td class="feat">图标库 <span class="tag t-ok">自动</span></td>
<td><code>lucide_icons</code>(细线条)全端同一套字体图标</td>
<td>随 golden 像素守</td>
<td></td></tr>
<tr><td class="feat">品牌 logo(应用内)<span class="tag t-warn">部分</span></td>
<td><code>design/assets/*.svg</code>mark / wordmark / app-icon)→ <b>拷贝一份到</b> <code>client/assets/</code><code>flutter_svg</code> 加载,四端同一份矢量</td>
<td><b>无自动闸</b>——<code>design↔client</code> 双拷贝靠手动同步(当前 4 个 SVG 已 diff 确认一致)</td>
<td><code>design/assets</code> 那份忘同步 <code>client/assets</code>CI 静默放过 → <span class="tag t-warn">待补 drift 闸</span></td></tr>
<tr><td class="feat">app 启动图标 <span class="tag t-warn">部分</span></td>
<td><code>assets/app-icon.svg</code><code>app-icon-ios-1024.png</code><code>flutter_launcher_icons</code> 一键铺五端全尺寸(iOS appiconset / Android mipmap / macOS / Windows .ico</td>
<td><b>无 drift 闸</b>——生成物(各端 PNG/ico,如 iOS 22 张)入库,靠手动 <code>dart run flutter_launcher_icons</code></td>
<td><code>app-icon.svg</code> 忘重跑生成,五端图标与源图分叉,CI 静默放过 → <span class="tag t-warn">待补 drift 闸</span></td></tr>
<tr><td class="feat">设计契约文档 <span class="tag t-man">人工</span></td>
<td><code>design/CONTRACT.md</code>design-distill 蒸馏):token 映射 / 组件映射 / 逐屏像素规格 / 逐屏验收清单(明×暗×中×英)</td>
<td>人读契约;像素侧由 golden 兜底</td>
<td>「还原对不对」的逐屏验收是人工抽查(截图 diff 可半自动)</td></tr>
<tr><td class="feat">screen 依从性<br>(源自真相源)<span class="tag t-bad"></span></td>
<td>要求 screen/widget 的颜色/字号/间距<b>必须源自</b> <code>PangolinColors</code>/<code>PangolinText</code>/<code>PangolinSpacing</code>,不得硬编码 hex/数值或绕过 <code>widgets/</code> 自拼组件</td>
<td><b>无任何检测</b><code>codegen-drift</code> 只守「生成物=CSS 源」<b>不守消费端是否用它</b><code>analysis_options.yaml</code> 无禁硬编码规则;<code>design/_adherence.oxlintrc.json</code> 能抓 raw hex/px 但<b>只对 JSX 原型、warn 级、未接 CI</b></td>
<td>实测 <code>lib/screens</code> 已有 <b>62+ 处</b>硬编码 <code>fontSize:</code>/<code>EdgeInsets</code> 数值无人拦(如 <code>account_page.dart:187</code> <code>fontSize: 16</code>)→ <span class="tag t-warn">待补依从性扫描闸(第①档:正则扫 raw 字面量,仿 <code>scan-portable-sql.sh</code></span></td></tr>
<tr><td class="feat">闸的反向 case 自检 <span class="tag t-bad"></span></td>
<td>每个闸应有「注入违规 → 断言闸变红」的负向自检,证明闸<b>非永真</b>(怎么改都绿 = 形同虚设)</td>
<td><b>无任何反向 case</b>(全仓 grep 零结果)——含 <code>codegen-drift</code> 在内的所有闸都没验证过「真能拦住违规」</td>
<td><span class="tag t-warn">待补 fixture 自检</span>(备一行违规样本,CI 跑「扫它必须非零退出」;新依从性闸与 codegen-drift 都该配)</td></tr>
</table>
<p class="note"><b>关键区别:</b><code>codegen-drift</code>(生成物不漂移)<b></b> 依从性(消费端真用真相源)。前者保证<b>真相源本身</b>没被改歪,后者保证 <b>screen 真的去用</b>了真相源——当前<b>只有前者有闸</b>,后者零守门、且所有闸都缺反向 case 自检。规划见 <code>planUI 真相源依从性收口</code>(本轮仅登记,补闸后续做)。</p>
<p class="note"><b>待补(性价比高):</b>仿 <code>ci/check-codegen-drift.sh</code><b>logo SVG 双拷贝</b><b>app-icon 生成物</b> 各加一个 drift 闸——重新生成/比对 <code>design↔client</code>,不一致即红,把「图标 logo 五端同源」从<b>靠纪律</b>升成<b>靠闸</b>。这是 token 已有、资产尚缺的一行。</p>
<!-- A.4 manual consolidated -->
<h3 id="manual">A.4 后端「只能人工」汇总</h3>
<div class="ck">
<ul>
<li><span class="box"></span> 真 SMTP / Telegram / 支付渠道 / 广告 SDK 的真实投递与回调(外部第三方,仅 mock 到逻辑边界)</li>
<li><span class="box"></span> 真云厂商 provision 开机→销毁→替换端到端(按需手测,烧钱)</li>
<li><span class="box"></span> 真 sing-box 加载渲染配置后端口真监听、真 v2ray_api 计数准确(渲染/解析已自动,运行时人测)</li>
<li><span class="box"></span> 512MB VPS 真并发/锁竞争压测</li>
<li><span class="box"></span> 后台前端页面交互(admin UI</li>
</ul>
</div>
<!-- ============================ PART B ============================ -->
<h2>B · 平台隧道层(五端逐一)</h2>
<p>真正逐端不同的只有<b>原生隧道实现</b>——这是测试架构 ⑩ 标注的<b>零自动化盲区</b>。三种策略:</p>
<ul>
<li><span class="pill">策略①</span><b>内嵌 libbox + 系统 VPN 扩展</b>iOS / iPad<code>NEPacketTunnelProvider</code> + <code>VpnManager.swift</code>)、Android<code>libbox.aar</code> + <code>PangolinVpnService.kt</code> VpnService)、macOS<code>PacketTunnel</code> 系统扩展,<b>当前默认关</b> <code>kUseNativeVpnMacOS=false</code></li>
<li><span class="pill">策略②</span><b>sing-box 子进程 + TUN</b>macOS(默认,<code>DesktopVpnBridge</code> + <code>kernel_process.dart</code>)、Windows<code>sing-box.exe</code> + <code>wintun.dll</code>)、Linux</li>
<li><span class="pill">桥接</span><b>原生↔Dart</b>MethodChannel/EventChannel<code>VpnEventBus.kt</code> / <code>VpnChannel.swift</code> / <code>StatsClient.swift</code>)传状态与统计——「mac 统计恒为 0」就出在这条没契约守门</li>
</ul>
<h3>各端隧道状态矩阵</h3>
<table>
<tr><th>终端</th><th>隧道实现</th><th>已覆盖</th><th>欠缺 / 验证重点</th></tr>
<tr><td class="feat">Android</td>
<td><code>PangolinVpnService.kt</code>(VpnService) + libbox.aar<code>DefaultNetworkMonitor.kt</code> 网络监听;<code>VpnEventBus.kt</code> 事件桥</td>
<td><span class="tag t-bad"></span> 原生层无自动化测试</td>
<td>VpnService 授权弹窗、TUN fd 建立、libbox 启动、断网重连、统计回传桥;真机连通 + 出网</td></tr>
<tr><td class="feat">iOS</td>
<td><code>VpnManager.swift</code>(NEPacketTunnelProvider) + 内嵌 libbox<code>StatsClient.swift</code> 统计</td>
<td><span class="tag t-bad"></span> 原生层无自动化测试</td>
<td>NE 内存 ≤50MB 闸、NE profile 安装授权、libbox 后台队列启动、TestFlight 分发;真机连通 + 出网</td></tr>
<tr><td class="feat">iPad</td>
<td>同 iOS 二进制;额外横屏侧栏布局</td>
<td><span class="tag t-warn">部分</span> 布局走 golden(<code>tablet_pages</code>),隧道同 iOS <span class="tag t-bad"></span></td>
<td>同 iOS + 横屏/分屏多任务下隧道与 UI;真机连通 + 出网</td></tr>
<tr><td class="feat">macOS</td>
<td><b>默认</b>子进程(<code>DesktopVpnBridge</code>+sing-box)<b>可选</b> PacketTunnel 系统扩展(站外 Developer ID + 公证)</td>
<td><span class="tag t-warn">部分</span> 子进程查找/启停有 <code>kernel_process_test · desktop_vpn_bridge_m4m5_test</code>sysext realize <span class="tag t-bad"></span></td>
<td>sysext 能否被 sysextd realize(见<a href="macos-sysext-realize-troubleshooting.html">踩坑复盘</a>)、CFBundleVersion 递增、公证/staple、三方死锁规避;真机连通 + 出网</td></tr>
<tr><td class="feat">Windows</td>
<td><code>sing-box.exe</code> 子进程 + <code>wintun.dll</code> TUN<code>kernel_process.dart</code> 管理</td>
<td><span class="tag t-warn">部分</span> 进程管理逻辑同 <code>kernel_process_test</code>(跨平台共用);wintun/打包 <span class="tag t-bad"></span></td>
<td>wintun.dll 随 exe 落位、TUN 适配器创建、UAC 提权、安装包;真机连通 + 出网</td></tr>
</table>
<h3 id="acceptance">每端真连通验收清单(同一张表,逐端打勾)</h3>
<p>第 17 步对五端是<b>同一份判据</b>,差异只在第 8 步平台专项。用<b>客观信号</b>代替「看着像连上了」。当前节点:<code>107.172.55.251</code>REALITY 443)。</p>
<div class="ck">
<ul>
<li><span class="box"></span> <b>1 流量真走节点</b> · 连前/连后各查出口 IP<code class="kbd">curl ifconfig.me</code> / ip.sb)。必须从本地 IP 变成节点 IP——<span class="crit">没变 = 隧道没真接管</span>,最易自欺的一步。</li>
<li><span class="box"></span> <b>2 DNS 劫持 + 路由对</b> · 浏览器开 youtube / google 能通 → 证明 <code>hijack-dns</code> 首条规则在设备上真生效(缺它「连上也打不开网站」)。</li>
<li><span class="box"></span> <b>3 国内分流不绕道</b> · 开 bilibili 等国内站走直连不进隧道(<code>split_cn</code>)。</li>
<li><span class="box"></span> <b>4 统计记账对</b> · 跑一段已知流量,对客户端统计页 GB/时长 vs 节点端 v2ray 计数器(咬合 §A.2,验 stats-overhaul 是否真对的天然 E2E)。</li>
<li><span class="box"></span> <b>5 断网保护 KillSwitch</b> · 隧道中途断开,确认无明文泄漏(分级见 <a href="killswitch-design.html">KillSwitch 设计</a>TUN <code>strict_route</code> 已渲染)。</li>
<li><span class="box"></span> <b>6 切节点</b> · 切换后出口 IP 跟着变、不掉线、不卡 connecting。</li>
<li><span class="box"></span> <b>7 重连 / 网络切换</b> · Wi-Fi↔蜂窝切换、息屏/休眠恢复后隧道自愈。</li>
<li><span class="box"></span> <b>8 平台专项</b>
<ul style="margin-top:4px">
<li>iOS/iPadNE 内存 ≤50MBInstruments)· profile 授权弹窗 · 后台保活</li>
<li>macOS(sysext)sysextd realize 成功 · 已公证 · CFBundleVersion 已递增</li>
<li>macOS(子进程)/Windows:内核子进程提权(sudo/UAC)· 退出清理 TUN</li>
<li>AndroidVpnService 授权弹窗 · 厂商省电杀后台白名单</li>
<li>Windowswintun.dll 落位 · TUN 适配器创建 · 安装包</li>
</ul>
</li>
</ul>
</div>
<p class="note"><b>半自动化建议:</b>第 1–3 步(出口 IP / 站点可达 / 分流判定)可写成一个连通探针脚本自动跑,把客观判据固化下来,只留第 4–8 步手测——这是把 §B 盲区往自动化推进的最小一步。</p>
<h3>下一步「功能更完善」可补的自动化(从盲区往里推)</h3>
<ul>
<li><b>原生↔Dart 统计契约</b>:给 <code>pangolin/vpn/stats</code> MethodChannel 两侧加契约测试(防「mac 统计恒为 0」复发)——这是 §B 里<b>唯一能自动化</b>的一块,性价比最高。</li>
<li><b>连通探针脚本</b>:固化验收清单第 1–3 步(出口 IP 变化 / 墙外站可达 / 国内站直连),CI 之外按需跑真节点。</li>
<li><b>记账对账</b>stats-overhaul 合并后,补 e2e <code>/v1/usage/devices</code> 分设备断言 + 客户端↔节点计数对账口径。</li>
<li><b>真 sing-box 烟测</b>(重,可选):起真 sing-box 吃渲染配置,验端口监听 + v2ray_api 真计数,替换当前假 StatsService 的一段。</li>
<li><b>资产 drift 闸</b>(见 §A.3 设计契约):仿 <code>ci/check-codegen-drift.sh</code>,给 <b>logo SVG 双拷贝(design↔client</b><b>app-icon 生成物</b> 各加比对闸,把「图标 logo 五端同源」从靠纪律升成靠闸——token 已有、资产尚缺。</li>
<li><b>Flutter 依从性扫描闸</b>(见 §A.3):仿 <code>ci/scan-portable-sql.sh</code>,正则扫 <code>lib/screens</code>+<code>lib/widgets</code> 的 raw 颜色/字号/间距字面量(第①档),命中即红,守「screen 真用真相源」——需先清存量 62+ 处或白名单冻结增量守门。</li>
<li><b>闸反向 case 自检</b>(见 §A.3):给依从性闸 + <code>codegen-drift</code> 各配一个违规 fixture,CI 跑「扫它必须非零退出」,证明闸非永真——当前所有闸都缺这一类自检。</li>
</ul>
<p class="small" style="margin-top:40px">维护:新功能上线时同步更新本表对应行的「覆盖」标签与「欠缺」列;与 <a href="test-architecture.html">test-architecture.html ⑩ 已知缺口</a> 互为索引——本表是「逐功能」视角,那里是「分层方法论 + 盲区跟踪」视角。</p>
</div>
</body>
</html>