Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 24 章 —— 第一条贷款业务正确性验证

本章承诺: 你会把一条由业务负责人确认的贷款决策写成可执行场景,沿真实 graph 路径解释每个断言,并读懂 BLOGE 0.9.8-RC1 生成的验证报告。你也会知道:没有业务标准答案时,验证必须停在哪里。

学习目标

学完本章,你能够:

  1. 区分“程序执行成功”和“业务结果正确”。
  2. 把业务规则拆成 given / when / then,并让 then 指向可观察的业务输出或结构事实。
  3. 从场景、graph、输出映射、验证策略和报告之间追踪一条完整证据链。
  4. 用少量代表性案例覆盖正常决策、人工复核、重试、降级和补偿。
  5. 在业务答案缺失时停止自动化,不让技术团队替业务拍板。

这一章只增加一个变量

前面的章节主要回答:“graph 能不能按设计运行?”本章只增加一个问题:它跑出的业务答案对不对?

这两个问题不能合并。一个贷款申请可能没有抛异常、每个节点都已完成,却把本该人工复核的客户自动批准了。运行时会说“执行成功”,业务负责人却会说“决策错误”。

因此,业务正确性验证不是更多单元测试的别名。它把业务认可的答案、真实 graph 路径和可复核报告连成一条链。

先预测:低风险申请应该走哪条路

贷款策略负责人给出一个已批准样例:申请 LA-VERIFY-1 风险较低,应该自动通过。先不要看测试代码,预测 graph 最终应留下哪些可观察事实:

观察点你的预测为什么不能只看最终 HTTP 200
风险决策auto_approve证明路由依据正确
批准状态approved证明批准分支真的产生业务结果
拒绝节点SKIPPED证明互斥分支没有被误执行

第三个观察点很重要。只检查 approved,可能漏掉“批准和拒绝都执行了”这种结构性错误。

图 24-1:业务故事如何变成可重复执行的验证场景

五份资产,各自回答一个问题

第一条可审计证据链不是一个大文件,而是五份职责不同、可以互相追踪的资产:

资产回答的问题本章中的实例
业务标准答案什么结果才算对低风险申请应自动批准
场景用什么输入、执行什么动作、检查什么事实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 追一遍:断言从哪里拿到事实

图 24-2:自动批准案例从输入到报告的证据链

自动批准案例实际经过以下因果链:

  1. FetchLoanApplication 根据 caseName=auto-approve 构造确定性的申请资料。
  2. creditfraudincomeblacklist 四项检查读取同一份申请,各自产生风险事实。
  3. AggregateRisk 汇总这些事实,输出 decision=auto_approve
  4. graph 只进入 approve 分支;reject 和人工复核分支不应执行。
  5. outputs 把节点内部数据映射成稳定的 riskDecisionapprovedStatus 等业务输出,验证器再比较预期和实际值。

这解释了两类断言为什么要同时存在:

  • 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;
  • thenrefundDecision=auto_approve,原路退款分支执行,人工审核分支跳过;
  • oracle:售后负责人批准的退款政策样例。

变化的是业务词汇和标准答案,不变的是证据链:业务 oracle → 场景 → graph → 可观察事实 → 报告。

轮到你:只改一个变量

复制 auto-approve 场景,只把 caseName 改成 manual-review,先写下你预测的三个事实,再与仓库中的正式 case 比较:

  1. riskDecision 应是什么?
  2. 哪个最终状态应出现?
  3. 哪条自动分支必须是 SKIPPED

如果你的答案与业务 owner 的批准样例冲突,不要修改 expected 让测试变绿;先确认是实现错误、样例过期,还是政策已经改变。

实验验收卡

  • 预期与观察: 低风险贷款满足 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.