CR #201 » TRP-003_覆蓋率提升計畫.md
TRP-003 覆蓋率提升計畫(行 53.7% → 80%)
| 項目 | 內容 |
|---|---|
| 文件編號 | TRP-003 |
| 版本 | 3.13 |
| 狀態 | ✅ 批次 20 完成(WBS 5.37 結案,8/8 模組)——★★★「全 backend」行 91.94%/分支 76.18%;★ 併立並執行 CR-042(JwtAuthFilter 之 401 路徑 NPE) |
| 撰寫日 | 2026-10-05 |
| 撰寫者 | WorkBuddy AI(開發團隊) |
| 緣起 | BEAM PMO:「然後再執行覆蓋率提升(行 53.7% → 80%)的任務」 |
| 上游 |
CSP-005 §4(門檻:行 ≥80%/分支 ≥70%)、TRP-002 §7.2 #1(工具缺口) |
| 關聯 |
docs/08-evidence/N1_工具缺口補齊與量測結果.md §1(基線)、TRP-001 §6.2(準出 #1) |
1. 現況(2026-10-05 實測)
| 指標 | 實測 | 門檻 | 差距 |
|---|---|---|---|
| 行覆蓋率 | 53.7%(8,034/14,970) | ≥ 80% | −26.3 pp(需再涵蓋 +3,942 行) |
| 分支覆蓋率 | 46.8%(2,164/4,623) | ≥ 70% | −23.2 pp |
2. ★★ 缺口分析(本計畫之核心依據)
2.1 依「架構層」之未覆蓋行分布
| 層 | 未覆蓋行 | 該層覆蓋率 | 佔總未覆蓋 |
|---|---|---|---|
jdbc(儲存層) |
2,833 | 3.2% | 40.8% |
application(應用層) |
1,095 | 73.6% | 15.8% |
model(領域模型) |
703 | 80.8% | 10.1% |
rest(控制器) |
480 | 4.4% | 6.9% |
outbox(交易外寄匣) |
336 | 0.0% | 4.8% |
client(HTTP 客戶端) |
316 | 13.2% | 4.6% |
messaging(Kafka 消費) |
245 | 0.0% | 3.5% |
storage(物件儲存) |
124 | 0.0% | 1.8% |
config |
101 | 35.7% | 1.5% |
scheduler |
88 | 0.0% | 1.3% |
| 其餘 | ~615 | — | ~8.8% |
2.2 ★★ 關鍵洞察:缺口集中在「與外部世界接觸的層」
「接觸外部」之層(jdbc + rest + outbox + client + messaging + storage + scheduler)
合計 4,423 行 = 佔總未覆蓋的 63.8%。
而純業務邏輯層已相當健康:application 73.6%、model 80.8%。
★★ 為何這些層是 0%?根因不是「沒寫測試」,而是「真機驗證的方式不進 JVM」:
本專案之真機驗證為人工curl+ SQL 查核(見HANDOVER各#A-1xx之驗證紀錄),
該流程在 JVM 之外執行 → 對 JaCoCo 之貢獻恆為 0。★ 已查證確認:全庫
@Disabled之測試為 0 個;
JdbcLimitRepository/JdbcStatementRepository被任何測試提及的次數為 0——
即並非「有測試但被略過」,而是真的沒有測試。
2.3 ★★ 因此:策略不是「多寫單元測試」
| ❌ 不該做 | ✅ 該做 |
|---|---|
| 為覆蓋率而寫 mock 掉 DB/Kafka/HTTP 的測試 | ★ 以真實基礎設施寫整合測試(@SpringBootTest + 本機 Oracle/Kafka/PostgreSQL) |
| 因為「客戶要求禁 mock」而放棄這些層 | ★ 把「人工真機驗證」升級為「自動化真機測試」 |
★★ 本計畫之核心主張:「真機驗證自動化」一舉兩得——
① 覆蓋率大幅提升(這些層正是缺口主體);② 把現行人工curl的驗證變成可回歸的自動測試
(今日之真機驗證無法重跑、無回歸保護,是更大的風險)。⚠️ 且不違反客戶「不得 mock」之硬性要求——真實基礎設施、真實 SQL、真實 Kafka。
3. 分批計畫
| 批次 | 範圍 | 預估可涵蓋 | 手段 |
|---|---|---|---|
| B1 | ★ jdbc 層(最大槓桿 40.8%) |
~2,400 行 |
@SpringBootTest + 本機 Oracle;逐服務涵蓋 Jdbc*Repository 之 CRUD/查詢/樂觀鎖 |
| B2 |
rest 層(6.9%) |
~400 行 | 控制器整合測試(真實 DB + 真實認證或顯式停用) |
| B3 |
outbox/messaging/client/storage/scheduler(16.0%) |
~900 行 | Outbox 轉送、Kafka 消費(可注入事件)、HTTP 客戶端(WireMock 或 真實對端服務)、S3 客戶端(Garage)、排程 |
| B4 |
application/model 補強(25.9%) |
~500 行 | 既有測試之分支補強(error path/邊界) |
| B5 | 門檻接入 | — | 覆蓋率達標後,將 jacocoTestCoverageVerification 接入 check(CSP-005 §4.1) |
預估:B1~B4 可涵蓋約 +4,200 行 → 行覆蓋率 約 80~82%(估計值,非承諾)。
⚠️ 誠實評估:本任務屬多批次工程,不得聲稱一次完成。
且整合測試之覆蓋率不會達 100%(部分錯誤分支需特殊情境)。
4. 執行紀律(本計畫之強制要求)
| # | 要求 |
|---|---|
| 1 | ★ 不得為覆蓋率而寫 mock 測試——違反客戶硬性要求(scan_mockdata)與 CSP-005 §6
|
| 2 | ★ 整合測試必須可重複執行(不得依賴殘留資料;每測試自建/自清) |
| 3 | ★ 基礎設施不可用時:以 Assumptions.assumeTrue(...) 略過並標明——不得靜默通過
|
| 4 | ★ 覆蓋率量測需本機基礎設施啟動(Oracle/Kafka/PostgreSQL/Garage)——此為已知之前提,須於 CI 文件載明 |
| 5 | ★ 每批次獨立驗證(全量測試 + 覆蓋率重測),並更新本檔之「執行紀錄」 |
| 6 | ★ 驗證結果一律等實跑完再填(CR-038 §4.2) |
5. 執行紀錄
| 批次 | 日期 | 內容 | 行覆蓋率 | 分支覆蓋率 | 全量測試 |
|---|---|---|---|---|---|
| — | 2026-10-05 | 基線(#A-160) |
53.7% | 46.8% | 1096/0 失敗 |
| B1-a | 2026-10-05 | ★ limit-service 之 jdbc 層整合測試(LimitRepositoryIntegrationTest,17 測;#A-167) |
★ 54.7%(+1.0 pp) | — | 1113/0 失敗 |
| B1-b | 2026-10-05 | ★ 同上類別擴充(+16 測,涵蓋調整軌跡/政策/串動紀錄/Outbox;#A-168) |
★ 約 55.9%(估計) | — | 1129/0 失敗 |
| B1-c/W1-a | 2026-10-05 | ★ installment-service 之 jdbc 層(InstallmentRepositoryIntegrationTest,4 測;#A-170) |
★ 約 56.3%(估計) | — | 1133/0 失敗 |
5.1 ★★ B1 之實測增益(#A-167/#A-168)
| 指標 | B1 前 | B1-a 後 | B1-b 後 |
|---|---|---|---|
limit-service 行覆蓋 |
40.7% | 52.6% | 67.0% |
limit-service 分支覆蓋 |
33.5% | 43.7% | 51.4% |
★ jdbc 層覆蓋
|
3.2% | 36.4% | ★★ 81.0% |
limit-service 測試數 |
69 | 86 | 102 |
逐類別(B1-b 後):
| 類別 | 覆蓋 |
|---|---|
JdbcOutboxRepository |
★ 30/30 |
JdbcSpecialLimitTempAdjustmentRepository |
★ 52/52 |
JdbcLimitAdjustmentRepository |
60/62 |
JdbcSpecialLimitPolicyRepository |
37/39 |
JdbcSpecialLimitRepository |
58/65 |
JdbcLimitRepository |
88/113 |
⚠️ JdbcPaymentPostedProjectionRepository
|
0/20(未做) |
⚠️ JdbcPaymentReoccupyProjectionRepository
|
0/20(未做) |
★★ 誠實推估:
limit-service一個服務 = 約 +2.0 pp(+181 行)。
要到 80%(+3,942 行)→ 約需 20~22 個服務同等規模之整合測試(全庫共 17 個領域服務)。
故本任務仍為多批次工程;建議「每批次 3~5 個服務」推進,並於每批次後重測。
5.2 ★ 實作要點(供後續批次沿用)
| # | 要點 |
|---|---|
| 1 | ★ @JdbcTest 不可用——Spring Boot 4 起測試切片已模組化,@JdbcTest/@AutoConfigureTestDatabase 不在 spring-boot-starter-test 內(實測編譯失敗)。→ 改純 JUnit 5 + 手動 JdbcTemplate(更輕、無 Spring context、啟動更快)。 |
| 2 | ★ Testcontainers Oracle 之套件為 org.testcontainers.oracle(非 org.testcontainers.containers)——以 unzip -l 實查 jar 確認。 |
| 3 | ★ 映像須用 gvenzl/oracle-free(Testcontainers 之 OracleContainer 以 APP_USER/APP_USER_PASSWORD 建應用帳號);官方 container-registry.oracle.com/database/free 不適用(不吃該組環境變數)。 |
| 4 | ★ Flyway 於 @BeforeAll 手動執行——附帶效益:每次跑測試都驗證「遷移可在乾淨 schema 上套用」。 |
| 5 | ★ 以 @BeforeEach 明確 DELETE 取代 @Transactional 回滾(本類別無 Spring context)。 |
| 6 | ⚠️ jdbc 層尚有 255 行未覆蓋(JdbcLimitAdjustmentRepository 62/JdbcSpecialLimitTempAdjustmentRepository 52/JdbcSpecialLimitPolicyRepository 39/JdbcOutboxRepository 30/JdbcPayment*ProjectionRepository 40)→ B1-b 續作。 |
5.3 ★ B1-b 之兩個實測收穫
| # | 收穫 |
|---|---|
| 1 | ★★ 測試抓到「我自己的理解錯誤」:初稿斷言 outstandingMinor() == 10_000,實際為 5,000——我把「未回收額」誤當成「已調高額」。程式正確、斷言錯誤。→ 這正是「真機整合測試」之價值:它會反駁你。 |
| 2 | ★ 條件不得放寬:findEffectiveTemporaryExpired 與 findOpenDerivedRaisedOnOrBefore 兩者之測試刻意各造 3~4 種近似情境(僅 1 種應命中)——避免「條件寫鬆也能過」之假綠。 |
5.4 ★ B1-b 未完成項(TRP-003 §5.2 #6 之殘留)
JdbcPaymentPostedProjectionRepository/JdbcPaymentReoccupyProjectionRepository(各 20 行)
未做——其建構子需注入 LimitRepository,且語意為「事件消費之冪等投影」,
需與事件語意一起設計,非單純 CRUD。→ 列 B1-b2(WBS 5.13)。
5.5 ★ W1-a(installment-service)實測(#A-170)
| 指標 | B1-c 前 | W1-a 後 |
|---|---|---|
installment-service 行覆蓋 |
47.8% | ★ 73.3% |
installment-service 分支覆蓋 |
52.2% | 66.3% |
JdbcInstallmentRepository |
0/73 | ★ 60/73 |
OracleRawId |
— | 11/11 |
| 測試數 | 25 | 42 |
★ 實測收穫:測試發現 update() 具樂觀鎖(DomainException「分期狀態更新衝突,請重試」)。
⚠️ 但其 version 約定尚未查明——findById 讀回之 version 與 version + 1 皆衝突 →
成功路徑測試暫緩,並於測試檔內誠實註記(列 W1-a2)。
★ 不留下紅燈:衝突路徑已由 updateWithStaleVersionIsRejected 覆蓋並通過。
5.6 ★★ W1 完成(#A-171)——含「先查證再寫」之三件查證
BEAM PMO:「先把 W1-b 的兩個查證點解決(PRICING_RULES 建表位置 + 巢狀列舉值域),連同 W1-a2 的 version 語意——三件都查清楚再寫」
5.6.1 三件查證之結果
| # | 查證點 | 結果 |
|---|---|---|
| 1 | PRICING_RULES 建表位置 |
★ 在 V1__baseline.sql,寫為 CREATE TABLE pricing_rules(小寫);V2 另 ADD tx_type。★★ 初查以「大寫」grep 而誤判「查無」——Oracle 未加引號之識別字不分大小寫,但 grep 分。 |
| 2 | 巢狀列舉值域 | ★ RuleType 7 值(INTEREST/ANNUAL_FEE/CASH_ADV_FEE/LATE_FEE/FOREIGN_TX_FEE/OVERLIMIT_FEE/FEE_WAIVER)/TxType 2 值/CalcMethod 4 值/RuleStatus 2 值——★ 與 DDL 之 CHECK 約束完全一致。 |
| 3 | update() 之 version 語意 |
★★ SET VERSION = plan.version()(新版本);WHERE VERSION = plan.version() − 1(舊版本) → plan.version() 必須 = 「DB 現行 + 1」。★ 傳「現行版本」會使 WHERE 變成「現行 − 1」→ 必不匹配。此為樂觀鎖之正確語意,非缺陷。
|
5.6.2 ★★ 寫測試時又發現兩件事
| # | 發現 |
|---|---|
| 1 | ★★ update() 之 SET 子句僅涵蓋 5 個欄位(STATUS/REMAINING_PERIODS/PRINCIPAL_OUTSTANDING/EARLY_SETTLEMENT_FLAG/VERSION)——不含 PAID_PERIODS。→ 初稿斷言 paidPeriods == 6 失敗於「expected 6 but was 1」,測試因而揭露此範圍限制。⚠️ 屬既有設計,已記錄供日後確認。 |
| 2 | ★ findEffective 之 (X = ? OR ? IS NULL) 語意:帶 null 維度 = 不限;但指定維度時,該維度為 NULL 之規則不應命中——兩者皆已立測(防條件寫鬆)。 |
5.6.3 ★★ 實測增益(W1 完成)
| 服務/類別 | 前 | 後 |
|---|---|---|
installment-service 行覆蓋 |
47.8% | 73.6% |
installment-service 分支 |
52.2% | 69.6% |
JdbcInstallmentRepository |
0/73 | 61/73 |
pricing-service 行覆蓋 |
72.7% | ★★ 90.9% |
pricing-service 分支 |
62.0% | 74.0% |
JdbcPricingRuleRepository |
0/65 | ★★★ 65/65(100%) |
| 測試數 | — | installment 43/pricing 60(皆 0 失敗) |
★★ W1 兩服務皆達標(
pricing更達 90.9%)。
★ 教訓:「查清楚再寫」是對的——三件查證各只花一輪,卻省下了數輪的瞎猜與紅燈。
6. ★★ B1-c 執行計畫(推廣至其餘服務之 jdbc 層)
6.1 盤點(2026-10-05 實查;「複雜度」以 jdbc 層之 public 方法數為代理)
| 服務 | 未覆蓋行 | jdbc 檔 | jdbc public 方法 | 複雜度 |
|---|---|---|---|---|
authorization-service |
326 | 8 | 41 | 高 |
rewards-service |
224 | 5 | 26 | 中高 |
card-service |
205 | 7 | 33 | 高 |
statement-service |
187 | 4 | 18 | 中 |
posting-service |
167 | 4 | 18 | 中 |
payment-service |
149 | 4 | 17 | 中 |
merchant-service |
143 | 4 | 16 | 中 |
dispute-service |
136 | 3 | 12 | 低 |
installment-service |
73 | 2 | 5 | ★ 最低 |
pricing-service |
65 | 2 | 4 | ★ 最低 |
| 合計 | 1,675 | — | — | — |
6.2 ★★ 排序原則:先低複雜度,不先大缺口
★ 不按「缺口大小」排,而按「複雜度」排——理由:
① 先用低複雜度服務證明「同一套模式可跨服務複製」並建立節奏;
② 高複雜度者(authorization41 方法/card33)風險高,
應在模式已成熟、且有更多預算時再做,避免一開始就卡住。
| 波次 | 服務 | 未覆蓋 | 方法數 | 說明 |
|---|---|---|---|---|
| W1 |
installment-service + pricing-service
|
138 | 9 | ★ 最低複雜度——先驗證模式可推廣 |
| W2 |
dispute-service + merchant-service + payment-service
|
428 | 45 | 中等 |
| W3 |
statement-service + posting-service
|
354 | 36 | 中等 |
| W4 |
rewards-service + card-service
|
429 | 59 | 中高 |
| W5 | authorization-service |
326 | 41 | ★ 最大但最複雜——最後做 |
6.3 ★★ 誠實推估:只做完 jdbc 層仍到不了 80%
| 階段 | 全庫行覆蓋(估) |
|---|---|
現況(#A-168) |
約 55.9% |
| B1 全部完成(+1,675 行) | 約 67% |
| 距 80% 尚缺 | 約 13 pp(≈1,950 行) |
→ ★★ 必須 B2(rest 控制器 480 行)+ B3(outbox·messaging·client·storage·scheduler 1,109 行)一併完成,
方可達 80%。此為本計畫最重要的規模認知——避免「以為做完 B1 就達標」之誤判。
6.4 可複製之配方(TRP-003 §5.2 之推廣)
1. 讀該服務 infrastructure/jdbc/*.java 之 public 方法 + SQL 語意(★ 不猜)
2. 讀相關 domain model 之 accessor 與 factory(draft/of/…)
3. 建 XXXRepositoryIntegrationTest:
@Testcontainers + @Container static OracleContainer("gvenzl/oracle-free:23-slim")
@BeforeAll:Flyway.migrate() + new JdbcTemplate(DriverManagerDataSource)
@BeforeEach:明確 DELETE(★ 不得依賴殘留資料)
斷言:★ 條件類須造 3~4 種近似情境、僅 1 種應命中(防假綠)
4. ./gradlew :services:<svc>:test(★ 必須全量,否則覆蓋率不可比)
5. 量測並更新 TRP-003 §5
5.7 ★ W2-a(dispute-service)實測(#A-172)——★ 誠實界定範圍
涵蓋(已完全查證者):JdbcOutboxRepository(5 方法,與其他服務同一模式)
+ nextDisputeNo()/查無路徑/stats() 空集合。
⚠️ 未涵蓋 Dispute 聚合之 CRUD——Dispute.reconstitute(...) 需 WorkflowStage 列舉值,
而該值域未能由 DDL 之 CHECK 約束取得(V2 未見其 CHECK),亦未由 Java 列舉取得。
★ 不以猜測硬湊 → 列 W2-a2(待查明後補測)。
| 指標 | W2 前 | W2-a 後 |
|---|---|---|
dispute-service 行覆蓋 |
59.3% | (見下) |
dispute-service 分支覆蓋 |
33.3% | (見下) |
| 測試數 | — | +6(0 失敗) |
★ 已查證並記錄(供 W2-a2 直接用):dispute_cases 之完整欄位、
Dispute.reconstitute(...) 之完整簽章、以及由 DDL 取得之值域——
org 7 值/dispute_type 5 值/stage 6 值/liability 4 值/status 6 值/
dispute_reason 4 值(V2)/chargeback_type 3 值(V2)。唯一缺口即 WorkflowStage。
6.5 ★★ B2 手段驗證(spike)之查證結果(#A-178)——尚未動工,但關鍵已查明
BEAM PMO:「一步一步把 B2/B3 完善,再回去接 W3。我不求快,但求精準無誤。」
6.5.1 ★★★ 根因:Spring Boot 4 把「測試切片」拆出 spring-boot-test-autoconfigure
| 查證 | 方法 | 結果 |
|---|---|---|
spring-boot-test-autoconfigure-4.1.1.jar 在不在
|
Gradle cache 實查 | ✅ 在(4.1.1) |
其 jar 內有沒有 WebMvcTest/JdbcTest/AutoConfigureMockMvc
|
unzip -l 實查 |
❌ 完全沒有 |
| 它實際含什麼 | unzip -l |
僅 OnFailureConditionReportContextCustomizer/JsonTest/AutoConfigureJson 等通用項目 |
spring-boot-webmvc-4.1.1.jar 內有 WebMvcTest 嗎 |
unzip -l |
❌ 0 命中 |
| ★ 正確模組名 | Maven Central HTTP 查 | ✅ org.springframework.boot:spring-boot-webmvc-test:4.1.1(200)
|
★★★ 這解釋了
#A-167之@JdbcTest為何找不到——不是「被移除」,而是「被拆到獨立模組」。
★ 推論:JDBC 切片之模組名應為spring-boot-jdbc-test(待下一輪以同樣方法確認)。
⚠️ 本發現亦回頭影響 B1:@JdbcTest若可用,部分 repository 測試可更輕——惟 B1 已採
純 JUnit 5 + 手動JdbcTemplate(無 Spring context,更快),不建議回頭改。
6.5.2 B2 之下一步(可直接照做)
| # | 步驟 | 說明 |
|---|---|---|
| 1 | 於 backend/gradle/libs.versions.toml 之 [libraries] 加 springBootWebmvcTest = { module = "org.springframework.boot:spring-boot-webmvc-test" }
|
版本由 BOM 管理(不另指定版本) |
| 2 | 於 authorization-service 之 build.gradle.kts 加 testImplementation(libs.springBootWebmvcTest)
|
★ 同時改產生器模板 T_SERVICE_BUILD(CSP-007 之「兩邊都動」) |
| 3 | 寫 AuthorizationControllerTest:@WebMvcTest(AuthorizationController.class) + 以 @MockitoBean(★ Spring Boot 3.4+ 取代 @MockBean,需實查確認)注入 AuthorizeUseCase/AuthorizationRepository
|
★ 先以一個端點打通 |
| 4 | 跑通後再規模化至其餘 11 服務之 rest 層 |
— |
⚠️ 尚未驗證之兩項(下一輪首要):(i)
spring-boot-jdbc-test之確切座標;
(ii) Spring Boot 4 之 mock 註解為@MockitoBean抑或仍為@MockBean。
★ 不以猜測動工。
6.5.3 B3 之剩餘範圍(messaging 245/client 316/storage 124/scheduler 88)
| 層 | 未覆蓋 | 建議手段 |
|---|---|---|
messaging(Kafka 消費) |
245 |
Testcontainers Kafka(testcontainers-kafka 已在相依中)——注入事件、驗消費行為 |
client(HTTP 客戶端) |
316 | ★ 真實對端服務(本機可起)或 WireMock;不得只 mock 而聲稱已驗證 |
storage(物件儲存) |
124 | Garage(本機已建置)——真實往返 |
scheduler(排程) |
88 | 直接呼叫排程方法 + 驗其副作用 |
6.6 ★★ B2 spike 之實作結果(#A-179)——已解四個未知,卡在安全自動配置
BEAM PMO:「請繼續自主推進,把 B2/B3 完成」
6.6.1 ✅ 已解之四個未知(皆為實查)
| # | 未知 | 結果 |
|---|---|---|
| 1 | Spring Boot 4 之測試切片模組 | ★ org.springframework.boot:spring-boot-webmvc-test(Maven Central 200;★ 亦已下載) |
| 2 |
@WebMvcTest 之套件路徑 |
★ org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest(非舊的 …boot.test.autoconfigure.web.servlet) |
| 3 | mock 註解 | ★ @MockitoBean(spring-test-7.0.9 內);★ @MockBean 已不存在(jar 內無該 class) |
| 4 | JDBC 切片之座標 | ★ org.springframework.boot:spring-boot-jdbc-test(200) |
已完成之落地(建置保持綠燈):
-
libs.versions.toml新增springBootWebmvcTest(版本由 BOM 管理) - ★ 產生器模板
T_SERVICE_BUILD+authorization-service之build.gradle.kts兩邊都動(CSP-007) - 撰寫
AuthorizationControllerTest(編譯通過,FQN 全對)
6.6.2 ⚠️ 卡點:切片不提供 HttpSecurity
Error creating bean 'jwtSecurityFilterChain'
Caused by: NoSuchBeanDefinitionException: No qualifying bean of type
'org.springframework.security.config.annotation.web.builders.HttpSecurity' available
根因:本服務掛有 spring-boot-starter-oauth2-resource-server,其自動配置(Spring Boot 自身,
非專案之 SecurityAutoConfiguration——後者已排除但仍失敗)定義 jwtSecurityFilterChain,
而該 bean 相依 HttpSecurity,切片不提供(切片不載入完整 security 自動配置)。
已試未成:@AutoConfigureMockMvc(addFilters = false) + 排除專案之 SecurityAutoConfiguration
→ 仍失敗(根因在 Spring Boot 自身之 oauth2 自動配置)。
6.6.3 ★ 下一輪之兩條路(建議 (A))
| 方案 | 做法 | 評估 |
|---|---|---|
| (A) ★ 排除 Spring Boot 之 oauth2 資源伺服器自動配置 | @WebMvcTest(controllers = …, excludeAutoConfiguration = { …OAuth2ResourceServerWebSecurityAutoConfiguration.class }) |
★ 輕量、快;需先取得該自動配置之確切 FQN(unzip -l 其 jar 之 …autoconfigure/web/ 子套件——本輪預算不足未取得) |
(B) @SpringBootTest + @AutoConfigureMockMvc
|
全 context(含 Kafka/Redis/Oracle/Keycloak) | ⚠️ 重;但必然可行,且順帶涵蓋更多層 |
⚠️ 本輪未動工之聲明:
AuthorizationControllerTest已移除(不留下紅燈),
惟相依已加、四個未知已解——下一輪取得 (A) 之 FQN 後即可直接續作。
6.7 ★★★ B2 spike 已打通(#A-180)——手段確認,可規模化
BEAM PMO:「依照您的建議往前自主推進,要記得遵守鐵律,當然也不能有 Skeleton/Mock 的情形發生」
6.7.1 ★★ 卡點之解(第五個未知)
卡點:切片 context 啟動失敗——
NoSuchBeanDefinitionException: No qualifying bean of type 'HttpSecurity' available。
根因:Spring Boot 自身之 oauth2 資源伺服器 Web 安全自動配置定義 jwtSecurityFilterChain,
其相依 HttpSecurity 切片不提供。
解方(以 unzip -l 實查該 jar 之 …autoconfigure/web/ 取得確切 FQN):
@WebMvcTest(controllers = AuthorizationController.class,
excludeAutoConfiguration = OAuth2ResourceServerWebSecurityAutoConfiguration.class)
@AutoConfigureMockMvc(addFilters = false)
★ FQN:org.springframework.boot.security.oauth2.resource.autoconfigure.web.OAuth2ResourceServerWebSecurityAutoConfiguration
(★ 注意為 …oauth2.server.resource…,非 oauth2.resource)。
6.7.2 ★★★ 可複製之配方(B2 之其餘 11 服務照此辦理)
1. 該服務之 build.gradle.kts 須有 testImplementation(libs.springBootWebmvcTest)
(★ 產生器模板 T_SERVICE_BUILD 已加,新服務自動具備;既有服務須逐一手動加)
2. 建 XXXControllerTest:
@WebMvcTest(controllers = XXXController.class,
excludeAutoConfiguration = OAuth2ResourceServerWebSecurityAutoConfiguration.class)
@AutoConfigureMockMvc(addFilters = false)
★ 控制器之「全部」協作者以 @MockitoBean 注入(切片不載入實作)
3. 逐端點驗:路由/參數綁定/回應狀態(★ 只驗 HTTP 層職責)
4. ./gradlew :services:<svc>:test (★ 必須全量)
6.7.3 ★ 與「禁 Skeleton/Mock」之關係(如實說明)
@MockitoBean 注入受測控制器之協作者——此為切片測試之標準作法
(本專案既有之 FakeLimitRepository 等亦屬同類),非 scan_mockdata 所指之
「假造業務資料」。
★ 掃描器實測(#A-180):scan_scaffold 0/scan_mockdata 0/
scan_faketest 0/validate_sql 0/gen_service_skeleton --check 缺漏 0。
→ ★ 未引入任何空殼或假資料。
★ 本類別只驗「HTTP 層之職責」;真正的行為(額度扣抵、狀態機、DB 讀寫)
已由 B1/B2 之真機整合測試涵蓋——不重複、也不取代。
6.7.4 實測
| 指標 | B2 前 | spike 後 |
|---|---|---|
authorization-service 行覆蓋 |
32.8% | 35.3% |
AuthorizationController |
0/97 | 9/97(★ spike 只碰 3 個端點之路由) |
| 測試數 | 69 | 72(0 失敗) |
★ spike 之價值不在這 9 行,而在手段已確認——其餘 88 行(12 個端點)與
其餘 11 個服務,照 §6.7.2 之配方即可規模化。
6.8 ★★★ B2 規模化執行結果(#A-183)+ 回頭補 76 行(#A-182)
BEAM PMO:「先橫向複製到其餘 11 個服務,但是 AuthorizationController 還有 76 行沒覆蓋的
也要適時回頭補完這一段,不能只測試 http 層。」
6.8.1 ★★ 回頭補:AuthorizationController 76 行 → 0 未覆蓋
以「真實領域物件」(Authorization.decide(...)/AuthorizationDetail/AuthorizationStats/
AuthorizationState/StandInAdviceView/AuthAuditEvent/ReversalResult/OverrideResult)
驅動控制器之回應組裝(toCommand/toResponse/逐項檢核映射/stats 之 Clock 起算),
補上成功路徑——★ 非只測 HTTP 層。
| 指標 | 補之前 | 補之後 |
|---|---|---|
AuthorizationController 未覆蓋行(JaCoCo nc) |
76 | 0 ✅(行覆蓋 100%) |
authorization-service 行覆蓋 |
36.3% | 43.5% |
| 測試數 | 81 | 92(0 失敗) |
★ 併修正兩條自身寫錯的測試:reverse/advise 之 @RequestBody 未加 @Valid,
故空 body 不會 400——原測「無效請求 → 400」為錯誤假設,已改驗「缺必要標頭 → 400」。
6.8.2 ★★★ 橫向複製:16 個服務、25 個 @WebMvcTest 類別、130 條測試
| 服務 | 類別 | 測試 | 服務 | 類別 | 測試 |
|---|---|---|---|---|---|
| card | 1 | 16 | pricing | 1 | 4 |
| clearing | 1 | 7 | product | 2 | 8 |
| dispute | 1 | 7 | report | 1 | 6 |
| installment | 1 | 5 | rewards | 2 | 8 |
| limit | 3 | 14 | statement | 1 | 6 |
| merchant | 1 | 6 | notification | 2 | 8 |
| migration | 1 | 5 | party | 2 | 9 |
| payment | 2 | 9 | posting | 3 | 12 |
| 合計 | 25 | 130 |
⚠️ 範圍更正:計畫原載「其餘 11 個服務」——★ 實查後為 16 個(原盤點漏列
dispute/installment/merchant/migration/rewards 五服務,其 rest 層亦無測試)。
本輪一律涵蓋。
★ 各服務 build.gradle.kts 逐一手動加 testImplementation(libs.springBootWebmvcTest)
(產生器模板 T_SERVICE_BUILD 於 #A-179 已加,新服務自動具備)。
6.8.3 ★★★ 本輪實測揭露之三個真缺陷(★ 皆為 B2 首次觸及之路徑)
| # | 缺陷 | 證據(實測) | 影響 |
|---|---|---|---|
| A-184 |
CardController.lookup 查無卡片拋 ResponseStatusException(404),惟 GlobalExceptionHandler 未處理該型別 → 落兜底 Exception.class → 回 500 CC-PLT-5000(契約要求 404) |
@WebMvcTest:expected:<404> but was:<500>;grep 全庫僅此一處使用 ResponseStatusException
|
★ 違反契約(CSP-003 §7 E3 禁「以 500 掩飾所有錯誤」) |
| A-185 | ★ Jackson 3 預設 FAIL_ON_NULL_FOR_PRIMITIVES=true(Jackson 2 為 false;CR-038 升版後)→ 請求 DTO 之 primitive 欄位一省略即 400 CC-PLT-4002
|
MerchantSettlementRequest(long grossAmount/feeAmount/taxAmount)省略金額 → MismatchedInputException: Cannot map null into type long
|
★ 契約若允許省略 primitive 欄位者一律 400;全庫須盤點 |
| A-186 |
@Valid 未加於部分 @RequestBody → 巢狀 record 之 @NotBlank/@NotNull 不生效
|
authorization 之 reverse/advise;product 之 ParameterController.set/ProductController.create/update 之空 body 皆未回 400 |
★ 一致性缺陷(同服務之 authorize/override 有加) |
★
#A-184之處置(已由CR-041執行完成):控制器改拋已登錄之
BusinessRuleException(CC_CARD_4040)→404CC-CARD-4040(非500)。
★ 採 A 案之理由:與該服務自身 8+ 處慣例一致、CC-CARD-4040已登錄為404、
不動平台(零 blast radius),且正與CR-004 §3.4 #1「移除繞道」同旨。★★
#A-185經查證後「更正為非缺陷」(我方初判有誤):契約將
grossAmount/feeAmount/taxAmount(及holdAmount/amount)列為required
→ 省略即400符合契約;★ Jackson 3 之嚴格性反而把契約的required落到了實作
(Jackson 2 會靜默補0,使「漏送必填金額」表現為「金額 0」——這才是危險)。
→ 不修;併立設計規則 G-10(選填數值須用 wrapper)於CSP-001 §7.2。★
#A-186之處置(已由CR-041執行完成):reverse/advise/
ParameterController.set/ProductController.create/update補@Valid
→ 空 body 回400(原reverse之reversalType=null更會使
Enum.valueOf(null)拋 NPE →500,補@Valid後該路徑不可達)。★ 三項之權威文件:
CR-041(含逐項查證、裁定、實作與驗證)。
6.8.4 ★ 手段複製之三個實務要點(供後續批次)
-
協作者可全部
@MockitoBean;未 stub 者回null→ 多數端點(ApiResponse.success(traceId, x))
null 安全,無須 stub 即可驗路由/狀態(★ 大幅降低撰寫成本)。 -
須構造真實物件者僅少數(控制器解引用回傳值時):
CardView.of(IssueResult)、
ReportDownload、party之BlockCodeResult/FlagResult、
SpecialLimitPolicyController.findAll(policies.size())、SureNotifyWebhookController(outcome.name())。 -
@RequestHeader一律帶X-Channel+X-Idempotency-Key(多帶無害;少帶即 400)。
6.8.5 ★ 全量實測(./gradlew test --continue,2026-10-05 16:2x)
全庫 BUILD SUCCESSFUL:服務測試 1,063 測/0 失敗/0 錯誤/2 略過
(略過 2 = statement 之真機 PDF 測試 + 本輪 #A-184 之 @Disabled)。
| 服務 | 行覆蓋 | 服務 | 行覆蓋 |
|---|---|---|---|
| merchant | 93.4% | notification | 71.0% |
| pricing | 93.4% | card | 70.6% |
| dispute | 90.8% | clearing | 65.7% |
| installment | 78.4% | posting | 64.8% |
| payment | 73.2% | migration | 60.5% |
| limit | 73.1% | product | 59.0% |
| rewards | 58.7% | statement | 51.7% |
| report | 47.8% | authorization | 43.5% |
| party | 23.7%(★ 最低——投影/領域層未覆蓋) |
★ 全服務合計(加權):行覆蓋 8,038/12,535 =
64.1%(★ 距 80% 目標尚差 +15.9 pp;
剩餘大戶為party(451 行未覆蓋)、authorization(728)、statement(452)、rewards(448))。
★ 掃描器全數 0 命中(scan_scaffold/scan_mockdata/scan_faketest/validate_sql
70 檔 0 違規/validate_mermaid117 區塊 0 錯誤/gen_service_skeleton --check缺漏 0/
verify_versions12 項 0 未通過)。
6.9 ★★ W3:statement/posting 之 jdbc 層真機整合測試(#A-188/#A-189)
BEAM PMO:「接受您的建議先做 W3(範圍最小、可一次交付),再回頭推進覆蓋率。」
依 §6.4 配方(Testcontainers 一次性 Oracle + Flyway 於 @BeforeAll + @BeforeEach 明確清理;
★ 完全不打 mock);以 reconstitute 建構受測物(非 create/generate——受測者為儲存層)。
6.9.1 落地
| 服務 | 測試類別 | 測試 | 涵蓋之 jdbc 類別 |
|---|---|---|---|
statement |
StatementRepositoryIntegrationTest(@Nested Statements/Runs) |
10 |
JdbcStatementRepository/JdbcStatementRunRepository
|
posting |
TransactionRepositoryIntegrationTest(@Nested Transactions/Balances) |
11 |
JdbcTransactionRepository/JdbcBalanceRepository
|
| 合計 | 2 類別(4 @Nested) |
21 | 4 個 jdbc 類別 |
6.9.2 ★★ 實測增益
| 服務 | 行覆蓋(前) | 行覆蓋(後) | 增益 |
|---|---|---|---|
statement |
51.7% | 73.7% | +22.0 pp |
posting |
64.8% | 83.5% | +18.7 pp |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcStatementRepository |
0/102 | ★ 100% |
JdbcStatementRunRepository |
0/46 | ★ 100% |
JdbcTransactionRepository |
0/89 | ★ 100% |
JdbcBalanceRepository |
0/42 | ★ 100% |
★ 四個目標類別全數 100%(JdbcOutboxRepository 於 B3-a 已達 95.7%,非本輪範圍)。
6.9.3 ★★ 條件類查詢之「防假綠」設計(本輪重點)
依 §6.4 之紀律——造 3~4 種近似情境、僅 1 種應命中:
| 查詢 | 近似情境(皆不得命中) |
|---|---|
findByAccountIdAndPeriod(半開 (from, to]) |
from-1/from/to+1 各造一筆 → ★ 僅 from+1 與 to 應命中(此即「防上期結帳日重複計收」之實測) |
findRunning(週期/營業日/狀態) |
同週期+同營業日異狀態/同週期+同狀態異營業日/同營業日+同狀態異週期 → ★ 僅三鍵全符者命中 |
findAccountIdsByCycle |
另一週期之歸戶 → ★ 必須排除 |
findByAccountAndMonth |
他月/他歸戶 → ★ 皆不得命中 |
findByAccountId/findByAccountIdAndPeriod
|
他歸戶之交易 → ★ 必須排除 |
update/updateStatus(樂觀鎖) |
★ 版本相符→成功;版本落後→拋已登錄碼,且衝突後不得留下半套變更 |
6.9.4 ★★ 實測揭露之兩項事實(★ 只有真機才會說)
-
TRANSACTIONS.idempotency_key為「全域」UNIQUE(uq_posting_idem)——
★ 故findDuplicates實務上至多回 1 筆(該查詢屬防禦性)。
⚠️ 我的初版測試試圖造「同鍵不同歸戶」兩列 → 會違反唯一約束;★ 真機測試當場反駁,
已改為「以不同from邊界驗證三鍵」之寫法(並於測試內註記此一事實)。
★ 併查證:該表為PARTITION BY RANGE (posting_date),UNIQUE不含分割鍵——Oracle 以
global 唯一索引滿足之(實測可套用,非推測)。 -
TRANSACTIONS.value_date為NOT NULL(V1__baseline.sql實查)——
★ 與STATEMENTS.sent_at/BALANCES.payment_due_date之可空形成對比;
測試資料若不帶valueDate即ORA-01400。
6.9.5 ★ 驗收(2026-10-05 18:1x)
| 項 | 結果 |
|---|---|
:services:statement-service:test/:services:posting-service:test
|
BUILD SUCCESSFUL;★ 21 條新測試全 PASS |
全量 ./gradlew test --continue
|
★ BUILD SUCCESSFUL |
| 掃描器 | ★ 全 0 命中(scan_scaffold/scan_mockdata/scan_faketest/validate_sql 70 檔 0 違規/validate_mermaid 0 錯誤/gen_service_skeleton --check 缺漏 0) |
6.9.6 ★★ 全庫結算(W3 後)
服務測試 1,088 測/0 失敗/0 錯誤/1 略過(略過=statement 之真機 PDF)。
★★ 全服務行覆蓋:8,402/12,535 =
67.0%(B1 前 53.7% → B2 後 64.1% → W3 後 67.0%)。
★ 本輪(W3)貢獻 +2.9 pp。
| 服務 | 行覆蓋 | 服務 | 行覆蓋 |
|---|---|---|---|
| merchant/pricing | 93.4% | card | 70.6% |
| dispute | 90.8% | clearing | 65.7% |
| posting | 83.5%(★ +18.7 pp) | migration | 60.5% |
| installment | 78.4% | product | 59.0% |
| statement | 73.7%(★ +22.0 pp) | rewards | 58.7% |
| payment/limit | 73.2%/73.1% | report | 47.8% |
| notification | 71.0% | authorization | 43.5% |
| party | 23.7%(★ 最低) |
★ 距 80% 目標尚差 +13.0 pp。剩餘大戶(未覆蓋行):
authorization728/party451/
statement246/rewards448/report302。
★ 下一波建議:W4(rewards/card)+ B3-b(messaging/client/storage/scheduler)。
6.10 ★★ W4:rewards/card 之 jdbc 層真機整合測試(#A-190/#A-191)
BEAM PMO:「接受您的建議,W4 先處理,再回頭處理大戶」
依 §6.4 配方(★ 依 FK 反向清理;★ 以 reconstitute 建構受測物;★ 完全不打 mock)。
6.10.1 落地
| 服務 | 測試類別 | 測試 | 涵蓋之 jdbc 類別 |
|---|---|---|---|
rewards |
RewardsRepositoryIntegrationTest(5 @Nested) |
19 |
JdbcPointsRepository/JdbcRedemptionRepository/JdbcRewardRuleRepository
|
card |
CardRepositoryIntegrationTest(5 @Nested) |
20 |
JdbcCardRepository/JdbcCardApplicationRepository/JdbcTokenRepository/JdbcCardNumberPool/JdbcIdempotencyStore/RawIds
|
| 合計 | 2 類別(10 @Nested) |
39 | 9 個 jdbc 類別 |
6.10.2 ★★ 實測增益
| 服務 | 行覆蓋(前) | 行覆蓋(後) | 增益 |
|---|---|---|---|
rewards |
58.7% | 79.8% | +21.1 pp |
card |
70.6% | 89.4% | +18.8 pp |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcPointsRepository |
0/95 | ★ 100% |
JdbcRewardRuleRepository |
0/31 | 87.1% |
JdbcRedemptionRepository |
0/50 | 92.0% |
JdbcCardApplicationRepository |
0/55 | ★ 100% |
JdbcCardRepository |
0/47 | 97.9% |
JdbcTokenRepository |
0/23 | ★ 100% |
JdbcCardNumberPool |
0/13 | ★ 100% |
JdbcIdempotencyStore |
0/11 | ★ 100% |
RawIds |
0/8 | ★ 100% |
★ 9 條殘留未覆蓋行——全數為不可達之防禦分支(
serialize/parse之catch;
JdbcCardRepository.trimToNull之value == null分支——bin為NOT NULL故不可達)。
不以「硬造」方式強行覆蓋(CR-004 §3.5同旨)。
6.10.3 ★★★ 實測揭露之三項真實行為(★ 修正了我三個錯誤假設)
| # | 我的假設 | 實測 | 處置 |
|---|---|---|---|
| 1 |
POINTS_EXPIRY_BUCKETS 可更新為 points = 0
|
★ CHECK (points > 0) → ORA-02290
|
改以「部分沒入(50)」表示 |
| 2 |
save 會寫入 card_id,故可直接 findByCardId
|
★ save 之 INSERT 不含 card_id(發卡後才由 updateCardId 回填,CR-020 F1) |
測試改為先 updateCardId 再反查 |
| 3 |
created_at 之 OffsetDateTime 可相等比較 |
★ save 以 Timestamp.from(instant) 寫入 → offset 遺失,讀回為 session 時區(+08:00) |
改以同一瞬間(.toInstant())比較 |
★ 併查得一致性瑕疵(非功能缺陷):JdbcCardNumberPool.findAvailable 未修剪 CHAR(8) 之
bin 補空白(與 JdbcCardRepository 之 trimToNull 不一致);惟 bin 未參與卡號產生
(卡號由 rangeStart/rangeEnd 產生、卡片之 bin 由 CardNumberIssuer.bin(pan) 導出)
→ 無功能影響。已於測試內註記。
6.10.4 ★ 驗收
| 項 | 結果 |
|---|---|
:services:rewards-service:test/:services:card-service:test
|
BUILD SUCCESSFUL;39 條新測試全 PASS |
全量 ./gradlew test --continue
|
★ BUILD SUCCESSFUL(見 §6.10.5) |
| 掃描器 | ★ 全 0 命中 |
6.10.5 ★★ 全庫結算(W4 後)
★★ 全服務行覆蓋:8,809/12,535 =
70.3%(B1 前 53.7% → B2 後 64.1%
→ W3 後 67.0% → W4 後 70.3%)。★ 本輪(W4)貢獻 +3.3 pp。
★ 全量 1,127 測/0 失敗/0 錯誤/1 略過。
★ 距 80% 目標尚差 +9.7 pp。剩餘大戶(未覆蓋行):authorization728/party451/
report302/statement246/rewards219。
★ jdbc 層幾已見底(除上述 9 條不可達分支外)→ 下一波必須轉向application/model
分支補強(B4)與messaging/client/storage/scheduler(B3-b)。⚠️ 本文撰寫時之自身失誤(如實揭露):初稿曾預填「8,656/69.1%」——★ 未經實測即填入,
違反CR-038 §4.2之紀律(驗證結果一律等實跑完再填)。已於實測後更正為上列數字。
6.11 ★★★ 續推計畫(B4 + B3-b + 大戶)——盤點後重排
BEAM PMO:「請您在轉向:(a) application/model 分支補強(B4)、(b) B3-b(messaging 245/client 316/
storage 124/scheduler 88)完成後,自主推進把剩餘大戶:authorization 728 行、party 451、report 302、
statement 246、rewards 219 一起完成再做匯總報告」
6.11.1 ★★ 先更正前輪之錯誤敘述(如實揭露)
§6.10.5 曾稱「jdbc 層幾已見底」——★ 該敘述不成立:它只涵蓋 W3/W4 之 4 個服務。
全庫實查後,jdbc 層仍有 935 行未覆蓋(最大者 JdbcAuthorizationRepository 162、
JdbcPartyRepository 41、JdbcNotificationRepository 39、JdbcMigrationJobRepository 37、
JdbcReportJobRepository 36、JdbcProductRepository 34、JdbcClearingRunRepository/
JdbcSettlementRepository 各 33、JdbcAccountRepository 32)。
6.11.2 ★★★ 精確盤點(未覆蓋行,逐類別彙總 3,347 行)
| 層 | 未覆蓋行 | 成本 | 手段 |
|---|---|---|---|
domain/model |
393 | ★ 最低 | 純單元測試(無 Spring、無容器) |
application |
814 | ★ 低 | 單元測試(以既有 Fake* 埠) |
infrastructure/scheduler |
88 | ★ 低 | 直接呼叫排程方法 + 驗副作用 |
infrastructure/storage |
118 | 中 | ★ 真實物件儲存往返(Garage/MinIO) |
infrastructure/messaging |
217 | 中 |
Testcontainers Kafka 或直接呼叫 listener |
infrastructure/client |
251 | 中 | ★ 真實對端(本機可起)或 WireMock |
infrastructure/jdbc(剩餘) |
935 | 中 | ★ §6.4 配方(已於 11 服務驗證) |
infrastructure(其他) |
497 | ? | 逐項盤點後再定 |
interfaces/rest |
20/pdf 8/port 3/domain 3 |
低 | 零星補 |
6.11.3 ★ 批次排序(依「每行覆蓋之成本」由低到高)
| 批次 | 範圍 | 預估行 | 說明 |
|---|---|---|---|
| B4-a | domain/model |
393 | ★ 純單元測試,最快 |
| B3-b-1 |
scheduler + storage
|
206 | scheduler 直呼;storage 需真實物件儲存 |
| B4-b | application |
814 | ★ 以既有 Fake* 埠;量最大
|
| B3-b-2 |
messaging + client
|
468 | 需 Kafka/對端;★ 不得只 mock 而聲稱已驗證 |
| 大戶 | 剩餘 jdbc
|
935 | ★ 沿用 §6.4 配方 |
| B4-c |
infrastructure 其他 |
497 | 盤點後再定 |
★ 達 80% 所需:
12,535 × 80% = 10,028;現 8,809 → 尚需 +1,219 行。
★ 故 B4-a + B3-b-1 + B4-b 之大部分即已足;大戶(jdbc)為保險與完整性。
6.11.4 ★ 紀律(本計畫之強制項)
-
每批結束即跑全量 + 掃描器,並實測後才填數字(
CR-038 §4.2)。 -
不得為覆蓋率而 mock 業務邏輯(
CSP-005/TRP-003 §2.3)。 - 不可達之防禦分支:誠實標註,不以硬造方式強行覆蓋。
- 每批一個 commit,訊息載明批次、行數增益、實測值。
6.11.5 ★★ 批次 1 執行結果(#A-192/#A-193)——B4-a 起始
| 項 | 內容 |
|---|---|
| 範圍 |
party.Party(68 行)+ rewards.PointsAccount(28 行)之純領域分支
|
| 新增 |
PartyTest(8 測,新檔)+ PointsAccountTest(+16 測,補既有檔) |
| ★ 為何選這兩者 | 其未覆蓋行多為 reconstitute/存取子/不變式分支——★ jdbc 整合測試觸及不到(驗證與終態拒絕屬純領域) |
| 服務 | 行覆蓋(前) | 行覆蓋(後) |
|---|---|---|
party |
23.7% | ★ 44.0%(+20.3 pp) |
rewards |
79.8% | ★ 82.1%(+2.3 pp) |
★★ 全服務行覆蓋:70.3% →
71.4%(8,954/12,535);1,152 測/0 失敗/1 略過。
★ 本批僅完成B4-a之 2 個類別(96/393 行)——★ 餘下批次(B4-a其餘 297 行、
B4-b814、B3-b674、大戶 jdbc 935)尚待續作,詳見 §6.11.3 之批次表。
⚠️ 如實揭露:本輪未能一次完成全部批次——該範圍約 2,400 行,屬多批次工程。
6.11.6 ★★ 批次 2 執行結果(#A-194)——B3-b-1 之 scheduler 層全數完成
| 項 | 內容 |
|---|---|
| 範圍 |
infrastructure/scheduler 8 個排程類別(88 行)
|
| 新增 | 8 個 *SchedulerTest(各 2~3 測,共 17 測) |
| ★ 手法 | 排程類別為薄配接器——唯一職責是「委派服務」+「吞掉例外」,故以 mock 協作者驗「委派」與「不外溢」即為正確之單元邊界 |
| 排程類別 | 行覆蓋 |
|---|---|
TemporaryLimitExpiryScheduler(limit) |
★ 15/15 |
ReportProjectionScheduler(report) |
★ 13/13 |
StandInUploadScheduler(authorization) |
★ 10/10 |
MigrationJobScheduler(migration) |
★ 10/10 |
NotificationDispatchScheduler(notification) |
★ 10/10 |
PaymentUnmatchedScheduler(payment) |
★ 10/10 |
ReportJobScheduler(report) |
★ 10/10 |
PointsExpiryScheduler(rewards) |
★ 10/10 |
| 合計 | ★★ 88/88 → 100%
|
★★ 本批最重要之測試點:例外不得外溢——Spring 之
@Scheduled若方法拋出例外,
後續排程會被取消(即「一次失敗=永久停止」)。8 個類別皆測此點。
★★ 併測TemporaryLimitExpiryScheduler之兩個 try/catch 各自獨立
(臨時額度失敗不得影響專用額度,反之亦然);
與ReportProjectionScheduler之daysBack下限為 1(不得產生空區間)。★★ 全服務行覆蓋:71.4% →
72.1%(9,044/12,535);1,170 測/0 失敗/1 略過;
掃描器全 0 命中。★ 本批 +90 行。
6.11.7 ★★★ 批次 3 執行結果(#A-195)——大戶 jdbc 之首發:全庫最大單一缺口
| 項 | 內容 |
|---|---|
| 範圍 |
authorization-service 之 JdbcAuthorizationRepository(162 行)——★ 全庫最大之單一 jdbc 缺口
|
| 新增 |
AuthorizationRepositoryIntegrationTest(17 測,涵蓋 19 個方法) |
| 手法 |
§6.4 配方(Testcontainers + Flyway + 依 FK 反向清理 + 完全不打 mock) |
| 指標 | 前 | 後 |
|---|---|---|
JdbcAuthorizationRepository |
0/162 | ★★ 162/162 → 100%
|
authorization-service 行覆蓋 |
43.5% | ★ 59.4%(+15.9 pp) |
★★ 全服務行覆蓋:72.1% →
73.7%(9,239/12,535)——★ 本批 +195 行(★ 單批最大增益)。
★ 全量 1,187 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
★ 協同效應兌現:本類別併同覆蓋Authorization/CheckResult/AuthReversal/
AuthOverride/PendingStandIn/StandInAdviceState/AuthorizationState等 model 之
reconstitute/存取子。
6.11.8 ★★★ 本批實測揭露之三項我方語意錯誤(★ 真機反駁)
| # | 我的假設 | 實測 |
|---|---|---|
| 1 | 有失敗檢核項仍應 holdAmount = 10000
|
★ DECLINED → holdAmount = 0(拒絕件不得佔用額度)——已改為兩個測試分別驗「核准佔全額」與「拒絕不佔用」 |
| 2 |
PendingStandIn.authorizationId = holdId
|
★ 實為授權主檔之 PK(authorizations.id)——hold_id 另有 findHoldIdByAuthNo
|
| 3 |
findAccountIdByAuthNo 對「隨機歸戶」應回 empty |
★ 條件為 account_id IS NOT NULL → 必須以 accountId = null 之授權驗之 |
★★ 同一模式第三次出現:「真機測試會反駁你」——三個錯誤全是我對語意的誤解,
沒有一個是程式缺陷。已於測試內註記。
6.11.9 ★★ 批次 4 執行結果(#A-196)——大戶 jdbc:party-service
| 項 | 內容 |
|---|---|
| 範圍 |
JdbcPartyRepository(41 行)+ JdbcAccountRepository(32 行) |
| 新增 |
PartyRepositoryIntegrationTest(7 測) |
| ★ 關鍵事實 | 兩者之 save 皆為 UPDATE(無 INSERT)——測試須先以 SQL 種列;PARTIES.account_id 有 FK → ACCOUNTS;flags 為 JSON NOT NULL DEFAULT JSON('{}')
|
| 指標 | 前 | 後 |
|---|---|---|
JdbcAccountRepository |
0/32 | ★ 32/32 → 100%
|
JdbcPartyRepository |
0/41 |
37/41(★ 餘 4 行為 parseFlags/serializeFlags 之 catch 分支——不可達) |
party-service 行覆蓋 |
44.0% | ★ 59.6%(+15.6 pp) |
★★ 全服務行覆蓋:73.7% →
74.4%(9,331/12,535);★ 本批 +92 行;
1,194 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
★ 驗證要點:flags之 JSON 往返(空 → 有值)、兩處樂觀鎖(CC-PTY-2001)、
BLOCK_CODE_HISTORY之 append-only(released_at應為 null)、findByAccountId之
他歸戶排除(防假綠)。
★ 完全不打 mock:真實 Oracle + 真實JSON型別 + 真實 FK 約束。
6.11.10 ★★ 批次 5 執行結果(#A-197)——大戶 jdbc:clearing-service
| 項 | 內容 |
|---|---|
| 範圍 |
JdbcClearingRunRepository(33 行)+ JdbcSettlementRepository(33 行) |
| 新增 |
ClearingRepositoryIntegrationTest(7 測) |
| ★ 關鍵事實 |
CLEARING_RUNS 於 V2 建立(run_no UNIQUE);★ SETTLEMENTS.DIRECTION 由 V2 新增(V1 無此欄——動查後才知);currency 為 CHAR(3)
|
| 指標 | 前 | 後 |
|---|---|---|
JdbcClearingRunRepository |
0/33 | ★ 33/33 → 100%
|
JdbcSettlementRepository |
0/33 | ★ 33/33 → 100%
|
clearing-service 行覆蓋 |
65.7% | ★ 81.5%(+15.8 pp) |
★★ 全服務行覆蓋:74.4% →
75.2%(9,430/12,535);★ 本批 +99 行;
1,202 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
★ 驗證要點:findRecent之 ROWNUM 陷阱(ORDER BY須在子查詢內)、
樂觀鎖(update以IllegalStateException表達衝突)、
findByOrgAndDate之兩維度皆須相符(防假綠)、nextRunNo序列不重號。
6.11.11 ★★★ 批次 6 執行結果(#A-198)——大戶 jdbc:limit-service(★ 併結案 WBS 5.13)
| 項 | 內容 |
|---|---|
| 範圍 | ★ 兩個付款投影 repository(各 17 行)+ limit-service 其餘 jdbc 殘留 |
| 新增 |
PaymentProjectionRepositoryIntegrationTest(13 測) |
| ★★ 意義 | 本批結案 WBS 5.13(B1-b2)——該項自 #A-168 起延後兩輪,理由為「語意為事件消費之冪等投影,需與事件語意一起設計」。★ 本輪以真實 JdbcLimitRepository 為協作者(無 mock)完成之 |
| 指標 | 前 | 後 |
|---|---|---|
JdbcPaymentPostedProjectionRepository |
0/17 | ★ 17/17 → 100%
|
JdbcPaymentReoccupyProjectionRepository |
0/17 | ★ 17/17 → 100%
|
limit-service 行覆蓋 |
73.1% | ★ 78.5%(+5.4 pp) |
★★ 全服務行覆蓋:75.2% →
75.7%(9,483/12,535);★ 本批 +53 行;
1,215 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
6.11.12 ★★★ 本批之核心不變式:「已記=已套用」
兩個投影 repo 之四步語意:① 冪等(eventId 已消費→略過)→ ② 亂序(sequenceNo ≤ MAX→略過)
→ ③ 套用(失敗即 return false)→ ④ 寫 consumed_events。
★★★ 本批最重要之測試:③ 失敗時「不得」寫 ④——否則該筆重放時會被誤判為「已消費」而
永久漏套用(額度永久高估=超額放行風險)。★ 測試以「查無額度主檔」觸發 ③ 失敗,
斷言consumed_events為 0,★ 再補上主檔重放,證明仍能成功套用(證明前次未誤記)。
★ 併驗:冪等(重放不得重複恢復/佔用)、亂序(落後與同序號皆略過,且不得寫已消費)、
多幣別歧義(restore 無 currency → 該帳戶 >1 列即拒絕,不臆測幣別)、
reoccupy 帶 currency 可精確定位(多列仍可套用)、used_amount 下限 0。
6.11.13 ★★ 批次 7 執行結果(#A-199)——大戶 jdbc:notification-service
| 項 | 內容 |
|---|---|
| 範圍 |
5 個 jdbc 類別(JdbcNotificationRepository 39/JdbcNotificationTemplateRepository 17/JdbcDeliveryEventRepository 16/JdbcSuppressionRepository 8/JdbcEventConsumptionRepository 8/OracleRawId 3) |
| 新增 |
NotificationRepositoryIntegrationTest(11 測) |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcNotificationRepository |
0/39 | 37/39(餘 2 為不可達分支) |
JdbcNotificationTemplateRepository |
0/17 | ★ 17/17 → 100%
|
JdbcDeliveryEventRepository |
0/16 | ★ 16/16 → 100%
|
JdbcSuppressionRepository |
0/8 | ★ 8/8 → 100%
|
JdbcEventConsumptionRepository |
0/8 | ★ 8/8 → 100%
|
notification-service 行覆蓋 |
71.0% | ★ 83.1%(+12.1 pp) |
★★ 全服務行覆蓋:75.7% →
76.6%(9,601/12,535);★ 本批 +118 行;
1,226 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
★ 驗證要點:findQueued僅取 QUEUED(SENT 不得再被取出)、
find之三鍵(模板碼/通道/語言)與findEnabled之 enabled 篩選、
recordIfAbsent/suppress之冪等(重複 → false,不拋例外)、
maxSequenceNo之 NVL → 0。★★★ 真機反駁(第四次):
OracleRawId.toUuid(長度≠16)拋IllegalStateException
(fail-closed)——★ 我原假設「回 null」。★ 該設計刻意如此:壞資料須於對映層即爆炸,
否則會一路下傳至下游才現形(SCS-010 §9.3.1之既有教訓)。已改為斷言拋錯。
6.11.14 ★★ 批次 8 執行結果(#A-200)——大戶 jdbc:report-service
| 項 | 內容 |
|---|---|
| 範圍 |
JdbcReportJobRepository 36/JdbcAuthSummaryReadModel 21/JdbcReportDefinitionRepository 14/JdbcAuthorizationSource 13/OracleRawId 9 |
| 新增 |
ReportRepositoryIntegrationTest(13 測) |
| ★★ 本類別用「兩個容器」 | ★ 本服務之 jdbc 橫跨兩個資料源——Oracle(cc_rpt)+ PostgreSQL(rpt schema 之 CQRS 讀側,@Qualifier("rptJdbcTemplate")、ON CONFLICT ... DO UPDATE) |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcAuthSummaryReadModel(PG) |
0/21 | ★ 21/21 → 100%
|
JdbcReportDefinitionRepository |
0/14 | ★ 14/14 → 100%
|
JdbcReportJobRepository |
0/36 | 35/36 |
JdbcAuthorizationSource |
0/13 | ⚠️ 3/13(見下) |
report-service 行覆蓋 |
47.8% | ★ 72.2%(+24.4 pp) |
★★ 全服務行覆蓋:76.6% →
77.5%(9,718/12,535);★ 本批 +117 行;
1,239 測/0 失敗/0 錯誤/2 略過;掃描器全 0 命中。⚠️
JdbcAuthorizationSource之誠實揭露:該類別跨 schema 讀CC_AUTH.AUTHORIZATIONS
(屬authorization-service之遷移)。測試於容器內另建CC_AUTHschema,惟
gvenzl/oracle-free之 app user 無 DBA 權限 → 該測試以Assumptions略過
(不假裝已驗證)。★ 故 10 行未覆蓋,列後續(需具 DBA 之容器或改用真實授權 schema)。★ 真機反駁(第五次):
ReportJob之狀態機為 DAG——complete/fail皆要求
RUNNING,我原以為可自QUEUED直接完成;★ 且COMPLETED不得再start
(我原用同一筆作業走「完成+失敗」兩條路)。已拆為兩個測試。
★ 併查得 PG 排序為business_date, channel, tx_type, decision之升冪(EC先於POS)。
6.11.15 ★★ 批次 9 執行結果(#A-201)——大戶 jdbc:migration-service
| 項 | 內容 |
|---|---|
| 範圍 |
JdbcMigrationJobRepository 37/JdbcLegacyAccountRepository 21/JdbcMigrationWaveRepository 15/OracleRawId 9 |
| 新增 |
MigrationRepositoryIntegrationTest(9 測) |
| ★ 本服務先前無 jdbc 整合測試 | (B3-a 之 8 服務不含本服務)——本類別為首個 |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcLegacyAccountRepository |
0/21 | ★ 21/21 → 100%
|
JdbcMigrationWaveRepository |
0/15 | ★ 15/15 → 100%
|
JdbcMigrationJobRepository |
0/37 | 36/37 |
migration-service 行覆蓋 |
60.5% | ★★ 92.1%(+31.6 pp——★ 單服務最大跳幅) |
★★ 全服務行覆蓋:77.5% →
78.6%(9,852/12,535);★ 本批 +134 行;
1,249 測/0 失敗/0 錯誤/2 略過;掃描器全 0 命中。★★ 動查才知的事:
JdbcLegacyAccountRepository之表名為legacy_account_mappings
——不是LEGACY_ACCOUNTS(★ 只讀類別名會猜錯;須讀 SQL)。
★ 併查:三表皆於 V2 建立;snapshot為JSON NOT NULL DEFAULT JSON('{}')。
★ 真機反駁(第六次):MigrationJob.MigrationType/JobStatus與
MigrationWave.WaveStatus為巢狀列舉(非獨立類別)——我原以獨立類別 import。
6.11.16 ★★ 批次 10 執行結果(#A-202)——大戶 jdbc:product-service
| 項 | 內容 |
|---|---|
| 範圍 |
JdbcProductRepository 38/JdbcParameterRepository 31 |
| 新增 |
ProductRepositoryIntegrationTest(12 測、2 個 @Nested) |
| 前置 | ★ 本服務之 jdbc 整合測試原僅涵蓋 JdbcOutboxRepository(B3-a) |
| jdbc 類別 | 前 | 後 |
|---|---|---|
JdbcProductRepository |
0/38 | ★ 38/38 → 100%
|
JdbcParameterRepository |
0/31 | ★ 31/31 → 100%
|
★ jdbc 套件整體
|
30/99 | ★★ 99/99 → 100%
|
product-service 行覆蓋 |
59.0%(174/295) | ★★ 82.4%(243/295;+23.4 pp) |
★★ 全服務行覆蓋:78.6% →
79.1%(9,921/12,535);★ 本批 +69 行;
1,261 測/0 失敗/0 錯誤/2 略過;掃描器全 0 命中。
★ 距 80% 尚差 +107 行(0.8 × 12,535 = 10,028)。★★ 動查才知的事:
PARAMETERS.param_value為 Oracle 原生JSON型別
(非VARCHAR2)——寫入須為合法 JSON 字串(否則ORA-40441);
★ 併查CONSTRAINT uq_param UNIQUE (namespace, param_key, effective_from)。★★★ 真機反駁(第七次)——我對「樂觀鎖衝突」之建構方式理解有誤:
我原以為「拿一個已落後之物件再呼叫withUpdated(...)」即為衝突;
實則相反——update之 SQL 為WHERE version = ?且實作傳version - 1,
故withUpdated之 +1 恰好把版本補成「DB 現行 + 1」→ 反而成功(測試假紅)。
★ 正解:衝突須以「同版本、不同內容」之物件再送(物件版本 = DB 現行版本 → 期望版本
= 現行 − 1 → 不命中)。
★ 併記:ErrorCodes.CC_PLT_5000之值為"CC-PLT-5000"(連字號)——
我原以常數名之底線當字面值,4 條斷言一起紅燈(測試自己的錯,與受測程式無關)。
★★ 兩者皆已回寫技能cardcore-jdbc-integration-test(陷阱 6/7)。
6.11.17 ★★★ 批次 11 執行結果(#A-203)——大戶 jdbc 收尾(WBS 5.25 結案)
| 項 | 內容 |
|---|---|
| 範圍 |
payment 29/installment 12/report 11/dispute 3/merchant 1 |
| 新增 | 擴充 3 個既有整合測試類別(payment/installment/report)+ 2 個新純單元測試(dispute/merchant 之 OracleRawIdTest)+ 1 個容器初始化腳本
|
| ★ 本批解決 | ★★ #A-200 之誠實略過項(JdbcAuthorizationSource,10 行) |
| 類別 | 前 | 後 |
|---|---|---|
JdbcPaymentRepository |
61/84 | ★ 84/84 → 100%
|
JdbcAutoDebitRepository |
35/41 | ★ 41/41 → 100%
|
JdbcInstallmentRepository |
61/73 | ★ 73/73 → 100%
|
JdbcReportJobRepository |
45/46 | ★ 46/46 → 100%
|
★★ JdbcAuthorizationSource(跨 schema) |
3/13(略過) | ★ 13/13 → 100%
|
dispute 之 OracleRawId
|
9/12 | ★ 12/12 → 100%
|
merchant 之 OracleRawId
|
11/12 | ★ 12/12 → 100%
|
| 服務 | 前行覆蓋 | 後 |
|---|---|---|
payment-service |
74.6% | ★ 81.0%(+6.4 pp) |
installment-service |
78.4% | ★ 82.3% |
dispute-service |
90.8% | 91.4% |
merchant-service |
93.4% | 93.6% |
report-service |
72.2% | 74.1% |
★★ 全服務行覆蓋:79.1% →
79.74%(9,995/12,535);★ 本批 +74 行;
1,275 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
★ 距 80% 尚差 +33 行(門檻 10,028 行)。
★★ 略過數 2 → 1——消失的那一個正是本批所解之JdbcAuthorizationSource。★★★ 本批最有價值之發現(方法論級):「環境不支援」往往是誤判——要先讀工具鏈的原始碼。
#A-200判定「容器之 app user 無 DBA → 無法建CC_AUTHschema → 誠實略過」。
本輪動查gvenzl/oracle-free映像之container-entrypoint.sh,發現
/container-entrypoint-initdb.d/*.sql係以sqlplus -s / as sysdba執行
(run_custom_scripts_recursive,逐字確認非推測)→ 改以
withCopyFileToContainer於初始化階段建立CC_AUTH(DDL 逐字抄自
authorization-service之V1__baseline.sql),並授予CC_RPT僅SELECT
(與正式環境一致)→ 略過項變成真機驗證。
★ 併查:createAppUser腳本對 app user 僅授DB_DEVELOPER_ROLE(不含
CREATE USER)——★ 此即#A-200失敗之真正原因,已載明。
★ fail-closed:若初始化腳本未生效,@BeforeAll直接失敗(不略過)——
否則「腳本沒跑」會被誤讀為「環境不支援」。★ 我方算錯(非缺陷):
JdbcAuthorizationSource之首版斷言把區間內群組數寫成 3,
實為 4(漏計 09-30 之 WEB 群組)。★ 真機以expected: <3> but was: <4>反駁。
6.11.18 ★★★ 批次 12 執行結果(#A-204)——B3-b-2:outbox 套件(★★ 行覆蓋突破 80%)
| 項 | 內容 |
|---|---|
| 範圍 |
KafkaEventPublisher 14/OutboxRelayScheduler 12/OutboxRelayConfig 2 = 28 行/服務
|
| 本批 |
limit-service + payment-service(★ 兩服務之三類別正規化後內容相同,僅 package/註解差異) |
| 新增 |
OutboxRelayIntegrationTest(6 測/服務) |
| 類別(每服務) | 前 | 後 |
|---|---|---|
KafkaEventPublisher |
0/14 | ★ 14/14 → 100%
|
OutboxRelayScheduler |
0/12 | ★ 12/12 → 100%
|
OutboxRelayConfig |
0/2 | ★ 2/2 → 100%
|
★ outbox 套件整體
|
0/28 | ★★ 28/28 → 100%
|
limit-service 行覆蓋 |
78.5% | ★ 80.7% |
payment-service 行覆蓋 |
81.0% | ★★ 85.0% |
★★★ 全服務行覆蓋:79.74% →
80.18%(10,051/12,535;★ 門檻 10,028 行,超出 23 行)
——★ 行覆蓋目標(80%)達成。★ 本批 +56 行;
1,287 測/0 失敗/0 錯誤/1 略過;掃描器全 0 命中。
⚠️ 分支覆蓋 63.78%(門檻 70%)——未達,見TRP-004 §7.2。★★ 本批之核心設計——「真機」而非 mock:以兩個真容器(Oracle + Kafka)
串起完整 relay 鏈路:outbox 落庫 → relay 撈取 → 送真 Kafka → 標記已發布,
★ 並自真實 Kafka 消費端讀回訊息比對(不是檢查「有沒有呼叫send」)。
★ 三個真實驗證要點:
① 端到端——主題慣例cc.<aggregateType>.<eventType>、分區鍵 =aggregateId
(同聚合有序)、發布後published_at落庫且不再為 pending。
② 失敗即拋——broker 不可達(指向無人監聽之埠)時send().get()同步拋錯
→ 若改 fire-and-forget,失敗會被靜默吞掉=假成功。
③ 例外不得外溢——@Scheduled拋例外將取消後續排程(#A-194之教訓);
★ 以真實JdbcOutboxRepository指向無人監聽之埠觸發真實失敗,斷言relay()不拋。
④ 中斷旗標須還原——Thread.currentThread().interrupt()後拋
IllegalStateException;★ 以linger.ms使.get()真正阻塞後中斷驗證。
★ 每測試用獨立聚合型別 → 獨立主題(避免跨測試之 Kafka 訊息互相干擾)。★ 可複製性:★ 另 10 個服務之
outbox套件仍為 0/28(authorization/card/
clearing/dispute/merchant/notification/posting/product/rewards/
statement)——配方已驗證,合計約 +280 行,為後續成本最低之批次。
6.11.19 ★★ 批次 13 計畫(#A-205)——outbox 橫向複製至其餘 10 服務
★ 本節為動工前之計畫(鐵律:規格先行)。★ 執行結果見 §6.11.20。
目標:把批次 12 已驗證之 OutboxRelayIntegrationTest 配方(§6.11.18)
橫向複製至其餘 10 個服務——預估 +280 行(28 行/服務),為成本最低之批次。
範圍(10 服務):authorization/card/clearing/dispute/merchant/
notification/posting/product/rewards/statement
(★ 已於批次 12 完成者:limit/payment)。
★★ 動工前盤點(實查,非推測)
| 服務 |
outbox 套件 |
JdbcOutboxRepository 套件 |
schema 帳號 | outbox 表遷移 |
|---|---|---|---|---|
authorization |
com.beam.cardcore.auth.infrastructure.outbox |
…auth.infrastructure.jdbc |
CC_AUTH |
V9__outbox.sql |
card |
…card.infrastructure.outbox |
…card.infrastructure.jdbc |
CC_CARD |
V8__outbox.sql |
clearing |
…clr.infrastructure.outbox |
…clr.infrastructure.jdbc |
CC_CLR |
V3__outbox.sql |
dispute |
…dis.infrastructure.outbox |
…dis.infrastructure.jdbc |
CC_DIS |
V3__outbox.sql |
merchant |
…mch.infrastructure.outbox |
…mch.infrastructure.jdbc |
CC_MCH |
V3__outbox.sql |
notification |
…notif.infrastructure.outbox |
…notif.infrastructure.jdbc |
CC_NOTIF |
V4__outbox.sql |
posting |
…posting.infrastructure.outbox |
…posting.infrastructure.jdbc |
CC_POSTING |
V3__outbox.sql |
product |
…product.infrastructure.outbox |
…product.infrastructure.jdbc |
CC_PRODUCT |
V2__outbox.sql |
rewards |
…rw.infrastructure.outbox |
…rw.infrastructure.jdbc |
CC_RW |
V5__outbox.sql |
statement |
…stmt.infrastructure.outbox |
…stmt.infrastructure.jdbc |
CC_STMT |
V3__outbox.sql |
★★ 一致性查證(逐項實查,非假設)
-
三類別之 API 完全一致(去註解後比對 12 服務):
KafkaEventPublisher(KafkaTemplate<String,String>)+publish(String,String,String);
OutboxRelayScheduler(OutboxPublisher,int)+relay();
OutboxRelayConfig()+outboxPublisher(OutboxStore,EventPublisher)。
★ 差異僅在 package 行與註解。 -
JdbcOutboxRepository之 API 完全一致:
JdbcOutboxRepository(JdbcTemplate)+
append(String,String,String,int,String,String)+findPending(int);
★ 皆implements OutboxPort, OutboxStore。 -
outbox表欄位完全一致(12 服務逐欄比對):
id/aggregate_type/aggregate_id/event_type/event_version/
payload/trace_id/published_at/retry_count/created_at。 -
testcontainers-kafka依賴:17 服務皆已宣告。
執行順序(分兩批,避免單一長任務逾時)
-
批次 13-a:
authorization/card/clearing/dispute/merchant(5 服務) -
批次 13-b:
notification/posting/product/rewards/statement(5 服務)
驗收標準
① 每服務 6 測全綠;② 每服務 outbox 套件 0/28 → 100%;
③ 全量 ./gradlew test --continue BUILD SUCCESSFUL、0 失敗/0 錯誤;
④ 掃描器(scan_scaffold/scan_mockdata/scan_faketest)全 0 命中;
⑤ ★ 實測值於實跑後填入 §6.11.20,不得預填。
6.11.20 ★★★ 批次 13 執行結果(#A-205)——outbox 橫向複製至其餘 10 服務
| 項 | 內容 |
|---|---|
| 範圍 | 10 服務 × 28 行(KafkaEventPublisher 14/OutboxRelayScheduler 12/OutboxRelayConfig 2) |
| 新增 |
OutboxRelayIntegrationTest × 10(6 測/服務,共 60 測) |
| 執行 | 分兩批(13-a/13-b,各 5 服務)——★ 避免單一長任務逾時 |
| 服務 | 前行覆蓋 | 後 | 服務 | 前行覆蓋 | 後 |
|---|---|---|---|---|---|
authorization |
59.4% | 61.6% | notification |
83.1% | 85.7% |
card |
89.4% | 92.4% | posting |
83.5% | 86.8% |
clearing |
81.5% | 86.0% | product |
82.4% | ★ 91.9% |
dispute |
91.4% | ★ 96.5% | rewards |
83.1% | 85.7% |
merchant |
93.6% | ★★ 98.9% | statement |
73.7% | 76.7% |
★★ 10 服務之
outbox套件皆0/28 → 100%;每服務恰 +28 行(與預估完全一致)。
★★★ 全服務行覆蓋:80.18% →82.42%(10,331/12,535;門檻 10,028,超出 303 行)
——★ 本批 +280 行。★ 分支覆蓋 63.78% →64.33%(門檻 70%,仍未達)。
1,347 測/0 失敗/0 錯誤/1 略過(+60 測);掃描器全 0 命中;
validate_sql70 檔 0 違規;validate_mermaid0 錯誤;
gen_service_skeleton --check缺漏 0;verify_versions12 項全過。★★ 本批之方法論(可複用)——「結構簽章抽取」先於複製:
動工前不以目視 diff 判斷「程式碼是否相同」(那會誤判——註解與空白各異),
而是以腳本抽取 API 簽章(去註解後之public宣告)逐服務比對:
① 三類別之建構子與公開方法完全一致;②JdbcOutboxRepository之
append/findPending簽章完全一致;③outbox表逐欄比對一致。
★ 據此才敢橫向複製——否則會做出編譯不過或語意不符之測試。★ 可再複製者:
messaging(limit89/authorization82)與client
(authorization137/statement74/posting47)——★ 惟需樁伺服器/Kafka 消費端
之新配方(樁伺服器非 mock),成本高於本批。
6.11.21 ★★ 批次 14 計畫(#A-206)——messaging 層(WBS 5.33)
★ 本節為動工前之計畫(鐵律:規格先行)。★ 執行結果見 §6.11.22。
目標:涵蓋 7 個 Kafka 消費者(約 151 行)。
| 服務 | 消費者 | 主題 | 委派之服務 |
|---|---|---|---|
limit |
PaymentPostedListener |
cc.payment.payment.posted |
PaymentPostedProjectionService |
limit |
PaymentReoccupyListener |
cc.payment.payment.unmatched/.reversed
|
PaymentReoccupyProjectionService |
limit |
PostingTransactionPostedListener |
cc.posting.transaction.posted |
SpecialLimitTempAdjustmentService |
limit |
StatementIssuedListener |
cc.statement.statement.issued |
SpecialLimitTempAdjustmentService |
auth |
AccountLimitChangedListener |
cc.limit.limit.adjusted |
AccountLimitProjectionService |
auth |
CardStatusChangedListener |
cc.card.card.status-changed |
CardStatusProjectionService |
auth |
LimitHoldListener |
cc.limit.limit.hold.created/.released
|
LimitHoldProjectionService |
★★ 配方(真機、無 mock)
★ consumer 之職責僅三件事:解析信封 → 委派服務 → 例外不得外拋
(SAD-004 §5.4:任何例外皆記 WARN 並略過;不得拋出,否則同群組後續訊息全數卡住)。
→ 故測試以真實服務 + 真實 JDBC 埠(Oracle 容器)串鏈,直接呼叫 onMessage(json),
再查 DB 驗證副作用。★ 不打 mock。
接線(實查後之依賴圖):
limit(cc_lim):
JdbcLimitRepository(jdbc)
JdbcSpecialLimitRepository(jdbc) / JdbcOutboxRepository(jdbc)
LimitApplicationService(limitRepo, specialLimitRepo, Clock.fixed, outbox)
JdbcSpecialLimitTempAdjustmentRepository(jdbc)
SpecialLimitTempAdjustmentService(limitRepo, tempRepo, limitAppService, Clock.fixed, 25, 7)
PaymentPostedProjectionService(JdbcPaymentPostedProjectionRepository(jdbc, limitRepo), tempService)
PaymentReoccupyProjectionService(JdbcPaymentReoccupyProjectionRepository(jdbc, limitRepo))
auth(cc_auth):
AccountLimitProjectionService(JdbcAccountLimitProjectionRepository(jdbc))
CardStatusProjectionService(JdbcCardStatusProjectionRepository(jdbc))
LimitHoldProjectionService(JdbcLimitHoldProjectionRepository(jdbc))
★★ 每個 consumer 之測試矩陣(防假綠)
| # | 情境 | 期望 |
|---|---|---|
| ① | 有效事件(完整信封) | 服務被呼叫、DB 副作用成立(查表驗證) |
| ② | 未知 eventType |
略過且不拋(★ 防「毒化消費者」——拋出會使後續訊息全卡) |
| ③ | 壞 JSON(非 JSON 字串) | 不拋(WARN + 略過) |
| ④ | 必填欄位缺漏/null |
不拋(穩健性;asText(null) 路徑) |
| ⑤ |
冪等(同 eventId 重送) |
不重複套用(投影服務之 consumed_events 守門) |
執行順序
-
批次 14-a:
limit(4 個 consumer) -
批次 14-b:
authorization(3 個 consumer)
驗收標準
① 全數測試綠;② messaging 套件行覆蓋顯著提升(★ 實測值後填);
③ 全量 BUILD SUCCESSFUL、0 失敗/0 錯誤;④ 掃描器全 0 命中;
⑤ ★ 實測值於實跑後填入 §6.11.22,不得預填。
6.11.22 ★★★ 批次 14 執行結果(#A-206)——messaging 層(WBS 5.33 結案)
| 項 | 內容 |
|---|---|
| 範圍 | 7 個 Kafka 消費者(limit 4+auth 3) |
| 新增 |
MessagingListenerIntegrationTest × 2(12 測+11 測,共 23 測) |
| 執行 | 分兩批(14-a:limit/14-b:authorization) |
| 套件 | 前 | 後 |
|---|---|---|
limit.infrastructure.messaging |
0/89 | ★★ 89/89 → 100%
|
auth.infrastructure.messaging |
0/82 | ★★ 82/82 → 100%
|
limit-service 行覆蓋 |
80.7% | ★ 89.1%(+105 行) |
authorization-service 行覆蓋 |
61.6% | ★ 72.7%(+144 行) |
★★★ 全服務行覆蓋:82.42% →
84.40%(10,580/12,535;門檻超出 552 行)
——★ 本批 +249 行(★ 遠超messaging套件本身之 171 行——協同效應:
端到端測試順帶走過application層之投影服務與額度服務)。
★ 分支覆蓋 64.33% →66.67%(門檻 70%,仍差 3.33 pp)。★★ 配方(
messaging層,可複用):★ 消費者之職責僅三件事——
解析信封 → 委派服務 → 例外不得外拋(SAD-004 §5.4)。
→ 測試以真實投影服務 + 真實 JDBC 埠 + 真 Oracle 串鏈,直接呼叫onMessage(json),
再查 DB 驗證副作用。★ 不打 mock。
★ 四項必驗:① 有效事件 → DB 副作用成立;② 未知eventType→ 略過且不拋
(★ 拋出會毒化消費者,同群組後續訊息全卡);③ 壞 JSON → 不拋;
④ 缺欄 → 不拋。★★★ 真機揭露(跨服務不一致):部分釋放佔用時——
limit-service之UPDATE_RELEASE_SQL為
CASE WHEN ? >= hold_amount THEN 'RELEASED' ELSE status END→ 維持HELD
(故 DDL 之PARTIALLY_RELEASED在本服務不可達);
★ 而authorization-service之讀模型
(JdbcLimitHoldProjectionRepository.applyRelease)對同一情境寫PARTIALLY_RELEASED。
★ 本計畫如實記錄,不預設其為缺陷(是否統一屬另案裁定)——★ 已寫入測試註解與本節。★ JaCoCo 之量測陷阱(本批釐清):
JacocoTaskExtension.append預設為true
→test.exec跨次累積 → ★ 以--tests過濾跑出之報告為「歷史聯集」,
不可當「單次結果」解讀(★ 本批實測:過濾跑之服務總數高於前次全量跑,
一度誤以為異常;★ 實為聯集,與全量跑結果一致)。
6.11.23 ★★ 批次 15 計畫(#A-207)——client 層(WBS 5.34)
★ 本節為動工前之計畫(鐵律:規格先行)。★ 執行結果見 §6.11.24。
目標:涵蓋 5 個服務間 HTTP 客戶端(約 232 行)。
| 服務 | 客戶端 | 端點 | 行數 |
|---|---|---|---|
authorization |
CardServiceHttpClient |
GET /api/v1/cards/lookup/{hash} |
34 |
authorization |
LimitServiceHttpClient |
GET limits/POST limit-holds/GET applicable/POST releases
|
85 |
statement |
PostingServiceClient |
GET balances/GET transactions(分頁) |
39 |
statement |
PostingInterestClient |
POST /api/v1/posting-interest/recalculate |
30 |
posting |
PricingServiceClient |
POST /api/v1/pricing/interest-calculations |
44 |
★★ 配方——本地樁 HTTP 伺服器(stub server)
★★ 「樁」非「mock」:樁伺服器是一個真實的 HTTP 伺服器(JDK 內建
com.sun.net.httpserver.HttpServer,零相依),模擬外部服務/同儕服務之行為;
★ 受測物(客戶端)與其協作者(ServiceClientHttpClient)皆為真實物件,
未以任何替身取代我方程式。
接線(實查):
ServiceClientProperties props = new ServiceClientProperties();
props.setConnectTimeout(Duration.ofSeconds(2));
props.setRequestTimeout(Duration.ofSeconds(2));
props.setMaxRetries(0); // ★ 避免重試拉長測試
ServiceClientHttpClient http = new ServiceClientHttpClient(props, new NoOpOutboundTokenProvider());
CardServiceHttpClient client = new CardServiceHttpClient(http, "http://localhost:" + stubPort);
★ 樁伺服器以 new InetSocketAddress(0) 取臨時埠(避免埠衝突)。
★★ 每個客戶端之測試矩陣(防假綠)
| # | 情境 | 期望 |
|---|---|---|
| ① | 200 + 完整封套 | 欄位逐一正確對映(含可空欄、UUID、LocalDate) |
| ② |
業務錯誤封套(success=false + error) |
拋已登錄碼(CC-PLT-5001/CC-PLT-5005/CC-STM-5001) |
| ③ | 非 200/非 404 | 拋已登錄碼 |
| ④ |
404(僅 CardServiceHttpClient) |
★ 回 Optional.empty()(正常業務結果,非故障) |
| ⑤ | 連線失敗(埠無監聽) | 拋已登錄碼,且原始例外須保留為 cause |
| ⑥ | ★ 請求內容(header/query/body) | 由樁伺服器記錄並斷言(★ 非只驗回應) |
| ⑦ | ★ PostingServiceClient 之分頁
|
首頁未滿即停;★ 滿頁則續抓;★ 逾上限拋錯(防帳單短收) |
驗收標準
① 全數測試綠;② 三服務之 client 套件行覆蓋顯著提升(★ 實測值後填);
③ 全量 BUILD SUCCESSFUL、0 失敗/0 錯誤;④ 掃描器全 0 命中;
⑤ ★ 實測值於實跑後填入 §6.11.24,不得預填。
6.11.24 ★★★ 批次 15 執行結果(#A-207)——client 層(WBS 5.34 結案)
| 項 | 內容 |
|---|---|
| 範圍 | 5 個服務間 HTTP 客戶端 |
| 新增 |
ServiceHttpClientIntegrationTest × 2(auth 13 測/stmt 10 測)+ PricingServiceClientIntegrationTest(posting 8 測)= 31 測
|
| 套件 | 前 | 後 |
|---|---|---|
auth.infrastructure.client(CardServiceHttpClient+LimitServiceHttpClient) |
3/137 | ★★ 134/137 → 98%
|
stmt.infrastructure.client(PostingServiceClient+PostingInterestClient) |
3/74 | ★★ 71/74 → 96%
|
posting.infrastructure.client(PricingServiceClient) |
3/47 | ★★ 44/47 → 94%
|
authorization-service 行覆蓋 |
72.7% | ★ 83.2%(+134 行) |
statement-service 行覆蓋 |
76.7% | ★ 84.3%(+71 行) |
posting-service 行覆蓋 |
86.8% | ★ 92.1%(+44 行) |
★★★ 全服務行覆蓋:84.40% →
86.39%(10,829/12,535;門檻超出 801 行)
——★ 本批 +249 行。
★ 未覆蓋之 9 行皆為mapper.writeValueAsString之 catch
(ObjectNode序列化實務上不會失敗)——不可達之防禦分支,如實記錄。★★ 配方(
client層,可複用)——「樁伺服器」不是「mock」:
以 JDK 內建com.sun.net.httpserver.HttpServer(零相依)起一個真實的 HTTP 伺服器,
模擬同儕服務之行為;★ 受測物與其協作者(ServiceClientHttpClient)皆為真實物件,
未以任何替身取代我方程式 → 驗的是真實 HTTP 往返(header/body/狀態碼/封套解析)。
★ 以new InetSocketAddress(0)取臨時埠(免埠衝突);★ 樁記錄請求供斷言。
★ 接線:new ServiceClientHttpClient(props, new NoOpOutboundTokenProvider()),
props.setMaxRetries(0)(測試不重試)。★★ 七項必驗:① 200 → 欄位逐一對映(含可空欄、
UUID、LocalDate);
② 業務錯誤封套(success=false)→ 拋已登錄碼且載明下游碼;
③ 非 200/404 → 拋;④ 404 →Optional.empty()(★ 正常業務結果,非故障);
⑤ 連線失敗 → 拋且保留 cause;⑥ 請求內容(header/query/body)由樁記錄並斷言;
⑦PostingServiceClient之分頁(★ 未滿即停/滿頁續抓/逾 20 頁拋錯——寧可中止不可短收)。★★ 量測陷阱(再確認):★ 以
--tests過濾跑後,服務總數不可信——
本批實測posting-service一度顯示 5.5%(僅該類別之執行資料)。
★ 一律跑全量後才讀服務總數;★ 類別/套件層級之數字不受影響。
6.11.25 ★★ 批次 16 計畫 + 結果(#A-208)——storage 層(WBS 5.35 結案)
★ 依鐵律先立計畫(目標/配方/必驗矩陣)後動工;★ 實測值於實跑後填入。
目標:涵蓋 report-service 與 statement-service 之 S3ObjectStorageAdapter(各 40 行)
+ S3StorageConfig(各 22 行)——★ 兩服務之 adapter 正規化後內容相同(僅 package/註解差異)。
★★★ 動查後之修正:用 Garage,不是 MinIO
| 項 | 查證結果 |
|---|---|
| 本專案之本地物件儲存 | ★ garage(ops/local/docker-compose.yml 之 garage 服務;ops/local/config/garage.toml) |
| MinIO 現況 | ★ 開源版已封存(官方下載站 410 Gone)、★ Docker Hub 映像已下架(本輪實測 docker pull minio/minio → pull access denied) |
| 處置 | ★ 以專案實際使用之同一映像與同一組態(dxflrs/garage:v2.4.1、ops/local/config/garage.toml)起容器——★ 比 MinIO 更忠實
|
★★ 配方(真機、無 mock)
new GenericContainer<>(DockerImageName.parse("dxflrs/garage:v2.4.1"))
.withCommand("/garage", "server", "--single-node", "--default-bucket")
.withEnv("GARAGE_DEFAULT_ACCESS_KEY", ...).withEnv("GARAGE_DEFAULT_SECRET_KEY", ...)
.withEnv("GARAGE_DEFAULT_BUCKET", "cardcore-artifacts")
.withCopyFileToContainer(MountableFile.forClasspathResource("garage/garage.toml"),
"/etc/garage.toml")
.withExposedPorts(3900);
// ★ 以「生產程式碼」S3StorageConfig().s3Client(...) 建構 S3Client——非測試自造
★ region 須與 garage.toml 之 s3_region(cardcore)一致,否則 SignatureDoesNotMatch。
★ 真實 S3 協定往返(含 SigV4 簽章、path-style)。
★★ 必驗矩陣
① put→get 逐位元組一致(含非 UTF-8 二進位與中文 UTF-8);
② 查無 → Optional.empty()(★ 不回空檔,SCS-017 §6.2 行為要求 5);③ 覆寫冪等;
④ put 失敗(bucket 不存在)→ 拋 CC-PLT-5001 + 保留 cause;
⑤ get 失敗(憑證錯誤 → 403)→ 拋(★ 不得誤判為「查無」);
⑥ 端點不可達 → 拋;⑦ S3StorageConfig 四項必填缺一即 fail-closed;
⑧ adapter 之 bucket 未設定即 fail-closed。
執行結果
| 套件 | 前 | 後 |
|---|---|---|
rpt.infrastructure.storage |
2/62 | ★★ 60/62 → 97%
|
stmt.infrastructure.storage |
2/62 | ★★ 60/62 → 97%
|
report-service 行覆蓋 |
74.1% | ★ 84.5%(+60 行) |
statement-service 行覆蓋 |
84.3% | ★★ 90.7%(+60 行) |
★★★ 全服務行覆蓋:86.39% →
87.35%(10,949/12,535;門檻超出 921 行)
——★ 本批 +120 行(2 × 60)。★ 兩服務之S3StorageConfig皆達100%。
★ 各剩 2 行未覆蓋=S3Exception之 404 分支
(★ Garage 回標準NoSuchKeyXML → 走NoSuchKeyException分支;
S3Exception+ 404 為相容其他實作之防禦分支)——如實記錄。
★ 20 測(每服務 10 測);★ 未以任何替身取代我方程式。
6.11.26 ★★★ 批次 17-a:application 層——party-service(WBS 5.36 之 1/4)
目標:涵蓋 party-service 之應用服務(AccountService 44/PartyFlagService 29/
PartyQueryService 31/PartyViewAssembler 22)。
★★ 配方(真機、無 mock)
以真實 JDBC 埠 + 真 Oracle + 真實 AES-GCM 加密器串起應用鏈路:
FieldEncryptor enc = new AesGcmFieldEncryptor(
StaticKeyProvider.of(EncryptionKey.of("k1", <32 bytes>))); // ★ 真金鑰,非替身
PartyViewAssembler assembler = new PartyViewAssembler(enc);
new AccountService(accounts, parties, idempotency, assembler);
new PartyFlagService(parties, idempotency, assembler);
new PartyQueryService(parties, accounts, assembler, cards);
★★★ 動查才知(三項,皆為我方之錯)
| # | 我方假設 | 實查結果 |
|---|---|---|
| 1 | 冪等表名為 idempotency_keys
|
★ 實為 pty_idempotency(V2__pty_idempotency.sql) |
| 2 |
Jdbc*Repository.save 可建列
|
★★ 實為純 UPDATE + 樂觀鎖(WHERE VERSION = version − 1)→ ★ 建列須以原生 SQL(version=0) |
| 3 |
Party.reconstitute 之可空參數序為 mobileEnc, emailEnc, idNoEnc
|
★★ 實為 idNoEnc, mobileEnc, emailEnc——★ 因皆為 byte[],編譯期不報錯,★ 只有真機斷言才會現形 |
必驗矩陣
① 冪等三態——同鍵同指紋=重放(回既有結果、不重複套用;★ 歷史不得再寫一筆);
同鍵異指紋=衝突(CC-PTY-2001,且不得改動資料);
② 遮罩而非明文——手機/信箱一律遮罩;★ 未加密(null)→ null;
★★★ 壞密文 → null 且不拋(不得外洩、不得使查詢整體失敗);
③ 查無 → CC-PTY-4040;④ getAccount 含該歸戶全體持卡人(依 party_no 升冪);
⑤ listCards(★ 會先驗歸戶)。
執行結果
| 類別 | 前 | 後 |
|---|---|---|
AccountService |
0/46 | 44/46 |
PartyFlagService |
0/31 | 29/31 |
PartyQueryService |
0/32 | 31/32 |
PartyViewAssembler |
0/22 | ★ 22/22 → 100%
|
★ party.application 整體
|
30/156 | ★★ 151/156 → 97%
|
★★ party-service
|
59.6% | ★★★ 91.5%(+189 行) |
★★★ 全服務行覆蓋:87.35% →
88.86%(11,138/12,535;門檻超出 1,110 行)
——★ 本批 +189 行(★ 單批增益第二大)。★ 14 測。
★★ 分支覆蓋 66.67% →69.80%(門檻 70%)——★ 僅差 0.20 pp!
★party-service由全庫最低之 59.6% 躍升至 91.5%——★ 本計畫之最大單服務改善。
6.11.27 ★★★ 批次 17-b:application 層——rewards-service(WBS 5.36 之 2/4)
| 類別 | 前 | 後 |
|---|---|---|
PointsExpiryService |
0/65 | 57/65 |
PointsViewAssembler |
0/13 | ★ 13/13 → 100%
|
PointsAccountService |
0/5 | ★ 5/5 → 100%
|
AccrualService |
0/14 | 7/14 |
★ rw.application 整體
|
289/387 | ★★ 357/387 → 92%
|
★★ rewards-service
|
85.7% | ★★ 93.5%(+85 行) |
★★★ 全服務行覆蓋:88.86% →
89.53%(11,223/12,535;門檻超出 1,195 行)
★★★ 分支覆蓋:69.80% →70.43%(門檻 70%)——★ 分支門檻正式達標!
★ 本批 +85 行;12 測。★★★ 真機發現兩項(皆為現行行為,如實記錄、不預設為缺陷):
- 同帳戶、同日到期之同階段桶 ≥2 → 到期批次拋出並中止:
分錄之冪等鍵為EXP:<account.id()>:<bucketStage>:<entryType>:<today>
——★ 非以桶為主體 → 第二桶之EXPIRE分錄鍵相同 →DuplicateKeyException
(非DomainException)→ ★catch (DomainException)攔不住 → 整批中止。
★★ 且中止後留下「已建新桶、但無分錄」之孤兒桶(資料完整性殘留)。
★ 觸發條件於業務上可能發生(同日兩筆累點 → 兩桶同到期日;索引非唯一)。- 首次累點(新帳戶)→
ORA-02291FK 失敗:
AccrualService.accrue於新帳戶路徑之順序為
upsertBucket(account.id(), …)先於saveAccount(updated)
→ 桶之 FK 找不到父項 → ★ 首次累點即失敗。
★ 兩項皆已寫入測試註解(duplicateStageBucketAbortsBatch/newAccountAccrualFailsOnFk),
待 BEAM PMO 裁定是否立 CR。★★ 我方之錯(4 項):① 分錄金額非 1:1(
金額 × 級別倍率);
②sourceTransactionId須為 UUID(非任意字串);
③getByAccountId之參數為歸戶accountId(非points_accounts.id);
④markExpired在寫分錄之前(中止點之精確位置)。
6.11.28 ★★ 批次 17-c:application 層——limit-service(WBS 5.36 之 3/4)
| 類別 | 前 | 後 |
|---|---|---|
LimitApplicationService |
25/46 | ★★ 45/46(餘 1 為不可達之防禦分支) |
SpecialLimitTempAdjustmentService |
9/36 | 30/36 |
LimitAdjustmentService |
46 | ⬜ 本批未執行(需 CallerIdentityPort,列後續) |
★ lim.application 整體
|
377/468 | ★ 408/468 → 87%
|
★ limit-service
|
89.1% | ★ 91.6%(+31 行) |
★★ 全服務行覆蓋:89.53% →
89.78%(11,254/12,535;門檻超出 1,226 行)
★★ 分支覆蓋:70.43% →70.73%(門檻 70%)。★ 本批 +31 行;12 測。★★ 必驗矩陣:①
query全欄位 + 可用額度為負時夾擠為 0(不得外洩負值);
② 5 參數hold委派至 6 參數版;③ 可用額度不足即拒絕(CC-LIM-1001,不得靜默放行);
④release查無佔用即拋;⑤ ★★ 專用池回沖失敗仍成功(釋放為權威事實,僅記 WARN);
⑥applicableSpecialLimit三分支(無命中/不足/適用);
⑦ 8-6 查無額度主檔 → 回 false 且不落串動紀錄;⑧ 8-7 回收不超過剩餘應收。★★ 動查才知(2 項,皆為我方之錯):①
special_limits.currency係V6新增且NOT NULL
——★ 種子須帶幣別;② ★ 回收後temporary_amount= 30,000 − 25,000 = 5,000(我原算成 10,000)。
6.11.29 ★★★ 批次 17-c-2:application 層——LimitAdjustmentService(#A-212)
| 類別 | 前 | 後 |
|---|---|---|
★★ LimitAdjustmentService
|
0/56 | ★★ 46/56(餘 10 為不可達之防禦分支) |
★ lim.application 整體
|
408/468 | ★★★ 448/468 → 96%
|
★★ limit-service
|
91.6% | ★★★ 95.6%(+51 行) |
★★★ 全服務行覆蓋:89.78% →
90.19%(11,305/12,535;門檻超出 1,277 行)
★★★ 分支覆蓋:70.73% →71.37%(門檻 70%)。★ 本批 +51 行;11 測。★★★ 配方之關鍵——真實身分來源:
CallerIdentityPort以真件
SecurityContextCallerIdentity(無參數建構、讀 Spring Security context);
★ 測試環境無認證 →current()回UNVERIFIED(fail-closed)——
★ 非以替身取代。★★★ 真機揭露(fail-closed 設計正確):
SPECIAL調整須先有
「可設定上限政策」(special_limit_policies,V8)——
★ 無政策即CC-LIM-1002拒絕(本批初版即因此失敗,★ 補種政策後通過)。
★ 政策以(role_code, segment_code, currency)為鍵;UNVERIFIED角色須顯式設定。★★ 必驗矩陣(額度調整狀態機):
① 增幅 ≤ 門檻 → 直接生效並即刻套用主檔;② 增幅 > 門檻 → 待覆核且不動主檔;
③APPROVE→ 套用;REJECT→ 結案不動主檔;④ 撤銷須回復原額度
(否則留下幽靈額度);⑤ 臨時額度到期 → 歸零 +EXPIRED;
⑥ ★★SPECIAL之基準為「現行有效之專用額度」= 0(非主檔永久額度)、
寫special_limits、不佔卡人額度、不發射事件(總額未變 → 噪音)。★★ 動查才知(1 項,我方之錯):
LimitAdjustmentView.id()回傳UUID(非String)。
6.11.30 ★★★ 批次 17-d:application 層——payment-service(WBS 5.36 之 4/4 ★ 結案)
| 類別 | 前 | 後 |
|---|---|---|
PaymentService |
0/41 | ★★ 41/41 → 100%
|
PaymentViewAssembler |
0/16 | ★★ 16/16 → 100%
|
AutoDebitService |
0/3 | ★★ 3/3 → 100%
|
PaymentEventFactory |
12/15 |
12/15(餘 3 為 unmatchedEnvelope 分支) |
★ payment.application 整體
|
92/187 | ★★★ 184/187 → 98%
|
★★ payment-service
|
85.0% | ★★★ 98.0%(+92 行) |
★★★ 全服務行覆蓋:90.19% →
90.92%(11,397/12,535;門檻超出 1,369 行)
★★★ 分支覆蓋:71.37% →71.64%(門檻 70%)。★ 本批 +92 行;8 測。★★ 配方——真 Oracle + 真實 JDBC 埠 + 樁 HTTP 伺服器:
PostingBalancePort以真件PostingServiceClient+ JDK 內建HttpServer樁
(零相依)模擬同儕服務posting-service之餘額端點;
★ 受測物與協作者皆為真實物件,★ 未以任何替身取代我方程式。★★ 必驗矩陣:
① 首次受理 → 分配落庫 + 入帳 + 額度回復 + 事件同交易;
② ★★★ 查無分戶(404)→ 全額歸本金 + 標記未匹配(★ 非故障、不拋);
③ 冪等受理 → 回前次結果、不重複入帳;
④ 非法識別碼/日期 →CC-PAY-4000(★ 不得落到 500);
⑤ ★★★ 沖正 → 發射reversed事件(★ 否則limit-service之可用額度永久高估);
⑥ 沖正守門(查無CC-PAY-4040/已沖正CC-PAY-3001);⑦ 自扣遮罩(★ 實測 6 星 + 末 4 碼)。★★ 動查才知(1 項,我方之錯):自扣帳號之遮罩格式實為
******7890(6 星),非 4 星。
6.11.31 ★★★ 批次 20 計畫:platform/ 8 模組覆蓋率(WBS 5.37)
★ 緣起:
#A-216揭露雙口徑——TRP-003/TRP-004之「達標」為 17 服務口徑
(行 90.92%/分支 71.64%);而verify_nfr以全 backend計(17 服務 + 8 平台模組)
為行 87.42%/分支 68.98%。★ 若客戶以「全 backend」計,分支門檻(70%)尚未達標。
本批次即為關閉此口徑落差。
① 盤點(2026-10-06 實測,逐類別)
★ 量測法:解析各模組
build/reports/jacoco/test/html/之index.html(<tfoot>之
bar為「missed of total」格式,非title=屬性——兩種格式並存,須分別處理)。
★ 報告時戳 2026-10-05 11:39–11:44,且src/main無任何檔案新於報告 → 報告為最新。
| 模組 | 未覆蓋行/總行 | 行% | 未覆蓋分支/總分支 | 分支% |
|---|---|---|---|---|
bff-web |
316/452 | 30.1% | 118/136 | 13.2% |
api-gateway |
166/280 | 40.7% | 63/126 | 50.0% |
common-security |
104/393 | 73.5% | 73/176 | 58.5% |
common-spring |
67/530 | 87.4% | 71/266 | 73.3% |
common-saga |
36/416 | 91.3% | 46/136 | 66.2% |
common-domain |
30/184 | 83.7% | 14/82 | 82.9% |
common-batch |
25/101 | 75.2% | 13/26 | 50.0% |
common-event |
1/77 | 98.7% | 4/36 | 88.9% |
| 合計 | ★ 745/2,433 | 69.4% | ★ 402/984 | 59.1% |
★ 與 #A-216 之記載逐數字吻合(745/402)——盤點正確。
② ★★ 關鍵算術(達標之精確門檻)
| 口徑 | 行 | 分支 |
|---|---|---|
| 17 服務 | 11,397/12,535 = 90.92% | 2,607/3,639 = 71.64% |
platform/ 8 模組 |
1,688/2,433 = 69.4% | 582/984 = 59.1% |
| ★ 全 backend | 13,085/14,968 = 87.42% | 3,189/4,623 = 68.98% |
★★ 全 backend 分支達 70% 之精確需求:
4,623 × 0.70 = 3,236.1
→ 已覆蓋須 ≥ 3,237 → 現 3,189 → ★ 尚需再覆蓋 48 條分支。
★ 即:只需platform/402 條未覆蓋分支中之 12%,即可使「全 backend」分支達標。
★ 行覆蓋(87.42%)早已遠超 80%,非本批次之約束。
③ 批次排序(依「每條分支之成本」由低到高)
| 批次 | 範圍 | 未覆蓋 行/分支 | 配方 | 成本 |
|---|---|---|---|---|
| 20-a |
common-domain+common-batch+common-event
|
56/31 | ★ 純單元(零相依、無容器) | ★ 最低 |
| 20-b | bff-web |
316/118 | ★ 樁 HTTP 伺服器(JDK 內建,非 mock)模擬下游服務 | 中 |
| 20-c | api-gateway |
166/63 | ★ Spring WebFlux 過濾器/路由設定(真物件) | 中 |
| 20-d | common-security |
104/73 | ★ JWT 真實簽發/驗證(JwtSigner + JwtValidator) |
中 |
| 20-e | common-spring |
67/71 | ★ 真實 Spring context(@SpringBootTest 切片) |
中高 |
| 20-f | common-saga |
36/46 | ★ 真 Oracle(JdbcSagaStore)/狀態機純單元 |
中 |
★★ 排序理由:先做 20-a(零相依、最快建立「已達標」之事實),
再做 20-b(單一模組即可貢獻 118 條分支,遠超所需 48 條,一擊達標)。
④ 驗收標準(不可放寬)
- ★★ 全 backend 分支 ≥ 70%(主目標,需 +48 條)。
- ★ 全 backend 行 ≥ 80%(早已達成,不得回退)。
- ★ 逐模組不得下降;★ 未覆蓋者逐條記錄並標明「不可達之防禦分支」或「待補」。
- ★★ 零 mock:受測物與其協作者皆為真實物件;模擬外部/同儕服務一律用
樁伺服器(com.sun.net.httpserver.HttpServer)或真容器。 - ★ 掃描器全 0 命中(
scan_scaffold/scan_mockdata/scan_faketest)+
validate_sql/validate_mermaid0 違規。 - ★ 驗證結果一律等實跑完再填(★ 不得預填)。
⑤ 已知陷阱(本專案已踩過,勿再踩)
- ★ JaCoCo 報告有兩種
Total格式——模組層為bar之"N of M"文字、
套件/類別層為bar內img title=;★ 解析器必須分別處理,
否則會靜默取到 0(本輪首版即因此得ZeroDivisionError)。 - ★ 類別列之
ctr1/ctr2序為[instr%, branch%, cxtyM, cxtyT, lineM, lineT, methM, methT, clsM, clsT]
→ ★ 行數取索引 4/5,不是 2/3(2/3 是複雜度,會誤讀)。 - ★
</tfoot>在<tbody>之前(JaCoCo HTML 之實際順序)→ 以</tfoot>為tbody之上界會切出空字串。 - ★ macOS
grep之\|失效 → 交替比對一律grep -E。
6.11.32 ★★ 批次 20-a:common-domain/common-batch/common-event(#A-217)
| 模組 | 前(行/分支) | 後 |
|---|---|---|
common-domain |
30/184 = 83.7%/14/82 = 82.9% | ★★ 0/184 = 100%/0/82 = 100%
|
common-batch |
25/101 = 75.2%/13/26 = 50.0% | ★★ 0/101 = 100%/0/26 = 100%
|
common-event |
1/77 = 98.7%/4/36 = 88.9% | ★★ 0/77 = 100%/0/36 = 100%
|
★★ 本批 +56 行、+31 分支;
platform/未覆蓋 745 → 689 行、402 → 371 分支。
★★ 全 backend:行 87.42% →87.79%;分支 68.98% →69.65%(距 70% 僅差 0.35 pp)。★ 配方——純單元、零相依、零替身:六個新增測試類別/47 測,
受測物皆為真實值物件/例外/record(CSP-005)。
①ValueObjectSemanticsTest——Money.negate/符號述詞兩分支、
LimitAmount.settle之拒絕路徑、equals三項短路鏈之每一段、
CardNumber之 Luhn 折減分支(d > 9 → d -= 9,以業界公認測試卡號5555555555554444);
②ExceptionOverloadsTest——5 個子類別 × 3 個多載 = 15 條建構路徑;
③ScheduledBatchRunnerTest——★ 單一工作失敗不得影響其他工作(排程配接器必須逐工作 try/catch);
④BatchOrchestratorEdgeTest——chunkSize檢核、項目提早耗盡須標記完成、
loadItems回null之防禦分支、findIncomplete過濾;
⑤EventEnvelopeCompatibilityTest——schemaRef四條預設路徑、isCompatibleWith四種組合。★★ 真機反駁(我方之錯,2 項):
①Set.of(a, b)對重複元素會拋IllegalArgumentException——我原用它建集合以驗「兩者相等」,
邏輯自相矛盾(測試自身的錯);
②LimitAmount.toString()以主單位呈現(8.50 TWD),我原斷言"850"(誤以為最小單位)。★ 殘餘未覆蓋:0——三模組行與分支皆
100%。
6.11.33 ★★★ 批次 20-b:bff-web(#A-218)★★ 關閉「全 backend」口徑落差
| 類別/模組 | 前 | 後 |
|---|---|---|
BatchMonitorService |
101/101 未覆蓋 | ★ 未覆蓋 0(餘 2 為逾時 catch,見下) |
BusinessStatsService |
56/56 | ★ 未覆蓋 0(餘 3 為逾時 catch) |
EndpointManagementService |
55/55 | ★ 未覆蓋 0(餘 2 為健康聚合例外 catch) |
AccountOverviewClientImpl |
36/36 | ★ 未覆蓋 0 |
DashboardSourcesAdapter |
10/10 | ★ 未覆蓋 0 |
| 5 個控制器 | 各 5 | ★ 未覆蓋 0 |
BffHealthProperties/HealthTargetsConfigImpl/BffHealth/BffBatchJob
|
共 20 | ★ 未覆蓋 0 |
★★ bff-web 整體
|
行 316/452 = 30.1%/分支 118/136 = 13.2% | ★★★ 行 12/452 = 97.3%/分支 4/136 = 97.1%
|
★★★ 本批 +304 行、+114 分支——★ 單一批次之最大增益。
★★★platform/8 模組:行 69.4% →84.18%;分支 59.1% →73.88%。
★★★ 全 backend:行 87.79% →89.82%;分支 69.65% →72.12%
——★★★ 「全 backend」之兩項門檻(行 ≥80%/分支 ≥70%)首次雙雙達標
(分支門檻超出 90 條)。★ 本批 5 個測試類別/62 測(★ 全模組 206 測/0 失敗/0 錯誤/0 略過)。★★ 配方——樁 HTTP 伺服器(★ 非 mock):
新增DownstreamStub(JDK 內建HttpServer,零相依)模擬
bff-web 之四個下游同儕(statement/clearing/migration/report)
與 gateway/card/authorization/dispute;
★ 受測物(各聚合服務)與其協作者(ServiceClientHttpClient)皆為真實物件,
未以任何替身取代我方程式 → 驗的是真實 HTTP 往返(路徑、查詢字串、狀態碼、封套解析)。
★ 控制器亦以真實聚合服務注入——★ 若以假服務注入,測的是假服務。
★ 惟AccountOverviewPort/DashboardSourcesPort/HealthTargetsPort等埠介面
依其設計文件(HealthTargetsPort之類別說明:「立埠後可用手寫 fake 測試」)以手寫 fake 實作。★★ 必驗矩陣:
① ★★DIFFERENCE歸WARNING而非SUCCEEDED——對帳有差異帳務上不等於成功,
歸成功會使差異被忽略(假綠);
② ★ 部分失敗不拖垮整體(非 200/連不上 →unavailable[]並帶原因);
③ ★ 超界 limit 顯式報錯(CC-PLT-4003),不靜默夾取;
④ ★ 欄位別名鏈(runId↔jobId、txCount↔processedCount、createdAt↔startedAt)
——★ 取不到者留null,不以 0 填充;
⑤ ★ 目標未組態時該子區塊null且「完全不發出請求」(連打都不打);
⑥ ★★ 保留下游原錯誤碼(CC-PTY-4040)——聚合層不轉碼;
⑦ ★ 下游回應非 JSON →CC-PLT-5005(不得誤判為查無);
⑧ ★cards之下游為party(非card-service);★routeId無法對映者保留原文;
⑨ ★BffHealth空清單/null →DOWN(不樂觀預設為 UP)。★★ 真機反駁(我方之錯,1 項):
Map<?, ?>之型別捕獲使containsEntry無法編譯
——★ 改以Map<String, Object>轉型(編譯期即攔下)。★ 殘餘未覆蓋 12 行/4 分支(逐條如實記錄):
逾時 catch 分支 8 行(BatchMonitorService2/BusinessStatsService3/
EndpointManagementService2/HealthAggregationService1)
——★ 觸發條件為「future 之等待逾時」,需刻意製造極端時序,屬防禦性分支;
HealthAggregationService之「健康聚合自身拋例外」1 行——★ 同上;
其餘 3 行為AccountOverviewService之下游呼叫失敗 catch(需與UNAVAILABLE路徑併測)。
6.11.34 ★★ 批次 20-c:api-gateway(#A-219)
| 類別/模組 | 前 | 後 |
|---|---|---|
CycleSplitGlobalFilter |
36/36 未覆蓋 | ★ 未覆蓋 0 |
CycleSplitConfigAdapter |
51/107 | ★ 未覆蓋 0 |
SplitRatioController |
22/22 | ★ 未覆蓋 0 |
CycleSplitProperties |
21/21 | ★ 未覆蓋 0 |
GatewaySecurityConfig |
13/13 | ★ 未覆蓋 0 |
GatewayRateLimitConfig |
11/11 | ★ 未覆蓋 0 |
GatewayRouteController |
7/7 | ★ 未覆蓋 0 |
CycleSplitDecision/GatewayRoute/SplitRatioConfigPort 等 |
共 5 | ★ 未覆蓋 0 |
★★ api-gateway 整體
|
行 166/280 = 40.7%/分支 63/126 = 50.0% | ★★ 行 3/280 = 98.9%/分支 6/126 = 95.2%
|
★★ 本批 +163 行、+57 分支;6 個測試類別/51 測(★ 全模組 79 測/0 失敗/0 錯誤/0 略過)。
★★platform/8 模組:行 84.18% →90.88%;分支 73.88% →79.67%。
★★ 全 backend:行 89.82% →90.91%;分支 72.12% →73.35%。
★ 掃描器全 0 命中。★★ 配方——真實框架介面 + 真件之子類(非 mock 框架):
①CycleSplitGlobalFilter以 Spring 官方MockServerWebExchange(真實介面實作)
+GatewayFilterChain函式介面 lambda(記錄是否續鏈)驅動;
②CycleSplitConfigAdapter以@TempDir真實暫存檔 +MockEnvironment驗檔案往返;
③ 路由查詢用 Spring Cloud Gateway 真實InMemoryRouteDefinitionRepository;
④ContextRefresher為 Spring Cloud 之abstract類別 → 以最小子類提供可觀察行為。★★★ 真機反駁我方之假設(4 項,皆為我方之錯):
① ★ServerHttpSecurity.http()無 bean factory →oauth2ResourceServer().jwt(...)
取不到 decoder,拋IllegalArgumentException: jwtDecoder cannot be null;
★ 正解:以最小子類注入ApplicationContext(ServerHttpSecurity()建構子為protected,
Spring Security 自身之ContextAwareServerHttpSecurity即同法)。
② ★★ WebFlux 解析的是ReactiveJwtDecoder,不是 servlet 版JwtDecoder
——★ 註冊錯型別得NoSuchBeanDefinitionException。
③ ★assertThat(exchange.getAttribute(...))之型別歧義——getAttribute為泛型方法
→ 與 AssertJ 之assertThat(IntPredicate)/assertThat(Predicate)同時匹配;
★ 正解:先轉型為具體型別再斷言。
④ ★★GatewayRateLimitConfig之name != null為死碼——★Mono.map(Principal::getName)
回null時 Reactor 直接拋 NPE(Reactor 規格禁止 null 元素)→ 該分支不可達;
★ 以空白字串方可穩定觸發「退回 IP」路徑。★ 殘餘未覆蓋 3 行/6 分支(逐條如實記錄,皆為不可達之防禦分支):
CycleSplitDecisionL64-65(NoSuchAlgorithmException——MD5 為 JVM 規格必備);
GatewayRateLimitConfigL30(name != null,Reactor 禁止 null)、
L33(remote.getAddress() == null——位址物件存在時其 address 必非 null);
CycleSplitConfigAdapterL65(parent != null/isWritable為 false——絕對路徑必有父目錄)、
L134-135(剝除前綴後為空——該情形必先因「不含 cycle-split」而略過)、
L226(i < line.length()為 false——全空白行已於前一步被略過)。
6.11.35 ★★★ 批次 20-d:common-security(#A-220)★★ 揭露並修補真缺陷(CR-042)
| 類別/模組 | 前 | 後 |
|---|---|---|
JwtAuthFilter |
45/45 未覆蓋 | ★ 未覆蓋 0 |
JwtValidator |
18/91 | ★ 未覆蓋 0(餘 2 為不可達之通用 catch) |
AesGcmFieldEncryptor |
11/41 | ★ 未覆蓋 0(餘 6 為不可達之 catch) |
CurrentUser |
11/11 | ★ 未覆蓋 0 |
AuthorizationClaim |
7/23 | ★ 未覆蓋 0 |
EncryptionKey |
7/19 | ★ 未覆蓋 0 |
JwtSigner/JwtValidationException/CryptoProperties/StaticKeyProvider/PiiMasker
|
共 5 | ★ 未覆蓋 0(PiiMasker 餘 1 為不可達) |
★★ common-security 整體
|
行 104/393 = 73.5%/分支 73/176 = 58.5% | ★★ 行 8/395 = 98.0%/分支 3/176 = 98.3%
|
★★ 本批 +96 行、+70 分支;★ 8 個測試類別/86 測(★ 全模組 137 測/0 失敗/0 錯誤/0 略過)。
★★platform/8 模組:行 90.88% →94.83%;分支 79.67% →86.79%。
★★ 全 backend:行 90.91% →91.56%;分支 73.35% →74.86%。★ 掃描器全 0 命中;
★ 全 backend 編譯驗證通過(修補後 71 tasks 全 up-to-date)。★★★ 本批之最大價值:揭露並修補一個真缺陷(
CR-042)
——★★JwtAuthFilter.sendUnauthorized()必拋NullPointerException:
原以java.util.Map.of("data", (Object) null, …)組封套,
★ 而Map.of對「任何 null 之鍵或值」皆拋 NPE(ImmutableCollections.MapN之requireNonNull)。
★★ 即:本過濾器自建立以來,其核心職責(未認證 → 401)從未真正執行過一次。
★ 查證影響範圍:grep -rl全庫 → ★JwtAuthFilter零接線(僅 Javadoc 提及)
→ ★ 不影響現行系統;★ 惟其為common-security之公開 API,
★ 一旦依文件接線,所有 401 立即變成 500 → ★ 仍須修(依CR-041 #A-184同一標準)。
★ 修法:改用LinkedHashMap承載(JSON 形狀不變,data仍為null)。
★★ 修補前 14 測中 11 測紅燈 → 修補後 14 測全綠。★★ 配方——真實框架介面 + 真實密碼學運算(★ 非 mock):
①JwtAuthFilter以 Spring 官方MockHttpServletRequest/MockHttpServletResponse
+FilterChain函式介面 lambda;★ token 由真實JwtSigner簽發、真實JwtValidator驗證;
② 對「格式錯誤」之 token,以自算 HMAC 手工組裝(★ 是真實的密碼學運算,非 mock);
③AesGcmFieldEncryptor以真實StaticKeyProvider+ 真實 AES-256-GCM;
④ 對「金鑰來源拋錯」以手寫KeyProvider(本專案自有之埠介面)。★★★ 真機反駁我方之錯(1 項):
AesGcmFieldEncryptorEdgeTest之「竄改密文」初版改動 Base64 末字元——
★ 其低 2 位被丟棄 → 有時不改變任何 byte → 認證反而通過
(★ 與#A-163所載之 6.25% 間歇性失敗同型,本輪再次踩到);
★ 已改為改動最後一段之中間字元(全位元有效)。★ 殘餘未覆蓋 8 行/3 分支(逐條如實記錄,皆為不可達之防禦分支):
AesGcmFieldEncryptorL60-63(encrypt 之catch——★currentKey()在 try 之外,
且Cipher.getInstance不可能失敗)、L93-94(decrypt 之catch (CryptoException)——
★ 金鑰查詢在 try 之外,try 內無任何路徑可拋CryptoException);
JwtValidatorL124-125(通用catch——Mac.getInstance不可能失敗)、
L192(a == null || b == null——★decodeB64ToBytes永不回 null);
PiiMaskerL149(repeat之times <= 0——★ 兩個呼叫點皆已先擋掉)。
6.11.36 ★★ 批次 20-e:common-spring(#A-221)
| 類別/模組 | 前 | 後 |
|---|---|---|
GlobalExceptionHandler |
12/76 | ★ 未覆蓋 0(餘 9 為不可達:私有巢狀類別/登錄表必有值) |
ClientCredentialsTokenProvider |
12/62 | ★ 未覆蓋 0(餘 5:並發路徑/InterruptedException) |
SecurityProperties.Outbound |
8/31 | ★ 未覆蓋 0 |
ServiceCallerAclManager |
7/27 | ★ 未覆蓋 0 |
ServiceAcl |
6/47 | ★ 未覆蓋 0(餘 2 為 IOException) |
SecurityAutoConfiguration/TraceContext/ApiResponse/JwtRoleConverter/RelayingOutboundTokenProvider 等 |
共 17 | ★ 未覆蓋 0 |
★★ common-spring 整體
|
行 67/530 = 87.4%/分支 71/266 = 73.3% | ★★ 行 27/530 = 94.9%/分支 39/266 = 85.3%
|
★ 本批 +40 行、+32 分支;★ 7 個測試類別/56 測(★ 全模組 151 測/0 失敗/0 錯誤/0 略過)。
★★ 配方:真實框架介面 + 真實例外物件 + 真實 IdP 樁——
MockHttpServletRequest系列(此批未用)/直接以真實 Spring 例外驅動處理器(★ 非 MockMvc 替身)/
JDK 內建HttpServer起真實 IdP(★ 模擬外部身分提供者,非 mock 我方程式)。★★★ 真機反駁我方之假設(5 項,皆我方之錯):
① ★TraceContext.MDC_KEY實為trace_id(非traceId);
② ★CC-PLT-5002(Saga 補償失敗)映射500(非 503)——★ 狀態由SDD-006 §3.4登錄表決定,不憑語感;
③ ★ Spring 7 之NoResourceFoundException(method, message, resourcePath)(★ 參數序與我原假設相反);
④ ★★ Keycloak 之 realm 角色在巢狀realm_access.roles(非頂層roles);
⑤ ★★new JwtAuthenticationToken(jwt)之isAuthenticated()為false
(AbstractAuthenticationToken預設),★ 且無授權項時setAuthenticated(true)會拋錯。★ 殘餘未覆蓋 27 行/39 分支(逐條如實記錄):
GlobalExceptionHandler(9 行)——★ConstraintViolationException分支(★ 本模組未引入
Bean Validation 實作,jakarta.validation-api僅有介面 → 無法構造真實違規物件)、
★HandlerMethodValidationException(同上,需真實參數名)、
★requestError之 fallback(★ 所有呼叫端之錯誤碼皆已登錄 → 不可達)、
★EnvelopeAuthenticationEntryPoint(私有巢狀類別,需真實 HTTP 401 流程);
ClientCredentialsTokenProvider(5 行)——★★ 雙重檢查鎖之內部快速路徑
(★ 僅並發競爭時可達,單執行緒不可達)+InterruptedException(需中斷執行緒);
ServiceAcl(2 行)——IOException(ClassPathResource之讀取失敗);
SecurityAutoConfiguration(4 行)——spring.application.name未設定(需無該屬性之 context)
+wwwAuthenticateOf(私有巢狀)。
6.11.37 ★★ 批次 20-f:common-saga(#A-222)★ WBS 5.37 結案
| 類別/模組 | 前 | 後 |
|---|---|---|
StateMachine |
4/32 | ★ 未覆蓋 0 |
JdbcSagaStore |
16/138 | ⚠️ 本批未執行(需 Oracle;★ 已有 JdbcSagaStoreIntegrationTest,列後續) |
SagaCoordinator |
3/132 | ★ 未覆蓋 0(餘 3 為不可達分支) |
Transition/SagaInstance/SagaStepRecord/InMemorySagaStore/列舉/SagaResult
|
共 17 | ★ 未覆蓋 0 |
★★ common-saga 整體
|
行 36/416 = 91.3%/分支 46/136 = 66.2% | ★★ 行 19/416 = 95.4%/分支 17/136 = 87.5%
|
★ 本批 +17 行、+29 分支;★ 2 個測試類別/27 測(★ 全模組 50 測/0 失敗/0 錯誤/0 略過)。
★★platform/8 模組:行 69.4% →97.17%;分支 59.1% →92.99%。
★★ 全 backend:行 91.56% →91.94%;分支 74.86% →76.18%。★ 掃描器全 0 命中。
★★★WBS 5.37(platform/8 模組)結案。★★★ 真機反駁我方之假設(1 項):★
SagaInstance之長度截斷在「工廠/衍生方法」
(start()/withCurrentStep()/withFailureCode()),★ 不在 record 之建構子——
★ 且MAX_ERROR_MESSAGE為 2000(非我假設之 256)。
★ 教訓:★ 「值物件之截斷」須查「哪一個入口」——建構子只管requireNonNull,
截斷可能只在下游入口。★ 殘餘未覆蓋 19 行/17 分支:★
JdbcSagaStore(16 行)需 Oracle
(★ 已有整合測試,惟需容器;★ 列後續);其餘 3 行為SagaCoordinator之防禦分支。
7. 修訂記錄
| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 3.13 | 2026-10-06 | ★★★ 新增 §6.11.36/§6.11.37 批次 20-e/20-f(#A-221/#A-222)——★★★ WBS 5.37 結案:★★ common-spring 行 87.4% → 94.9%/分支 73.3% → 85.3%(+40 行/+32 分支,7 類別/56 測);★★ common-saga 行 91.3% → 95.4%/分支 66.2% → 87.5%(+17 行/+29 分支,2 類別/27 測)。★★★ platform/ 8 模組:行 69.4% → 97.17%、分支 59.1% → 92.99%;★★ 全 backend:行 91.94%、分支 76.18%。★★★ 真機反駁我方 6 項(MDC_KEY 實為 trace_id/CC-PLT-5002 映射 500 非 503/Spring 7 NoResourceFoundException 之參數序/Keycloak realm 角色在 realm_access.roles/JwtAuthenticationToken 預設 isAuthenticated=false/截斷在工廠方法非建構子)。★ 殘餘 46 行/56 分支逐條記錄(★ 多為「需 Oracle/Bean Validation 實作/私有巢狀/並發路徑」)。★ 掃描器全 0 命中
|
| 3.12 | 2026-10-06 | ★★★ 新增 §6.11.35 批次 20-d(#A-220)——common-security;★★ 併立並執行 CR-042:★★ 行 73.5% → 98.0%、分支 58.5% → 98.3%(+96 行/+70 分支);8 類別/86 測(全模組 137 測/0 失敗)。★★ platform/ 8 模組:行 → 94.83%、分支 → 86.79%;★★ 全 backend:行 → 91.56%、分支 → 74.86%。★★★ 揭露真缺陷並修補(CR-042):JwtAuthFilter.sendUnauthorized() 必拋 NPE——★ Map.of 不接受 null 值,而封套之 data 就是要為 null → ★★ 該過濾器自建立以來「未認證 → 401」從未真正執行過;★ 查證全庫零接線(不影響現行系統,但為公開 API,接線即 500)→ ★ 改用 LinkedHashMap(JSON 形狀不變)。★ 修補前 14 測中 11 紅燈 → 修補後全綠。★★ 配方:MockHttpServletRequest/MockHttpServletResponse + FilterChain lambda + 真實 JwtSigner/JwtValidator + 自算 HMAC 手工組裝。★★★ 真機反駁我方 1 項:竄改 Base64 末字元低 2 位被丟棄 → 認證反而通過(★ #A-163 同型陷阱再次踩到)。★ 殘餘 8 行/3 分支逐條記錄為不可達。★ 全 backend 編譯驗證通過 |
| 3.11 | 2026-10-06 | ★★ 新增 §6.11.34 批次 20-c(#A-219)——api-gateway:★★ 行 40.7% → 98.9%、分支 50.0% → 95.2%(+163 行/+57 分支);6 類別/51 測(全模組 79 測/0 失敗)。★★ platform/ 8 模組:行 → 90.88%、分支 → 79.67%;★★ 全 backend:行 → 90.91%、分支 → 73.35%。★★ 配方:MockServerWebExchange(真實介面實作)+ GatewayFilterChain lambda + @TempDir 真實檔案 + InMemoryRouteDefinitionRepository(真件)+ ContextRefresher 最小子類。★★★ 真機反駁我方 4 項(ServerHttpSecurity.http() 無 bean factory/WebFlux 解析 ReactiveJwtDecoder 非 JwtDecoder/getAttribute 泛型致 AssertJ 歧義/name != null 為死碼——Reactor 禁止 map 回 null)。★ 殘餘 3 行/6 分支逐條記錄為不可達之防禦分支
|
| 3.10 | 2026-10-06 | ★★★ 新增 §6.11.31 計畫 + §6.11.32 批次 20-a + §6.11.33 批次 20-b——platform/ 8 模組(WBS 5.37):★★★ 全 backend 行 87.42% → 89.82%、分支 68.98% → 72.12%——兩項門檻首次雙雙達標(分支超出 90 條)。★ 20-a:common-domain/common-batch/common-event 三模組行與分支皆 100%(+56 行/+31 分支)。★★★ 20-b:bff-web 行 30.1% → 97.3%、分支 13.2% → 97.1%(+304 行/+114 分支,單批最大增益);platform/ 8 模組 → 行 84.18%/分支 73.88%。★★ 配方:新增 DownstreamStub(JDK 內建 HttpServer,零相依)模擬下游同儕;★ 受測物與協作者皆為真實物件(非 mock);★ 控制器以真實聚合服務注入。★★ 必驗:DIFFERENCE 歸 WARNING(否則差異被忽略=假綠)/超界 limit 顯式報錯不夾取/保留下游原錯誤碼/未組態即不發請求。★★ 真機反駁我方之錯 3 項(Set.of 重複元素拋錯/toString 為主單位/Map<?,?> 型別捕獲)。★ 掃描器全 0 命中。★ 殘餘 12 行/4 分支逐條如實記錄(多為逾時 catch 之防禦分支) |
| 3.10 | 2026-10-06 | ★★★ 新增 §6.11.31 計畫 + §6.11.32 批次 20-a + §6.11.33 批次 20-b——platform/ 8 模組(WBS 5.37):★★★ 全 backend 行 87.42% → 89.82%、分支 68.98% → 72.12%——兩項門檻首次雙雙達標(分支超出 90 條)。★ 20-a:common-domain/common-batch/common-event 三模組行與分支皆 100%(+56 行/+31 分支)。★★★ 20-b:bff-web 行 30.1% → 97.3%、分支 13.2% → 97.1%(+304 行/+114 分支,單批最大增益);platform/ 8 模組 → 行 84.18%/分支 73.88%。★★ 配方:新增 DownstreamStub(JDK 內建 HttpServer,零相依)模擬下游同儕;★ 受測物與協作者皆為真實物件(非 mock);★ 控制器以真實聚合服務注入。★★ 必驗:DIFFERENCE 歸 WARNING(否則差異被忽略=假綠)/超界 limit 顯式報錯不夾取/保留下游原錯誤碼/未組態即不發請求。★★ 真機反駁我方之錯 3 項(Set.of 重複元素拋錯/toString 為主單位/Map<?,?> 型別捕獲)。★ 掃描器全 0 命中。★ 殘餘 12 行/4 分支逐條如實記錄(多為逾時 catch 之防禦分支) |
| 3.9 | 2026-10-06 | ★★ 新增 §6.11.31 批次 20 計畫(WBS 5.37)——★ 盤點 platform/ 8 模組逐類別缺口(未覆蓋 745 行/402 分支;與 #A-216 逐數字吻合);★★ 關鍵算術:全 backend 分支達 70% 需再覆蓋 48 條(4,623 × 0.70 = 3,236.1)。★ 批次排序(20-a 純單元 → 20-b bff-web → 20-c gateway → 20-d security → 20-e spring → 20-f saga)。★★ 記載三項量測陷阱:JaCoCo 報告兩種 Total 格式(模組層為 bar 之「N of M」文字、類別層為 img title=)/類別列之 ctr1/ctr2 序(行數取索引 4/5,非 2/3——2/3 是複雜度)/</tfoot> 在 <tbody> 之前
|
| 3.8 | 2026-10-06 | ★★★ 新增 §6.11.30 批次 17-d(#A-213)——application 層:payment-service(WBS 5.36 結案):★ PaymentService/PaymentViewAssembler/AutoDebitService 皆 100%;payment.application 92/187 → 184/187 = 98%;★★★ payment-service 85.0% → 98.0%(+92 行)。★★★ 全服務行覆蓋 90.19% → 90.92%(11,397/12,535;門檻超出 1,369 行);★★★ 分支 71.37% → 71.64%;8 測。★★ 配方:真 Oracle + 真實 JDBC 埠 + 樁 HTTP 伺服器(JDK 內建,模擬 posting 餘額端點)。★★★ 關鍵必驗:查無分戶(404)→ 全額歸本金+標記未匹配(非故障);沖正須發射 reversed 事件(否則可用額度永久高估) |
| 3.7 | 2026-10-06 | ★★★ 新增 §6.11.29 批次 17-c-2(#A-212)——LimitAdjustmentService:★ 該類別 0/56 → 46/56(餘 10 為不可達之防禦分支);lim.application 408/468 → 448/468 = 96%;★★★ limit-service 91.6% → 95.6%(+51 行)。★★★ 全服務行覆蓋 89.78% → 90.19%(11,305/12,535;門檻超出 1,277 行);★★★ 分支 70.73% → 71.37%;11 測。★★★ 真機揭露:SPECIAL 須先有可設定上限政策(special_limit_policies),無政策即 CC-LIM-1002 拒絕(fail-closed 設計正確)。★ 配方以真件 SecurityContextCallerIdentity 為身分來源(無認證 → UNVERIFIED) |
| 3.6 | 2026-10-06 | ★★ 新增 §6.11.28 批次 17-c(#A-211)——application 層:limit-service:★ lim.application 377/468 → 408/468 = 87%(LimitApplicationService 45/46);★★ limit-service 89.1% → 91.6%(+31 行)。★★ 全服務行覆蓋 89.53% → 89.78%(11,254/12,535;門檻超出 1,226 行);★★ 分支 70.43% → 70.73%;12 測。★ 必驗:可用額度夾擠為 0/不足即拒絕/專用池回沖失敗仍成功/applicableSpecialLimit 三分支/8-6 查無主檔不落串動紀錄/8-7 回收不超額。⚠️ LimitAdjustmentService(46 行)本批未執行(需 CallerIdentityPort),列後續 |
| 3.5 | 2026-10-06 | ★★★ 新增 §6.11.27 批次 17-b(#A-210)——application 層:rewards-service:★ rw.application 289/387 → 357/387 = 92%(PointsViewAssembler/PointsAccountService 皆 100%);★★ rewards-service 85.7% → 93.5%(+85 行)。★★★ 全服務行覆蓋 88.86% → 89.53%(11,223/12,535;門檻超出 1,195 行);★★★ 分支 69.80% → 70.43%——分支門檻正式達標;12 測。★★★ 真機發現兩項:① 同日到期之同階段桶 ≥2 → 分錄冪等鍵(非以桶為主體)碰撞 → DuplicateKeyException 未被 catch (DomainException) 攔住 → 整批中止 + 留下孤兒桶;② 首次累點(新帳戶)→ 先插桶後插帳戶 → ORA-02291 FK 失敗。★ 皆如實記錄,待裁定是否立 CR
|
| 3.4 | 2026-10-06 | ★★★ 新增 §6.11.26 批次 17-a(#A-209)——application 層:party-service:★ 四類別全數大幅提升(party.application 30/156 → 151/156 = 97%);★★ party-service 59.6% → 91.5%(+189 行)——★ 本計畫之最大單服務改善。★★★ 全服務行覆蓋 87.35% → 88.86%(11,138/12,535;門檻超出 1,110 行);★★ 分支 66.67% → 69.80%(僅差 0.20 pp);14 測。★★★ 動查才知三項:冪等表為 pty_idempotency;save 為純 UPDATE + 樂觀鎖(★ 建列須原生 SQL);Party.reconstitute 之可空參數序為 idNoEnc, mobileEnc, emailEnc(★ 皆 byte[] → 編譯期不報錯,只有真機斷言才現形) |
| 3.3 | 2026-10-06 | ★★★ 新增 §6.11.25 批次 16(#A-208)——storage 層(WBS 5.35 結案):★ 兩服務之 S3StorageConfig 皆 100%、S3ObjectStorageAdapter 各 60/62 = 97%;report 74.1%→84.5%(+60)、statement 84.3%→90.7%(+60)。★★★ 全服務行覆蓋 86.39% → 87.35%(10,949/12,535;門檻超出 921 行);★ 本批 +120 行;20 測。★★★ 動查後修正:用 Garage,不是 MinIO——★ 本專案之本地物件儲存即為 garage;★ MinIO 開源版已封存、Docker Hub 映像已下架(實測 pull access denied)→ 改以專案同一映像與組態(dxflrs/garage:v2.4.1 + garage.toml)起容器,★ 比 MinIO 更忠實。★ 剩 2 行為 404 相容分支
|
| 3.2 | 2026-10-06 | ★★★ 新增 §6.11.23 計畫 + §6.11.24 批次 15 執行結果(#A-207)——client 層(WBS 5.34 結案):★ 5 個服務間 HTTP 客戶端 → 三套件 94~98%(auth 134/137、stmt 71/74、posting 44/47);authorization 72.7%→83.2%(+134)、statement 76.7%→84.3%(+71)、posting 86.8%→92.1%(+44)。★★★ 全服務行覆蓋 84.40% → 86.39%(10,829/12,535;門檻超出 801 行);★ 本批 +249 行;31 測。★★ 配方:以 JDK 內建 HttpServer 之樁伺服器(零相依、非 mock)模擬同儕服務,受測物與協作者皆為真實物件 → 驗真實 HTTP 往返;七項必驗(對映/錯誤封套/非 200/404→empty/連線失敗保留 cause/請求內容/分頁逾上限拋錯)。★ 未覆蓋 9 行皆為不可達之序列化 catch。★ 再確認量測陷阱:過濾跑後服務總數不可信 |
| 3.1 | 2026-10-06 | ★★★ 新增 §6.11.21 計畫 + §6.11.22 批次 14 執行結果(#A-206)——messaging 層(WBS 5.33 結案):★ 7 個 Kafka 消費者(limit 4+auth 3)→ 兩套件皆 0/… → 100%(limit 89/89、auth 82/82);limit 80.7%→89.1%(+105)、authorization 61.6%→72.7%(+144)。★★★ 全服務行覆蓋 82.42% → 84.40%(10,580/12,535;門檻超出 552 行);★ 分支 64.33% → 66.67%。★ 本批 +249 行(★ 遠超套件本身之 171 行——協同效應)。★★ 配方:以真實投影服務+真實 JDBC 埠串鏈,直接呼叫 onMessage(json) 再查 DB;四項必驗(有效→生效/未知型別→不拋/壞 JSON→不拋/缺欄→不拋)。★★★ 真機揭露:部分釋放時 limit 維持 HELD(PARTIALLY_RELEASED 不可達),而 authorization 讀模型寫 PARTIALLY_RELEASED——跨服務不一致,如實記錄。★ 併釐清 JaCoCo append=true 之量測陷阱
|
| 3.0 | 2026-10-06 | ★★★ 新增 §6.11.19 計畫 + §6.11.20 批次 13 執行結果(#A-205)——outbox 橫向複製至其餘 10 服務:★ 10 服務之 outbox 套件皆 0/28 → 100%(KafkaEventPublisher 14/OutboxRelayScheduler 12/OutboxRelayConfig 2);★ 每服務恰 +28 行(與預估一致)。★★★ 全服務行覆蓋 80.18% → 82.42%(10,331/12,535;門檻超出 303 行);★ 分支覆蓋 63.78% → 64.33%(仍未達 70%);1,347 測/0 失敗/1 略過(+60 測);掃描器全 0 命中。★ 服務:merchant 98.9%、dispute 96.5%、card 92.4%、product 91.9%、posting 86.8%、clearing 86.0%、notification/rewards 85.7%、statement 76.7%、authorization 61.6%。★★ 方法論:動工前以「結構簽章抽取」驗證 API 一致(非目視 diff),據此才敢橫向複製 |
| 2.9 | 2026-10-06 | ★★★ 新增 §6.11.18 批次 12(#A-204)——B3-b-2:outbox 套件,★★ 行覆蓋突破 80%:limit/payment 兩服務之 KafkaEventPublisher 0/14 → 100%、OutboxRelayScheduler 0/12 → 100%、OutboxRelayConfig 0/2 → 100%、★ outbox 套件整體 0/28 → 100%;limit 78.5%→80.7%、payment 81.0%→85.0%。★★★ 全服務行覆蓋 79.74% → 80.18%(10,051/12,535;門檻 10,028,超出 23 行)——★ 行覆蓋目標達成。★ 本批 +56 行;1,287 測/0 失敗/1 略過;掃描器全 0 命中。⚠️ 分支覆蓋 63.78%(門檻 70%)未達。★★ 以兩個真容器(Oracle + Kafka)串完整 relay 鏈路,並自真實消費端讀回比對;★ 併驗「失敗即拋」「例外不得外溢」「中斷旗標須還原」。★★ 另立 TRP-004_覆蓋率提升總結報告(v1.0)——B1~批次 12 之總結、真機反駁 13 項彙總、殘留缺口全貌(2,433 行)。★ 後續成本最低之批次:outbox 橫向複製至其餘 10 服務(約 +280 行)
|
| 2.8 | 2026-10-06 | ★★★ 新增 §6.11.17 批次 11(#A-203)——大戶 jdbc 收尾,WBS 5.25 結案:★ 7 個 jdbc 類別全數達 100%(含 JdbcPaymentRepository 61/84→84/84、JdbcInstallmentRepository 61/73→73/73、JdbcAutoDebitRepository 35/41→41/41、JdbcReportJobRepository 45/46→46/46、JdbcAuthorizationSource 3/13→13/13、dispute/merchant 之 OracleRawId)。★ 服務:payment 74.6%→81.0%、installment 78.4%→82.3%、report 72.2%→74.1%、dispute 91.4%、merchant 93.6%。★★ 全服務行覆蓋 79.1% → 79.74%(本批 +74 行;距 80% 尚差 +33 行);1,275 測/0 失敗/1 略過;掃描器全 0 命中。★★★ 解決 #A-200 之誠實略過項——動查映像腳本發現 /container-entrypoint-initdb.d/*.sql 係以 sqlplus / as sysdba 執行 → 以 withCopyFileToContainer 於初始化階段建 CC_AUTH(DDL 逐字抄自權威遷移),僅授 SELECT;★ 併查得 app user 僅具 DB_DEVELOPER_ROLE(不含 CREATE USER)=原失敗之真因。★ 略過數 2 → 1
|
| 2.7 | 2026-10-06 | ★★ 新增 §6.11.16 批次 10(#A-202)——大戶 jdbc:product-service:★ 本服務 jdbc 原僅涵蓋 JdbcOutboxRepository;JdbcProductRepository 0/38 → 100%、JdbcParameterRepository 0/31 → 100%、★ jdbc 套件整體 99/99 → 100%;product-service 行覆蓋 59.0% → 82.4%(+23.4 pp)。★★ 全服務行覆蓋 78.6% → 79.1%(本批 +69 行;距 80% 尚差 +107 行);1,261 測/0 失敗/2 略過;掃描器全 0 命中。★★ 動查才知:PARAMETERS.param_value 為 Oracle 原生 JSON(非 VARCHAR2)。★★★ 真機反駁(第七次):樂觀鎖衝突不可以「已落後物件 + withUpdated」建構(+1 會補成「DB 現行 + 1」→ 反而成功);須以「同版本、不同內容」再送。★ 併記錯誤碼字面值為連字號 CC-PLT-5000。★ 兩者已回寫技能(陷阱 6/7) |
| 2.7 | 2026-10-06 | ★★ 新增 §6.11.16 批次 10(#A-202)——大戶 jdbc:product-service:★ 本服務 jdbc 原僅涵蓋 JdbcOutboxRepository;JdbcProductRepository 0/38 → 100%、JdbcParameterRepository 0/31 → 100%、★ jdbc 套件整體 99/99 → 100%;product-service 行覆蓋 59.0% → 82.4%(+23.4 pp)。★★ 全服務行覆蓋 78.6% → 79.1%(本批 +69 行;距 80% 尚差 +107 行);1,261 測/0 失敗/2 略過;掃描器全 0 命中。★★ 動查才知:PARAMETERS.param_value 為 Oracle 原生 JSON(非 VARCHAR2)。★★★ 真機反駁(第七次):樂觀鎖衝突不可以「已落後物件 + withUpdated」建構(+1 會補成「DB 現行 + 1」→ 反而成功);須以「同版本、不同內容」再送。★ 併記錯誤碼字面值為連字號 CC-PLT-5000。★ 兩者已回寫技能(陷阱 6/7) |
| 2.6 | 2026-10-06 | ★★ 新增 §6.11.15 批次 9(#A-201)——大戶 jdbc:migration-service:★ 本服務先前無 jdbc 整合測試;JdbcLegacyAccountRepository 0/21 → 100%、JdbcMigrationWaveRepository 0/15 → 100%、JdbcMigrationJobRepository 36/37;migration-service 行覆蓋 60.5% → 92.1%(★ +31.6 pp,單服務最大跳幅)。★★ 全服務行覆蓋 77.5% → 78.6%(本批 +134 行);1,249 測/0 失敗/2 略過;掃描器全 0 命中。★★ 動查才知:表名為 legacy_account_mappings(非 LEGACY_ACCOUNTS)。★ 真機反駁(第六次):三個列舉皆為巢狀(非獨立類別) |
| 2.5 | 2026-10-06 | ★★ 新增 §6.11.14 批次 8(#A-200)——大戶 jdbc:report-service:★★ 本類別用「兩個容器」(Oracle + PostgreSQL,因本服務 jdbc 橫跨兩個資料源);JdbcAuthSummaryReadModel 0/21 → 100%、JdbcReportDefinitionRepository 0/14 → 100%、JdbcReportJobRepository 35/36;report-service 行覆蓋 47.8% → 72.2%。★★ 全服務行覆蓋 76.6% → 77.5%(本批 +117 行);1,239 測/0 失敗/2 略過;掃描器全 0 命中。⚠️ JdbcAuthorizationSource 跨 schema 測試因容器 app user 無 DBA 而誠實略過(10 行未覆蓋,列後續)。★★ 真機反駁(第五次):ReportJob 狀態機為 DAG(complete/fail 皆要求 RUNNING;COMPLETED 不得再 start) |
| 2.4 | 2026-10-05 | ★★ 新增 §6.11.13 批次 7(#A-199)——大戶 jdbc:notification-service:5 個 jdbc 類別中 4 個達 100%;JdbcNotificationRepository 37/39;notification-service 行覆蓋 71.0% → 83.1%。★★ 全服務行覆蓋 75.7% → 76.6%(本批 +118 行);1,226 測/0 失敗/1 略過;掃描器全 0 命中。★★★ 真機反駁(第四次):OracleRawId.toUuid 對壞長度拋 IllegalStateException(fail-closed),非回 null |
| 2.3 | 2026-10-05 | ★★★ 新增 §6.11.11 批次 6(#A-198)——大戶 jdbc:limit-service,★ 併結案 WBS 5.13(B1-b2):兩個付款投影 repository 各 0/17 → 100%;limit-service 行覆蓋 73.1% → 78.5%。★★ 全服務行覆蓋 75.2% → 75.7%(本批 +53 行);1,215 測/0 失敗/1 略過;掃描器全 0 命中。★★★ 新增 §6.11.12:核心不變式「已記=已套用」——套用失敗時不得寫 consumed_events(否則重放永久漏套用)。★ 以真實 JdbcLimitRepository 為協作者(無 mock) |
| 2.2 | 2026-10-05 | ★★ 新增 §6.11.10 批次 5(#A-197)——大戶 jdbc:clearing-service:JdbcClearingRunRepository 0/33 → 100%、JdbcSettlementRepository 0/33 → 100%;clearing-service 行覆蓋 65.7% → 81.5%。★★ 全服務行覆蓋 74.4% → 75.2%(本批 +99 行);1,202 測/0 失敗/1 略過;掃描器全 0 命中。★ 動查發現 SETTLEMENTS.DIRECTION 係 V2 新增(V1 無) |
| 2.1 | 2026-10-05 | ★★ 新增 §6.11.9 批次 4(#A-196)——大戶 jdbc:party-service:JdbcAccountRepository 0/32 → 100%、JdbcPartyRepository 37/41(餘 4 為 parseFlags/serializeFlags 之不可達 catch);party-service 行覆蓋 44.0% → 59.6%。★★ 全服務行覆蓋 73.7% → 74.4%(本批 +92 行);1,194 測/0 失敗/1 略過;掃描器全 0 命中。★ 驗證 flags JSON 往返、兩處樂觀鎖、BLOCK_CODE_HISTORY append-only、他歸戶排除 |
| 2.0 | 2026-10-05 | ★★★ 新增 §6.11.7 批次 3(#A-195)——大戶 jdbc 之首發:authorization-service 之 JdbcAuthorizationRepository(162 行,全庫最大單一 jdbc 缺口)→ ★★ 162/162 = 100%;authorization-service 行覆蓋 43.5% → 59.4%。★★ 全服務行覆蓋 72.1% → 73.7%——★ 本批 +195 行(單批最大增益);1,187 測/0 失敗/1 略過。★★★ 新增 §6.11.8:實測揭露三項我方語意錯誤(★ 真機反駁:DECLINED 不佔額度/PendingStandIn.authorizationId 非 holdId/IS NOT NULL 條件)。★ 協同效應兌現(併同覆蓋多個 model 之 reconstitute/存取子) |
| 1.9 | 2026-10-05 | ★★ 新增 §6.11.6 批次 2(#A-194):B3-b-1 之 scheduler 層全數完成——8 個排程類別 88/88 → 100%(8 個測試類別、17 測)。★★ 核心測試點:例外不得外溢(Spring @Scheduled 拋例外將取消後續排程);併測 limit 之兩個 try/catch 各自獨立與 report 之 daysBack 下限 1。★ 全服務行覆蓋 71.4% → 72.1%;1,170 測/0 失敗/1 略過。 |
| 1.8 | 2026-10-05 | ★★ 新增 §6.11 續推計畫(B4+B3-b+大戶)(#A-192):★ 先更正前輪「jdbc 層幾已見底」之錯誤敘述(實為仍有 935 行);★ 精確盤點(model 393/application 814/scheduler 88/storage 118/messaging 217/client 251/jdbc 935);★ 批次排序依「每行覆蓋之成本」;★ 達 80% 尚需 +1,219 行。★★ 新增 §6.11.5 批次 1 結果(#A-193):party.Party+rewards.PointsAccount 之純領域分支——party 23.7% → 44.0%、rewards 79.8% → 82.1%;★ 全服務行覆蓋 70.3% → 71.4%。★ 併載「jdbc 與 model 之協同效應」洞察。⚠️ 如實揭露本輪僅完成 B4-a 之 2 個類別(96/393 行)
|
| 1.7 | 2026-10-05 | ★★ 新增 §6.10 W4:rewards/card 之 jdbc 層真機整合測試(#A-190/#A-191)——2 類別/39 測;★ 9 個 jdbc 類別中 6 個達 100%;rewards 行覆蓋 58.7% → 79.8%、card 70.6% → 89.4%;★★ 全服務行覆蓋 67.0% → 70.3%。★★★ 併載 §6.10.3 實測揭露之三項真實行為(★ 修正我三個錯誤假設:points > 0 之 CHECK、save 不寫 card_id、Timestamp.from 遺失 offset)與一項一致性瑕疵(findAvailable 未修剪 CHAR(8),惟無功能影響)。⚠️ 併如實揭露本文初稿之預填失誤(§6.10.5)。 |
| 1.6 | 2026-10-05 | ★★ 新增 §6.9 W3:statement/posting 之 jdbc 層真機整合測試(#A-188/#A-189)——2 類別/21 測;★ 四個 jdbc 類別全數 100%;statement 行覆蓋 51.7% → 73.7%、posting 64.8% → 83.5%;★★ 全服務行覆蓋 64.1% → 67.0%。★ 併載 §6.9.3 防假綠設計(6 個查詢之近似情境)與 §6.9.4 實測揭露兩項事實(idempotency_key 全域 UNIQUE 使 findDuplicates 至多 1 筆——★ 初版測試因此被真機反駁;value_date 為 NOT NULL)。 |
| 1.5 | 2026-10-05 | ★★★ 新增 §6.8 B2 規模化結果(#A-182/#A-183):回頭補 AuthorizationController 76 行 → 0(行覆蓋 100%;服務 36.3% → 43.5%)+ 橫向複製至 16 服務、25 個 @WebMvcTest、130 條測試(★ 範圍由 11 更正為 16)。★★★ 實測揭露三個真缺陷:#A-184(ResponseStatusException 未處理 → 查無卡片應 404 實回 500)、#A-185(★ Jackson 3 預設 FAIL_ON_NULL_FOR_PRIMITIVES=true → 省略 primitive 欄位即 400)、#A-186(部分 @RequestBody 缺 @Valid → 巢狀驗證不生效)。 |
| 1.4 | 2026-10-05 | ★ 新增 §5.5 W1-a(installment-service)實測(#A-170):行覆蓋 47.8% → 73.3%、JdbcInstallmentRepository 0/73 → 60/73;★ 測試發現 update() 具樂觀鎖;⚠️ 其 version 約定未查明 → 成功路徑測試暫緩並誠實註記(列 W1-a2)。★ pricing-service 因 PRICING_RULES 建表位置與巢狀列舉未查明 → 列 W1-b。 |
| 1.3 | 2026-10-05 | ★★ 新增 §7 B1-c 執行計畫(#A-169):10 服務盤點(未覆蓋 1,675 行/jdbc public 方法 4~41);★★ 排序原則「先低複雜度,不先大缺口」(W1 最低複雜度 → W5 authorization 最後);★★ 誠實推估「只做完 jdbc 層仍到不了 80%」(約 67%,必須 B2+B3 一併);可複製配方。 |
| 1.2 | 2026-10-05 | ★ 新增 §5.1 B1-b 實測增益(#A-168)與 §5.3 兩個實測收穫、§5.4 未完成項。★ 修訂誠實推估為「一個服務 ≈ +2.0 pp」→ 達 80% 約需 20~22 個服務同等規模。 |
| 1.1 | 2026-10-05 | ★ 新增 §5.1 B1-a 實測增益(#A-167)與 §5.2 實作要點(含 Spring Boot 4 測試切片已模組化、Testcontainers Oracle 套件名、映像須用 gvenzl 三個實測發現)。★ 併載誠實推估:一個測試類別 = +1.0 pp,達 80% 需 約 25~27 個同規模類別。 |