BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · English
第 24 章 —— 第一条贷款业务正确性验证
本章承诺: 你会把一条由业务负责人确认的贷款决策写成可执行场景,沿真实 graph 路径解释每个断言,并读懂 BLOGE
0.9.8-RC1生成的验证报告。你也会知道:没有业务标准答案时,验证必须停在哪里。
学习目标
学完本章,你能够:
- 区分“程序执行成功”和“业务结果正确”。
- 把业务规则拆成
given / when / then,并让then指向可观察的业务输出或结构事实。 - 从场景、graph、输出映射、验证策略和报告之间追踪一条完整证据链。
- 用少量代表性案例覆盖正常决 策、人工复核、重试、降级和补偿。
- 在业务答案缺失时停止自动化,不让技术团队替业务拍板。
这一章只增加一个变量
前面的章节主要回答:“graph 能不能按设计运行?”本章只增加一个问题:它跑出的业务答案对不对?
这两个问题不能合并。一个贷款申请可能没有抛异常、每个节点都已完成,却把本该人工复核的客户自动批准了。运行时会说“执行成功”,业务负责人却会说“决策错误”。
因此,业务正确性验证不是更多单元测试的别名。它把业务认可的答案、真实 graph 路径和可复核报告连成一条链。
先预测:低风险申请应该走哪条路
贷款策略负责人给出一个已批准样例:申请 LA-VERIFY-1 风险较低,应该自动通过。先不要看测试代码,预测 graph 最终应留下哪些可观察事实:
| 观察点 | 你的预测 | 为什么不能只看最终 HTTP 200 |
|---|---|---|
| 风险决策 | auto_approve | 证明路由依据正确 |
| 批准状态 | approved | 证明批准分支真的产生业务结果 |
| 拒绝节点 | SKIPPED | 证明互斥分支没有被误执行 |
第三个观察点很重要。只检查 approved,可能漏掉“批准和拒绝都执行了”这种结构性错误。
五份资产,各自回答一个问题
第一条可审计证据链不是一个大文件,而是五份职责不同、可以互相追踪的资产:
| 资产 | 回答的问题 | 本章中的实例 |
|---|---|---|
| 业务标准答案 | 什么结果才算对 | 低风险申请应自动批准 |
| 场景 | 用什么输入、执行什么动作、检查什么事实 | loan-approval.scenario.yaml |
| graph | 系统实际经过哪些节点和分支 | loan-approval.bloge |
| 验证策略 | 哪些案例必须存在、证据至少多深 | verification-policy.yaml |
| 报告 | 这次运行究竟观察到了什么 | target/bloge-verification/ |
它们的关系是:业务负责人给答案,技术团队把答案变成可执行观察,BLOGE 负责运行并留下证据。
把一句业务规则写成 Given / When / Then
仓库中的真 实场景先声明责任人、graph 来源和 case 身份,然后写三段式合同。下面只保留 auto-approve 的关键部分:
- caseId: auto-approve
title: 低风险申请自动通过
requirementRefs: [LOAN-DECISION-001]
oracle:
type: APPROVED_EXAMPLE
sourceRef: starter-policy:auto-approve
owner: loan-policy-owner
given:
context:
applicationId: LA-VERIFY-1
caseName: auto-approve
when:
action: EXECUTE
then:
outcome: SUCCESS
expectations:
- actual: {kind: BUSINESS_OUTPUT, outputId: riskDecision}
matcher: EQUALS
expected: auto_approve
- actual: {kind: BUSINESS_OUTPUT, outputId: approvedStatus}
matcher: EQUALS
expected: approved
- actual: {kind: NODE_STATUS, nodeId: reject}
matcher: EQUALS
expected: SKIPPED
读它时不要先记 YAML 字段,先问三个业务问题:
given:当前 Case 描述哪一类现实申请?when:要让系统做什么?这里是执行真实 graph。then:哪些事实足以让业务负责人说“这个答案对”?
oracle 不是一个神秘算法。它说明标准答案来自哪里、属于哪一类证据、由谁负责确认。APPROVED_EXAMPLE 的含义是“业务方认可的代表性样例”,不是 BLOGE 自己推导出的政策。
沿 graph 追一遍:断言从哪里拿到事实
自动批准案例实际经过以下因果链:
FetchLoanApplication根据caseName=auto-approve构造确定性的申请资料。credit、fraud、income、blacklist四项检查读取同一份申请,各自产生风险事实。AggregateRisk汇总这些事实,输出decision=auto_approve。- graph 只进入
approve分支;reject和人工复核分支不应执行。 outputs把节点内部数据映射成稳定的riskDecision、approvedStatus等业务输出,验证器再比较预期和实际值。
这解释了两类断言为什么要同时存在:
BUSINESS_OUTPUT检查业务结果,例如“批准状态是什么”。NODE_STATUS检查执行结构,例如“拒绝分支确实跳过”。
如果业务输出命名稳定,即使 graph 内部重构了若干节点,大部分业务合同仍可保持不变;只有真正关心互斥、重试或补偿行为时,才下探到结构事实。
跑一次真实验证
在书稿根目录执行聚焦测试:
cd submodule/bloge
mvn -pl bloge-starter-loan-approval -am \
-Dtest='LoanApprovalApplicationTest,LoanApprovalBusinessScenarioTest,LoanApprovalOperatorFlowTest' \
-Dsurefire.failIfNoSpecifiedTests=false test
2026-09-14 在 BLOGE commit cc38fbe5、版本 0.9.8-RC1 上得到:
LoanApprovalApplicationTest 1 passed
LoanApprovalOperatorFlowTest 3 passed
LoanApprovalBusinessScenarioTest 7 passed
Total 11 passed, 0 failed
这里的七个动态场景不是七个“方法能否返回”的单元测试,而是三条决策规则和四条放款韧性规则。报告摘要进一步给出:
Suite: loan-approval-decisions
Status: PASS
Cases: 3/3 passed
Executed nodes: 10/10
auto-approve depth=L2 effectSafety=PORT_GUARDED replay=CONTRACT_CONTROLLED
Requirement LOAN-DECISION-001 satisfied=true cases=1/1
auto-approve 的机器可读结果为:
{
"caseId": "auto-approve",
"effectSafety": "PORT_GUARDED",
"realEffectPortIds": [],
"reasonCodes": [],
"replayAssurance": "CONTRACT_CONTROLLED",
"status": "PASS",
"unverifiedOperatorRefs": [],
"verificationDepth": "L2"
}
不要把这些标签读成宣传语:
L2表示证据达到了 graph 场景执行层,不代表生产环境已验收。PORT_GUARDED表示副作用端口受到验证边界约束;空的realEffectPortIds说明这次没有触达真实外部副作用端口。CONTRACT_CONTROLLED表示重放条件由合同控制,不等于任意生产现场都可完全复现。reasonCodes=[]和unverifiedOperatorRefs=[]表示本次没有额外降级理由或未验证 Operator 引用。
从一条案例扩到决策覆盖图
一条自动批准 Case 只支持一个政策区域内已经声明的断言。这个决策 suite 抽样三个互斥业务区域:
| Case | 业务形状 | 必须命中的结果 | 必须排除的结果 |
|---|---|---|---|
auto-approve | 低风险 | 自动批准 | 拒绝分支跳过 |
auto-reject | 高风险 | 自动拒绝 | 批准分支跳过 |
manual-review | 中风险或信息需复核 | 进入人工审核并得到最终状态 | 自动批准分支跳过 |
这不是为了追求“Case 数量”。三个 Case 对应三个互斥的业务决策区域;少一个,就有一块已声明政策空间没有标准答案,但三条样例本身也不等于政策全集。
再看放款 :正确性也包括失败后的世界
放款 suite 把问题从“选哪条分支”推进到“失败后系统应留下什么业务状态”:
| Case | 现实映射 | 关键证据 |
|---|---|---|
funding-success | 正常放款并通知借款人 | fundingStatus=notified |
transient-booking-retried | 账务服务短暂抖动 | 第一次安排重试,第二次成功 |
booking-fallback-used | 重试耗尽,采用已批准降级 | 第三次调用留下 FALLBACK_APPLIED |
notification-failure-compensated | 钱已预订,但通知发生不可重试错误 | graph 失败,账务预订撤销,资金释放 |
最后一条尤其容易写错:业务正确的结果不是“整条 graph 成功”,而是明确失败,同时把已经发生的可逆副作用补偿掉。所以 then.outcome 可以是 FAILURE,而案例本身仍然验证通过。
没有业务答案时,必须停下来
假设产品经理只说:“高净值客户的人工复核可以更宽松。”这句话还不能直接写成可执行 oracle。至少缺少:
- “高净值”的可判断定义;
- 哪些风险维度可以放宽、放宽到什么值;
- 最终应该批准、拒绝,还是仍由人工决定;
- 谁对这个标准答案负责。
此时正确动作不是让开发者猜一个 expected,也不是从当前代码反推出“代码现在怎么做,什么就是正确”。应把场景标成待业务澄清,记录缺失的规则和 owner,然后停止发布门禁的通过判断。
停止规则: 没有可追溯的业务 oracle,就可以验证“系统做了什么”,但不能宣称“业务结果正确”。
Maven 插件失败时,也不要猜
0.9.8-RC1 的 Maven 验证入口需要测试 classpath。如果插件无法构造它,会稳定报告:
TEST_CLASSPATH_UNAVAILABLE: cannot execute BLOGE verification
这不是业务案例失败,而是验证环境没有建立起来。应先修复依赖或 reactor 范围,再重新执行;不要把“验证没有运行”写成 PASS,也不要仅凭已有报告目录判断这次构建成功。
当前证据支持什么,又没有证明什么
本章证据支持以下结论:
- 固定 commit 上的三条贷款决策和四条放款场景全部通过。
auto-approve的业务输出、互斥分支和需求映射都留下了机器可读证据。- 场景没有触达真实外部副作用端口,重放条件受合同控制。
它没有证明:
- 所有可能输入组合都正确;
- 业务政策本身合理或符合法规;
- 生产数据库、消息系统和外部服务已经验收;
- 未来版本仍会得到相同结果。
业务正确性验证的价值不在于给系统盖一个永久的“正确”印章,而在于让一次结论能够回答:谁定义了答案、用什么输入、跑过哪条路径、观察到什么、在哪个版本上成立。
换一个行业,方法仍然成立
把贷款换成电商退款:
given:订单已签收 3 天,商品未拆封,金额 199 元;when:执行退款资格 graph;then:refundDecision=auto_approve,原路退款分支执行,人工审核分支跳过;oracle:售后负责人批准的退款政策样例。
变化的是业务词汇和标准答案,不变的是证据链:业务 oracle → 场景 → graph → 可观察事实 → 报告。
轮到你:只改一个变量
复制 auto-approve 场景,只把 caseName 改成 manual-review,先写下你预测的三个事实,再与仓库中的正式 case 比较:
riskDecision应是什么?- 哪个最终状态应出现?
- 哪条自动分支必须是
SKIPPED?
如果你的答案与业务 owner 的批准样例冲突,不要修改 expected 让测试变绿;先确认是实现错误、样例过期,还是政策已经改变。
编码实践
BP-14 — 验证独立答案与禁止动作
使用认可的标准答案,不从当前输出生成 expected。业务结果、节点路径与 effect 证据分别观察,只替换有意受控的边界。
单变量练习: 运行工作坊黑名单优先案例及规则提前的变异,解释保留旧行为、捕获一个变异、证明全部政策正确为何是不同结论。
实验验收卡
- 预期与观察: 低风险贷款满足 Given/When/Then,报告 3/3 passed。
- 失败与恢复: 删除标准答案或让插件失败;恢复已确认答案或构建,缺事实时停下。
- 证明边界: 证明固定 Case 和 graph 合同,不证明政策完整或已签署发布。
- 练习合同: auto-approve Case;只改一个字段;交付 path、Expectation、Report;结果可归因且无猜测即停止。
继续阅读源码
下一章第 25 章——Policy、Fixture 与 Assurance会继续追问:通过的 Suite 是否在预期动作、依赖和 assurance 边界内执行。
Coding Agent: Open the versioned task guide.