29ddd5b453
- docs/test-architecture.html:分层测试金字塔(L0契约→L4 E2E) + 契约接缝 数据流 + 四类诉求映射 + CI 流水线 + E2E 拓扑,5 张内联 SVG/表格图 - docs/dev-conventions.html:可测试性五支柱(接缝即接口/契约单源/纯逻辑分离/ 错误是值/可观测) + 支柱↔测试层咬合矩阵 + 反例对照表 + 执行机制四道闸 - docs/index.html:登记两份文档到知识库/调研,双向互链 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
294 lines
22 KiB
HTML
294 lines
22 KiB
HTML
<!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:920px;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:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
h3{font-size:16.5px;margin:24px 0 8px;color:var(--accent)}
|
||
p{margin:10px 0}
|
||
code{font-family:var(--mono);font-size:.88em;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}
|
||
.fig{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px;margin:18px 0;overflow-x:auto}
|
||
.fig .cap{color:var(--fg2);font-size:12.5px;margin:0 0 10px;font-weight:600;letter-spacing:.02em}
|
||
.fig svg{display:block;max-width:100%;height:auto}
|
||
.pillar{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin:14px 0}
|
||
.pillar .badge{display:inline-block;font-size:11px;font-weight:700;padding:2px 9px;border-radius:999px;margin-bottom:8px;background:rgba(224,136,79,.16);color:var(--accent)}
|
||
.pillar .rule{font-size:15px;margin:6px 0 12px;padding:10px 12px;background:var(--panel2);border-radius:8px;border-left:3px solid var(--accent)}
|
||
.kv{margin:6px 0}
|
||
.kv .k{display:inline-block;min-width:84px;color:var(--fg2);font-size:12.5px;font-weight:700;vertical-align:top}
|
||
.kv .v{display:inline-block;width:calc(100% - 92px);font-size:13.5px;vertical-align:top}
|
||
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:13.5px}
|
||
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)}
|
||
.tag{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;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)}
|
||
.ex-ok{color:var(--ok);font-weight:700}
|
||
.ex-bad{color:var(--bad);font-weight:700}
|
||
.kbd{font-family:var(--mono);font-size:.86em;color:#f0d9c4}
|
||
.xref{display:inline-block;margin-top:6px;font-size:13px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
|
||
<h1>Pangolin 开发规范 · 可测试性五支柱</h1>
|
||
<p class="sub">「怎么写才好测」——开发规范是可测试性的前置条件 · 与测试框架咬合</p>
|
||
|
||
<div class="lead">
|
||
<strong>核心立场:</strong>测试做不做得起来,根子在代码<b>有没有遵守可测试的结构约定</b>。哪里没遵守,哪里就测不了。这份规范把决定「能不能自动化测」的约定收成 <b>5 条支柱</b>,每条回答三件事:<b>怎么写 → 所以测得了 → 由哪层守门</b>。它与 <a href="test-architecture.html">测试框架架构说明</a> 一一咬合:规范定义接缝,测试在接缝下刀。
|
||
<div class="xref">↔ 配套文档:<a href="test-architecture.html">统一测试框架 · 架构说明(L0–L4 分层)</a></div>
|
||
</div>
|
||
|
||
<h2>五支柱速览</h2>
|
||
<p>写法采用<b>通用原则 + Pangolin 实例</b>双层——原则可复制到任何多端项目,实例替换即可。</p>
|
||
|
||
<div class="pillar">
|
||
<span class="badge">支柱 1 · 接缝即接口</span>
|
||
<div class="rule">所有跨进程 / 外部依赖(原生隧道、HTTP、子进程、存储、<b>时钟、随机</b>)藏在<b>接口 / 可注入构造</b>之后;业务逻辑里禁止直接 <code>new</code> 或调静态单例。分层<b>单向依赖</b>(token→实现→组件/页面),生成物 <code>*.gen.dart</code> 勿手改。</div>
|
||
<div class="kv"><span class="k">测试红利</span><span class="v">L2 集成/交互能换假替身,无需真依赖即可驱动真实路径。</span></div>
|
||
<div class="kv"><span class="k">验证钩子</span><span class="v">每个外部依赖都存在 Fake/Mock + 一条 flow 测试;依赖方向 lint(dart import / go internal 包边界)。</span></div>
|
||
<div class="kv"><span class="k">正 / 反例</span><span class="v"><span class="ex-ok">✅</span> <code>VpnBridge</code> 接口 → 可换 <code>VpnBridgeMock</code>;<code>AuthApi(baseUrl, http.Client)</code> 可注入。<span class="ex-bad">❌</span> <code>NETunnelProviderManager</code> 直接散在 UI / 业务里 new。</span></div>
|
||
</div>
|
||
|
||
<div class="pillar">
|
||
<span class="badge">支柱 2 · 契约单源</span>
|
||
<div class="rule">每段跨边界数据(<code>pangolin/vpn/stats</code> EventChannel、HTTP JSON、gRPC <code>UsageEntry</code> proto、设计 token)有<b>唯一定义源</b>,两端从它<b>生成或校验</b>,禁止两端各写各的。</div>
|
||
<div class="kv"><span class="k">测试红利</span><span class="v">L0 契约快照能守住「字段形状」,接缝两端永不漂移。</span></div>
|
||
<div class="kv"><span class="k">验证钩子</span><span class="v">契约快照测试(字段 diff=0)+ codegen 漂移检查(<code>colors_and_type.css</code>→<code>*.gen.dart</code>、proto→桩,生成物与源一致)。</span></div>
|
||
<div class="kv"><span class="k">针对</span><span class="v">这条正是「mac 实时统计恒为 0」的根因解药——两端各写 stats、无单源、无守门。</span></div>
|
||
</div>
|
||
|
||
<div class="pillar">
|
||
<span class="badge">支柱 3 · 纯逻辑与 IO 分离</span>
|
||
<div class="rule">计算 / 换算 / 状态机是<b>纯函数</b>,不混时间、网络、IO、随机;这些都当「可注入依赖」从外面传,IO 收进薄壳(functional core / imperative shell)。<b>纯核心天然确定 → 不 flaky。</b></div>
|
||
<div class="kv"><span class="k">测试红利</span><span class="v">L1 单元无依赖、可重复、毫秒级;确定性是 E2E 不 flaky 的前提。</span></div>
|
||
<div class="kv"><span class="k">验证钩子</span><span class="v">覆盖率阈值;纯函数测试<b>零 mock</b>;时钟/随机可注入。</span></div>
|
||
<div class="kv"><span class="k">正例</span><span class="v"><code>bytes→速率/时长</code> 格式化、配额计算、<b>时间 Go 端算好传 <code>?</code></b>(不用 MySQL <code>NOW()</code>)、golden <b>字体锁定</b>。</span></div>
|
||
</div>
|
||
|
||
<div class="pillar">
|
||
<span class="badge">支柱 4 · 错误是有类型的值</span>
|
||
<div class="rule">错误带<b>类型 / 码</b>、可断言;<b>禁止吞异常、禁止只打日志不返回</b>。失败路径和成功路径一样要可观测、可测。</div>
|
||
<div class="kv"><span class="k">测试红利</span><span class="v">测试能断言失败分支(401、超额、连接失败),而不是只测 happy path。</span></div>
|
||
<div class="kv"><span class="k">验证钩子</span><span class="v">错误路径用例;lint 禁空 <code>catch {}</code> / 裸吞;错误类型可枚举。</span></div>
|
||
<div class="kv"><span class="k">正例</span><span class="v"><code>AuthApiException(statusCode, messageZh, messageEn)</code>——带码、带双语、可断言。</span></div>
|
||
</div>
|
||
|
||
<div class="pillar">
|
||
<span class="badge">支柱 5 · 可观测即可断言</span>
|
||
<div class="rule">关键状态 / 事件经<b>稳定通道</b>暴露(status / stats stream、结构化日志、<code>/healthz</code>、<code>/v1/usage</code>),让测试<b>读数值断言,而非截屏猜</b>。</div>
|
||
<div class="kv"><span class="k">测试红利</span><span class="v">L4 端到端能断言:bytes 增长、限流触发、延迟 >0、状态流转。</span></div>
|
||
<div class="kv"><span class="k">验证钩子</span><span class="v">每个关键状态有可程序读取的出口;E2E 冒烟据此断言。</span></div>
|
||
<div class="kv"><span class="k">正例</span><span class="v"><code>VpnStatusStreamHandler</code> / <code>stats</code> EventChannel / <code>/v1/usage</code> 数值出口。</span></div>
|
||
</div>
|
||
|
||
<h2>支柱 ↔ 测试层 咬合矩阵</h2>
|
||
<p>每条支柱直接<b>解锁</b>测试架构里的某些层。这张图就是规范与测试的咬合证明:</p>
|
||
<div class="fig">
|
||
<p class="cap">图 1 · 五支柱解锁五测试层(● 主要解锁 ○ 间接帮助)</p>
|
||
<svg viewBox="0 0 760 380" role="img" aria-label="支柱与测试层咬合矩阵">
|
||
<!-- column headers -->
|
||
<g text-anchor="middle" font-size="12" font-weight="700" fill="var(--fg)">
|
||
<text x="345" y="34">L0</text><text x="345" y="50" fill="var(--accent2)" font-size="11">契约</text>
|
||
<text x="440" y="34">L1</text><text x="440" y="50" fill="var(--ok)" font-size="11">单元</text>
|
||
<text x="535" y="34">L2</text><text x="535" y="50" fill="var(--accent2)" font-size="11">集成·交互</text>
|
||
<text x="630" y="34">L3</text><text x="630" y="50" fill="var(--accent)" font-size="11">视觉</text>
|
||
<text x="710" y="34">L4</text><text x="710" y="50" fill="var(--bad)" font-size="11">E2E</text>
|
||
</g>
|
||
<!-- grid -->
|
||
<g stroke="var(--border)" stroke-width="1">
|
||
<line x1="300" y1="62" x2="755" y2="62"/>
|
||
<line x1="300" y1="122" x2="755" y2="122"/>
|
||
<line x1="300" y1="177" x2="755" y2="177"/>
|
||
<line x1="300" y1="232" x2="755" y2="232"/>
|
||
<line x1="300" y1="287" x2="755" y2="287"/>
|
||
<line x1="300" y1="342" x2="755" y2="342"/>
|
||
<line x1="300" y1="62" x2="300" y2="342"/>
|
||
<line x1="392" y1="62" x2="392" y2="342" stroke-dasharray="2 3"/>
|
||
<line x1="487" y1="62" x2="487" y2="342" stroke-dasharray="2 3"/>
|
||
<line x1="582" y1="62" x2="582" y2="342" stroke-dasharray="2 3"/>
|
||
<line x1="675" y1="62" x2="675" y2="342" stroke-dasharray="2 3"/>
|
||
<line x1="755" y1="62" x2="755" y2="342"/>
|
||
</g>
|
||
<!-- row labels -->
|
||
<g font-size="12.5" fill="var(--fg)" font-weight="600">
|
||
<text x="14" y="96">支柱 1 · 接缝即接口</text>
|
||
<text x="14" y="151">支柱 2 · 契约单源</text>
|
||
<text x="14" y="206">支柱 3 · 纯逻辑/IO 分离</text>
|
||
<text x="14" y="261">支柱 4 · 错误是值</text>
|
||
<text x="14" y="316">支柱 5 · 可观测</text>
|
||
</g>
|
||
<!-- dots: ● solid (strong), ○ open (weak) -->
|
||
<g>
|
||
<!-- 支柱1 (y92): L1○ L2● L4○ -->
|
||
<circle cx="440" cy="92" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<circle cx="535" cy="92" r="8" fill="var(--accent2)"/>
|
||
<circle cx="710" cy="92" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<!-- 支柱2 (y147): L0● L3○ -->
|
||
<circle cx="345" cy="147" r="8" fill="var(--accent2)"/>
|
||
<circle cx="630" cy="147" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<!-- 支柱3 (y202): L1● L2○ L3○ -->
|
||
<circle cx="440" cy="202" r="8" fill="var(--accent2)"/>
|
||
<circle cx="535" cy="202" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<circle cx="630" cy="202" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<!-- 支柱4 (y257): L1● L2● L4○ -->
|
||
<circle cx="440" cy="257" r="8" fill="var(--accent2)"/>
|
||
<circle cx="535" cy="257" r="8" fill="var(--accent2)"/>
|
||
<circle cx="710" cy="257" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<!-- 支柱5 (y312): L2○ L4● -->
|
||
<circle cx="535" cy="312" r="7" fill="none" stroke="var(--accent2)" stroke-width="2"/>
|
||
<circle cx="710" cy="312" r="8" fill="var(--accent2)"/>
|
||
</g>
|
||
<!-- legend -->
|
||
<g font-size="11.5" fill="var(--fg2)">
|
||
<circle cx="312" cy="366" r="7" fill="var(--accent2)"/><text x="326" y="370">主要解锁</text>
|
||
<circle cx="430" cy="366" r="6" fill="none" stroke="var(--accent2)" stroke-width="2"/><text x="444" y="370">间接帮助</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<p class="note">读法:补齐<b>支柱 2</b>(契约单源)就点亮 <b>L0</b>;做好<b>支柱 1</b> 才有 <b>L2</b> 的假桥;<b>支柱 5</b> 是 <b>L4</b> 端到端能断言的唯一前提。规范不是道德要求,是测试能力的开关。</p>
|
||
|
||
<h2>反例 → 真实 bug → 本可由哪条支柱防住</h2>
|
||
<p>规范的说服力来自真实的坑。下表把项目里遇到/潜在的问题,回溯到被违反的支柱:</p>
|
||
<table>
|
||
<thead><tr><th style="width:26%">现象 / 真实 bug</th><th style="width:38%">根因(违反了什么)</th><th style="width:36%">本可由哪条支柱防住</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><b>mac 实时统计恒为 0</b></td>
|
||
<td>原生侧产出、Dart 侧消费 <code>stats</code>,两端各写、无单源;触发条件一变即静默失效,无任何测试守门。</td>
|
||
<td><span class="tag t-bad">支柱 2 契约单源</span> + <span class="tag t-bad">支柱 5 可观测</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><b>延迟显示 stale / 拿不到</b><br><span class="small">Android·Windows urltest</span></td>
|
||
<td>延迟值没有稳定出口、没有契约校验,靠各端自行解析、刷新时机不一致。</td>
|
||
<td><span class="tag t-bad">支柱 5 可观测</span> + <span class="tag t-bad">支柱 2 契约单源</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><b>记账若用 MySQL 专属时间函数</b></td>
|
||
<td>时间/方言混进 SQL,无法在 SQLite 真库测、不可移植。</td>
|
||
<td><span class="tag t-bad">支柱 3 纯逻辑/IO 分离</span><br><span class="small">时间 Go 端算、方言走 dialect</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><b>连接交互测不了</b><br><span class="small">(通用风险)</span></td>
|
||
<td>连接逻辑直接依赖 <code>NETunnelProviderManager</code> / 真子进程,不连真隧道就无法驱动。</td>
|
||
<td><span class="tag t-bad">支柱 1 接缝即接口</span><br><span class="small">藏到 VpnBridge 后面</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><b>失败路径不可测 / UI 卡死</b><br><span class="small">(通用风险)</span></td>
|
||
<td>连接失败只打日志不返回、空 <code>catch</code> 吞异常,调用方拿不到可断言的错误。</td>
|
||
<td><span class="tag t-bad">支柱 4 错误是有类型的值</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2>执行机制 · 四道闸</h2>
|
||
<p>规范只靠文档一定漂。执行 = 尽量把每条往<b>机器自动守门</b>推,推不动的用<b>脚手架降低犯错成本</b> + <b>评审清单兜底</b>。这不是一个动作,是从「写之前」卡到「push」的<b>四道闸</b>——越早拦,成本越低。</p>
|
||
<div class="fig">
|
||
<p class="cap">图 2 · 四道闸:从「写之前」预防到「push」硬闸,外加评审兜底</p>
|
||
<svg viewBox="0 0 760 250" role="img" aria-label="规范执行四道闸时间线">
|
||
<text x="20" y="18" font-size="11" fill="var(--fg2)">← 越早拦,成本越低</text>
|
||
<g text-anchor="middle" font-size="11.5" fill="var(--fg2)" font-weight="700">
|
||
<text x="95" y="42">写之前</text>
|
||
<text x="267" y="42">写的时候</text>
|
||
<text x="439" y="42">提交时</text>
|
||
<text x="638" y="42">push · PR</text>
|
||
</g>
|
||
<!-- gates -->
|
||
<rect x="20" y="58" width="150" height="112" rx="10" fill="rgba(94,194,122,.10)" stroke="var(--ok)" stroke-width="1.5"/>
|
||
<rect x="192" y="58" width="150" height="112" rx="10" fill="rgba(95,176,201,.10)" stroke="var(--accent2)" stroke-width="1.5"/>
|
||
<rect x="364" y="58" width="150" height="112" rx="10" fill="rgba(224,184,79,.10)" stroke="var(--warn)" stroke-width="1.5"/>
|
||
<rect x="536" y="58" width="204" height="112" rx="10" fill="rgba(224,106,106,.10)" stroke="var(--bad)" stroke-width="1.5"/>
|
||
<!-- gate texts -->
|
||
<g text-anchor="middle">
|
||
<text x="95" y="86" font-size="12.5" font-weight="700" fill="var(--fg)">闸 1 · 脚手架</text>
|
||
<text x="95" y="108" font-size="10.5" fill="var(--fg2)">接口 + Fake 骨架</text>
|
||
<text x="95" y="125" font-size="10.5" fill="var(--fg2)">codegen 默认</text>
|
||
<text x="95" y="155" font-size="11" font-weight="700" fill="var(--ok)">预防</text>
|
||
|
||
<text x="267" y="86" font-size="12.5" font-weight="700" fill="var(--fg)">闸 2 · lint/analyze</text>
|
||
<text x="267" y="108" font-size="10.5" fill="var(--fg2)">空 catch · import 方向</text>
|
||
<text x="267" y="125" font-size="10.5" fill="var(--fg2)">IDE 即时报</text>
|
||
<text x="267" y="155" font-size="11" font-weight="700" fill="var(--accent2)">即时</text>
|
||
|
||
<text x="439" y="86" font-size="12.5" font-weight="700" fill="var(--fg)">闸 3 · pre-commit</text>
|
||
<text x="439" y="108" font-size="10.5" fill="var(--fg2)">format · 快检</text>
|
||
<text x="439" y="125" font-size="10.5" fill="var(--fg2)">codegen 漂移检查</text>
|
||
<text x="439" y="155" font-size="11" font-weight="700" fill="var(--warn)">本地拦</text>
|
||
|
||
<text x="638" y="86" font-size="12.5" font-weight="700" fill="var(--fg)">闸 4 · CI 硬闸</text>
|
||
<text x="638" y="108" font-size="10.5" fill="var(--fg2)">全量测试 · 契约快照</text>
|
||
<text x="638" y="125" font-size="10.5" fill="var(--fg2)">覆盖率 · golden · E2E</text>
|
||
<text x="638" y="155" font-size="11" font-weight="700" fill="var(--bad)">挡 merge</text>
|
||
</g>
|
||
<!-- arrows -->
|
||
<g stroke="var(--fg2)" stroke-width="1.5" fill="var(--fg2)">
|
||
<line x1="170" y1="114" x2="190" y2="114"/><polygon points="192,114 182,110 182,118"/>
|
||
<line x1="342" y1="114" x2="362" y2="114"/><polygon points="364,114 354,110 354,118"/>
|
||
<line x1="514" y1="114" x2="534" y2="114"/><polygon points="536,114 526,110 526,118"/>
|
||
</g>
|
||
<!-- review bar -->
|
||
<rect x="20" y="196" width="720" height="38" rx="8" fill="var(--panel2)" stroke="var(--border)" stroke-width="1"/>
|
||
<text x="380" y="219" text-anchor="middle" font-size="11.5" fill="var(--fg2)"><tspan fill="var(--accent)" font-weight="700">评审清单(人治兜底)</tspan> · 机器判不了的:接缝抽象是否合理 / 关键状态有没有留可观测出口</text>
|
||
</svg>
|
||
</div>
|
||
<ul>
|
||
<li><b>闸 1 · 脚手架(预防)</b>:让对的写法成默认路径——新模块生成器自带「接口 + Fake 骨架」,token/proto 一律走 codegen。不主动绕,自然合规。</li>
|
||
<li><b>闸 2 · lint/analyze(即时)</b>:<code>flutter analyze</code> + <code>go vet</code> + 自定义规则(空 <code>catch</code>、import 方向、业务层禁直接 new 外部依赖),编辑器当场红线。</li>
|
||
<li><b>闸 3 · pre-commit(本地把关)</b>:format + 快检 + 红线词扫描 + codegen 漂移检查(生成物与源不一致即拦,逼你别手改 <code>*.gen.dart</code>)。</li>
|
||
<li><b>闸 4 · CI 硬闸(守门,失败挡 merge)</b>:全量测试 + 覆盖率阈值 + 契约快照 diff=0 + 可移植 SQL 扫描 + golden 像素闸 +(按需)E2E。不可绕过的最终防线。</li>
|
||
</ul>
|
||
<p>诚实区分「能不能机器强制」——能强制的硬卡 CI,强制不了的靠脚手架 + 评审兜底:</p>
|
||
<table>
|
||
<thead><tr><th style="width:20%">支柱</th><th style="width:40%">主执行手段</th><th style="width:16%">强制度</th><th style="width:24%">现状</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>1 接缝即接口</td><td>import 方向 lint + 脚手架 + 评审清单</td><td><span class="tag t-warn">半自动</span></td><td><span class="tag t-bad">待补</span></td></tr>
|
||
<tr><td>2 契约单源</td><td>契约快照 diff=0 + codegen 漂移检查</td><td><span class="tag t-ok">强制</span></td><td><span class="tag t-bad">待补</span><span class="small">(OpenAPI 已有)</span></td></tr>
|
||
<tr><td>3 纯逻辑/IO 分离</td><td>覆盖率阈值 + 可移植 SQL 扫描</td><td><span class="tag t-ok">强制</span></td><td><span class="tag t-warn">部分已有</span></td></tr>
|
||
<tr><td>4 错误是值</td><td>lint 禁空 catch + 失败路径用例</td><td><span class="tag t-warn">半自动</span></td><td><span class="tag t-bad">待补</span></td></tr>
|
||
<tr><td>5 可观测</td><td>关键状态出口评审 + L4 冒烟据出口断言</td><td><span class="tag t-warn">半自动</span></td><td><span class="tag t-bad">随 L4</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="note">这个仓库的落点:<code>.gitea/workflows/ci.yml</code>(shellcheck、OpenAPI 校验、红线词扫描、<code>flutter analyze + test</code>)= 闸 4 骨架已在。<b>缺</b>:契约快照、golden 进 CI、可移植 SQL 扫描、pre-commit hook、自定义 lint、PR checklist。</p>
|
||
|
||
<h2>可复用:通用原则 vs 本项目实例</h2>
|
||
<p>换到你别的多端项目时,<b>五条原则不变</b>,只替换右列实例:</p>
|
||
<table>
|
||
<thead><tr><th style="width:30%">通用原则</th><th style="width:70%">Pangolin 实例(换项目时替换)</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>接缝即接口</td><td><code>VpnBridge</code> / <code>KernelProcess</code> / <code>TokenStore</code> / <code>AuthApi(http.Client)</code></td></tr>
|
||
<tr><td>契约单源</td><td><code>stats</code> EventChannel · <code>/v1/*</code> JSON · <code>UsageEntry</code> proto · <code>colors_and_type.css</code></td></tr>
|
||
<tr><td>纯逻辑/IO 分离</td><td>速率/时长格式化 · 配额计算 · 时间 Go 端算 · dialect 方言层</td></tr>
|
||
<tr><td>错误是值</td><td><code>AuthApiException</code> · 控制面错误码</td></tr>
|
||
<tr><td>可观测</td><td>status/stats stream · 结构化日志 · <code>/v1/usage</code> · <code>/healthz</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p class="note">本规范为「怎么写」的真相源;「怎么验证」见 <a href="test-architecture.html">测试框架架构说明</a>。两份配套维护,新增接缝时同步更新两边。</p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|