BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · 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,而案例本身仍然验证通过。