# test-specialist/baseline-history — gist
distill-gen: 1
source-verbatim: .claude/agent-memory/test-specialist/archive/baseline-history.md (29 entry-heading + 1 dòng chuỗi → 10 cụm; curator khai 25 entry — chênh ghi ở phụ-lục "nguồn-đếm")
pointer-style: substring

## TỔNG-HỢP (3-5 ý)
- Chuỗi baseline 254 (S60) → 561 (S155) là RUỘT file: chỉ có giá-trị khi mỗi mốc kèm delta + lý-do; giữ NGUYÊN 16 mốc chuỗi gốc + 12 mốc hậu-chuỗi + **8 LỖ** (4 curator: 419→431 · 434→440 · 477→486 · 500→501 + 4 bổ @S170 P3-gate reviewer: 254→263 chênh+1 · 402→419 chênh+11 · 519→528 chênh+1 · **528→561 chênh+23 — LỚN NHẤT, gấp đôi 4 lỗ đầu cộng lại**; cùng một định-nghĩa delta-không-nối) + **2 MÂU-THUẪN nội-tại** — ⚠️ mâu-thuẫn nội-tại văn-bản — KHÔNG tự sửa số @S170.
- Bẫy vacuous là chủ-đề lặp xuyên file: fixture không wire `AuditingInterceptor` ⇒ assert đúng ở CẢ HAI thế-giới (TS-B); fixture-hỏng-cho-xanh trông Y HỆT máy-đúng-cho-xanh (TS-C); phép chống mạnh nhất = số CHÍNH XÁC, 2 hàm, 1 seed (C6, TS-E) + mỗi assert "cho qua" của phần tiền kèm 1 seed "chặn lại" (TS-G).
- Test tìm được cả bug prod lẫn lỗi tài-liệu: T26 GIỮ ĐỎ = bug prod thật (cascade Immediate → dependent `Added`→`Detached` ⇒ 0 row changelog, TS-B); spec ghi sai tên query `GetPurchaseEvaluationDetailQuery` — test bắt, lead sửa spec (TS-D). Cả hai đều REPORT, KHÔNG tự sửa.
- Test xanh KHÔNG chứng đường chạy thật còn sống: S92 reflection gọi method cô-lập vẫn xanh dù prod đã TẮT lời gọi seed (TS-F); dedup collation nằm ở SERVICE `OrdinalIgnoreCase` — fixture SQLite BINARY không bắt được ca prod SQL Server bắt (TS-H).
- Hai ghi-chú "API trông như có kiểm mà không kiểm" sống sót mọi vòng curate: `NotEmpty()` trên `Guid?` không bắt `Guid.Empty`; `ExecuteUpdateAsync` đi vòng change tracker (TS-J).

## Entries

**[cao] TS-A — chuỗi baseline: giữ nguyên mốc, gắn cờ 4 lỗ + 2 mâu-thuẫn**
- VIỆC: duy-trì một chuỗi số test-pass xuyên ~20 phiên làm bằng-chứng "không hồi-quy".
- KẾT-LUẬN (giữ NGUYÊN VĂN mốc): **254 (S60) → 263 (S61: +22 PeWorkItemBudget, −14 BudgetPolicy Domain drop) → 286 (S67 +23 HRM) → 292 (S69 +6 Office) → 306 (S69b +14 PE) → 334 (S72 +28 PE Mig54) → 339 (S74 +5 CcmNote) → 344 (S76 +5 PRO-split) → 351 (S77 +7 suggested-price NOTE) → 354 (S77b symmetric, superseded) → 366 (S78-S84, MEMORY chưa ghi từng delta) → 371 (S85 +5 urgent drafter-unset) → 374 (S85b +3 UpdateDraft WF-preserve) → 377-395 (S86-S86d) → 402 (S87 +7 CCM-budget-period) → 419 (D2 +6 PE Mig58 multi-winner + translation-smoke)**. Hậu-chuỗi rải trong entry (chuỗi một-dòng KHÔNG được cập nhật tiếp): **431→434 (S92 +3)** · **440 (S97, +0 extend in-place)** · **440→450 (D3 +10)** · **450→458 (D4 +8)** · **458→477 (S113 +19)** · **486→495 (S116 +9)** · **495→500 (S117 +5)** · **501→505 (S118 +4)** · **505→509 (S118b +4)** · **→519 (S133, Domain 45 + Infra 474)** · **→528 (S134, 45D+483I)** · **→561 (S155 +10; 560 PASS / 1 FAIL; 45D + 516I)**.
- BÀI-HỌC: chuỗi chỉ có giá-trị khi mỗi mốc kèm delta + lý-do; đoạn "366 (S78-S84, MEMORY chưa ghi từng delta)" và "377-395 (S86-S86d)" cho thấy chỗ nào bỏ ghi thì vĩnh-viễn không truy được nữa. Khi nén, mốc đầu/cuối mỗi giai-đoạn là thứ KHÔNG được phép mất.
- BẤT-NGỜ / CỜ CHO LEAD — ⚠️ mâu-thuẫn nội-tại văn-bản — KHÔNG tự sửa số @S170: **8 LỖ (4 curator + 4 bổ @S170 P3-gate):** ① 419 → 431 (+12 không có mốc) · ② 434 → 440 (+6) · ③ 477 → 486 (+9) · ④ 500 → 501 (+1) · **⑤ 254→263** khai +22/−14=+8 nhưng thật +9, chênh +1 (S61) · **⑥ 402→419** khai +6 nhưng thật +17, chênh +11 (D2) · **⑦ 519→528** khai +8 (Infra 474→483) nhưng thật +9, chênh +1 (S134) · **⑧ 528→561** khai +10 và tự khai "Infra cũ 506 nguyên vẹn" trong khi S134 để lại 483 — thật +33, **chênh +23, lỗ LỚN NHẤT** (4 lỗ bổ dùng CHÍNH định-nghĩa delta-không-nối của curator, reviewer P2b áp lên toàn chuỗi; số gốc GIỮ NGUYÊN — chỉ danh-sách lỗ được bổ, con số "4 LỖ" cũ là claim "đã-soi-hết" sai). **Mâu-thuẫn (a):** S133 tiêu-đề ghi "+10 -> 519" nhưng thân ghi "+9 test vào PeBudgetFreezeTests" — hoà-giải KHẢ DĨ, chưa xác-minh: T0 của INV-A (+1) + T1-T9 của INV-C (+9) = +10, khớp "464+10". **Mâu-thuẫn (b):** S153 (2026-07-26) ghi "baseline 509 KHÔNG đổi" trong khi S134 (2026-07-17, TRƯỚC đó) đã ghi 528 và S155 (2026-07-27, NGAY SAU) ghi 551→561 ⇒ con số 509 ở S153 không nằm cùng thang với hai mốc kẹp nó. Curator CHỈ đọc file, KHÔNG chạy `dotnet test` — đây là mâu-thuẫn TRONG VERBATIM, không phải phán-quyết về baseline thật.
→ substring:"Count progression"

**[cao] TS-B — S155: giữ nguyên 1 test ĐỎ vì đó là bug prod**
- VIỆC: đợt 2 tính-năng "xoá phiếu khi đang duyệt" + màn "Đã xoá" — +10 test `PeDeleteByApproverTests.cs` (T21 lũy-kế 4 hướng · T22/b/c màn Đã xoá · T23/T24/b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog).
- KẾT-LUẬN: 561 = 560 PASS / 1 FAIL (45 Domain + 516 Infra), 0 hồi-quy (506 Infra cũ nguyên vẹn). T26 GIỮ ĐỎ vì là BUG PROD thật: `PeSoftDeleteFeatures.cs` thêm changelog `:126` rồi `Remove(pe)` `:142`; `CascadeDeleteTiming.Immediate` cascade NGAY TẠI `Remove`, thực-thể con đang `Added` bị đẩy sang `Detached` (không phải `Deleted`) ⇒ 0 row; `AuditingInterceptor` chạy ở `SavingChanges` = SAU điểm mất nên không cứu được. Đo bằng chẩn-đoán `ChangeTracker` (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng lớp: site DUY NHẤT có hình "Add con → Remove cha → 1 SaveChanges". Báo lead, KHÔNG tự sửa.
- BÀI-HỌC: `SoftDeleteFixture` (~90 LOC, ngay trong file test) là fixture ĐẦU TIÊN của repo wire `AuditingInterceptor` — bắt buộc khi đo `Remove()` nghĩa là xoá-mềm, vì `TestApplicationDbContext` không wire ⇒ mặc-định XOÁ CỨNG ⇒ màn "Đã xoá" rỗng ⇒ assert đúng ở CẢ HAI thế-giới = đúng bẫy vacuous.
- BẤT-NGỜ: số học T21 phải seed thêm phiếu hàng-xóm (DaDuyet + TraLai) để hai đại-lượng "không đổi" (PrevSelected, PendingSubmitted) khác 0 ở cả hai phía — để 0 thì "bất động" và "rỗng" nhìn giống hệt nhau.
→ substring:"PeDeleteByApproverTests"

**[cao] TS-C — S153: tiêm lỗi vào máy `mind-check`, bắt được chính fixture của mình nói dối**
- VIỆC: fault-inject #8-#17 + #21 lên `scripts/session_ctx.py mind-check` (KHÔNG phải test C#), mọi đột-biến ở cây tạm qua seam `--root`, repo read-only.
- KẾT-LUẬN: 18/18 PASS 0 FAIL, baseline C# KHÔNG đổi (con số "509" khai kèm — xem mâu-thuẫn (b) ở TS-A). Bắt 1 GAP THẬT: ở chế-độ closed, xoá con-trỏ → cờ/exit 0 trong khi giữ con-trỏ đang-chờ trung thực → trượt/exit 1 ⇒ đường rẻ nhất để xanh là đường SAI (chữ luật cấm, gradient thưởng). Báo em-main, KHÔNG tự sửa.
- BÀI-HỌC: fixture `draft-ts` MALFORMED vẫn cho exit 0 — `inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find` = −1 ⇒ chèn GIỮA heading ⇒ `ts:` không đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. Fixture-hỏng-cho-xanh trông Y HỆT máy-đúng-cho-xanh ⇒ soi RUỘT fixture (`cat -A` / `repr` từng dòng), không đọc mỗi exit code.
- BẤT-NGỜ: 2 kỹ-thuật tái dùng — (i) cặp CÙNG-RUỘT khác đúng 1 biến (cap=32 vs cap=1 cùng tệp = chứng đọc config LIVE; `_end` giả `_pause=9` vs không = chứng nguồn đếm là ĐĨA, `diff` chứng 2 output BYTE-IDENTICAL); (ii) đo wall-clock phải đi nhánh ĐẮT — lệnh literal rơi nhánh thoát-sớm là tự đo nhánh rẻ; đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×, trả lead điền lại.
→ substring:"inject_A"

**[cao] TS-D — S133 budget-freeze: dựng ĐỎ rồi đẩy XANH + 2 lượt cùng vai đè file nhau** *(gộp 2 entry INV-A + INV-C — hai nửa một chu-trình test-before, giữ đủ 2 verdict)*
- VIỆC: INV-A dựng test tái-hiện lỗi (test-before) → INV-C viết T1-T9 phủ tính-năng đóng băng ngân-sách phiếu đã duyệt.
- KẾT-LUẬN: INV-A RED thật `Failed 1/1` — kỳ vọng ProInitial 100tr, thấy 999tr @`PeBudgetFreezeTests.cs:184`; luồng chuẩn = đưa phiếu tới DaDuyet QUA `svc.TransitionAsync` (V2 1-Bước-1-Cấp `AllowApproverFinalize=true` + giá chốt), KHÔNG seed thẳng Phase (sau vá thì snapshot chỉ đặt qua transition); đổi giá qua `UpdatePeBudgetPro/Ccm` (2 handler không chặn theo phase = đúng cơ-chế lỗi); 2 sanity-assert chống vacuous (Phase==DaDuyet + bản ghi live đổi thật) đặt TRƯỚC assert chính. INV-C +9 test (`:426` T1 đủ-11-cột · `:480` T2 CCM-threshold · `:524` T3 all-steps · `:555` T4 V1-legacy · `:638` T5 guard CCM+Admin · `:687` T6 AdjustBudget · `:730` T7 live-vs-frozen · `:783` T8 null-safe · `:821` T9 overwrite+refresh) → lead chạy full exit-0: Domain 45 + Infra 474 = 519.
- BÀI-HỌC: INV-C ghi đè `sub-test-specialist-0.md` của INV-A — cùng vai + cùng chỉ-số task qua 2 lần gọi trong cùng run-folder = va-chạm tên sub-file; nội-dung INV-A sống nhờ journal + diary. Nhiều lần gọi trong một run PHẢI có nhãn/chỉ-số phân biệt.
- BẤT-NGỜ: spec ghi SAI tên truy-vấn — thật là `GetPurchaseEvaluationQuery` (bundle `.BudgetSummary`), spec ghi `GetPurchaseEvaluationDetailQuery`; test-specialist bắt được và lead sửa spec (test soi ra lỗi tài-liệu, không phải lỗi mã).
→ substring:"wf_d9639faa"

**[cao] TS-E — S134: hai hàm, một seed, khớp số CHÍNH XÁC**
- VIỆC: +8 test `PePendingAccumulationTests.cs` (C1-C8) cho lũy-kế TẠM TÍNH (mirror seed `PeBudgetFreezeTests:277-336`).
- KẾT-LUẬN: C1 ChoDuyet+winner → delta2+PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 nháp chỉ-liệt-kê / TraLai delta1 · C4 cửa-sổ `CreatedAt` · C5 delta âm có dấu · C6 bổ-sung-không-đếm-hai-lần: gọi CẢ `ComputeAsync` + `ComputePendingAsync` trên CÙNG seed, assert SỐ CHÍNH XÁC hai phía (PrevSelected 90tr/1 bất động ⟂ PendingSelected 50tr/1) · C7 frozen→0/null qua query handler · C8 changelog D4 dương + đối-chứng (lọc `ContextNote.Contains("[S134 pending-prior]")`, gotcha #48). Full 528/0 (45D+483I).
- BÀI-HỌC: (1) gọi thẳng accumulator chỉ cần `curPeId` tổng-hợp + `CreatedAt` muộn, không cần row phiếu đang xét (chỉ lọc `Id!=` / `CreatedAt<`); (2) phép chống hồi-quy mạnh nhất cho đại-lượng "bất động" = kiểu C6 — số chính xác, 2 hàm, 1 seed; (3) FluentAssertions `ContainInOrder(params T[])` đụng overload với `because` khi T=Guid ⇒ bọc `new[]{...}`.
- BẤT-NGỜ: helper D4 không gọi SaveChanges ⇒ row chỉ thực sự nằm DB SAU `TransitionAsync` — assert sớm sẽ thấy "không có row" và kết luận sai về tính-năng.
→ substring:"PePendingAccumulationTests"

**[vừa] TS-F — cụm SECURITY/authz: khuôn reflection + đối-chứng chống vacuous** *(gộp 5 entry theo loại rủi-ro: S118b +4→509 · S118 +4→505 · S92 +3→434 · S69 Office +6 · S54 ItTicket +13)*
- VIỆC: 5 đợt khoá quyền bằng test hồi-quy security.
- KẾT-LUẬN: S118b mở rộng `Api/AuthorizePolicyRegressionTests.cs` (Pattern 10 reflection, file 15→19 [Fact]): `Dashboard` + `ExportContracts` nay `[Authorize(Policy="Reports.Read")]`, `MyDashboard` CỐ Ý để trần — T4 assert `(attr?.Policy).Should().BeNull()` để CẤM mọi policy (thêm policy = khoá dashboard cá-nhân role không-Admin); csproj đã ref Api từ S45. S118 `ProcurementMasterAccessSeedTests.cs` 4 [Fact], T2 ⭐ScopeIsolation là lõi: DeptManager pre-grant → revoke → seed ⇒ 9 key VẪN all-false (row còn giữ vết) = cấp quyền role này không rò sang role khác, kèm đối-chứng Procurement thật sự được nâng. S92 `AdminOnlyModulesRevokeTests.cs` 3 test giữ bất-biến "PE KHÔNG bị revoke". S69 Office khác HRM: cấp cả Read AND Create trên 16 key. S54 ItTicket = authz theo dữ-liệu (2 phòng ban + ma-trận role/dept).
- BÀI-HỌC: khuôn chung = reflection vào private static + seed FK đúng thứ tự (MenuItem → Role → Permission) + luôn có nhánh đối-chứng dương để test không "xanh vì chẳng có gì xảy ra"; authz theo dữ-liệu (handler) khác hẳn authz tĩnh (`[Authorize]` đọc reflection) — hai điểm thực-thi, test khác nhau.
- BẤT-NGỜ: S92 — `OfficeModulePermissionSeedTests` + `HrmProfilePermissionSeedTests` VẪN xanh dù prod đã TẮT 2 lời gọi cấp quyền, vì test gọi thẳng method cô-lập qua reflection, không chạy chuỗi `SeedAsync` ⇒ test xanh không chứng minh đường chạy thật còn sống; đã thêm dòng ghi chú đầu mỗi file.
→ substring:"ProcurementMasterAccessSeedTests"

**[cao] TS-G — cụm FINANCIAL / critical-algo: giá chốt, số âm, ngưỡng, đa người thắng** *(gộp 6 entry: S117 +5→500 · S116 +9→495 · S72 +28 · S69b +14 · D2 +6→419 · S87 +7)*
- VIỆC: 6 đợt test cho phần dính tiền.
- KẾT-LUẬN: S117 khoá bất-biến `Max != null ⟺ Source=="ProMinMax"` + ca biên min==max PHẢI OK (revert `>` thành `>=` là đỏ). S116 đổi guard nộp phiếu từ `Sum()<=0` sang `Count==0` / `All(x==0)` cho báo giá ÂM đi qua; ranh-giới = ca net==0 nhưng từng dòng ≠ 0 `[+500k, −500k] → ChoDuyet`; ngưỡng CCM tính trên NET (`[+1.2tỷ, −0.3tỷ]=0.9tỷ` không chạm ⟂ đối-chứng `[+1.2tỷ]` một mình thì chạm). S72 chuyển CCM-finalize từ TỰ-ĐỘNG sang CHỌN-THAM-GIA + thứ tự fail-closed (threshold-null → Conflict / sai role → Forbidden / vượt ngưỡng → Conflict). D2 chuyển 1→N hợp-đồng cho N đơn-vị thắng, `GiaTri` per-winner = tổng ThanhTien báo giá của ĐÚNG winner (140/260) ⟂ ca single dùng legacy `ThanhTienNganSach` (800). S87 CCM budget-period setter authz (+7).
- BÀI-HỌC: với phần tiền, đối-chứng ngược luôn đi kèm — mỗi assert "cho qua" phải có một seed "chặn lại" cạnh bên, nếu không thì không phân-biệt được "luật đúng" với "luật không chạy".
- BẤT-NGỜ: ① S117 — method prod đổi sang 5 tham số: `MethodInfo.Invoke` không tự bind optional param ⇒ giữ 4 arg là `TargetParameterCountException` phá 3 test cũ (tự gây, tự tránh). ② S72 — nhánh `isSystem` KHÔNG với tới được qua `ApproveV2Async` công-khai (PE không có job SLA) ⇒ phòng-thủ/chết, test bằng unit + reflection, ghi rõ OBSERVATION. ③ D2 — `PurchaseEvaluationQuote.PESupplierId` FK CỨNG → Supplier Restrict (`cfg:135`) nên Guid ngẫu-nhiên làm SQLite ném lỗi 19, phải seed row thật; trong khi `Contract.SupplierId/ProjectId` KHÔNG có FK vật lý — hai quy-ước ngược nhau cùng một luồng. ④ S87 — `Changelog.UserId` là `Guid?` không FK nên Guid ngẫu-nhiên vô-hại.
→ substring:"TargetParameterCountException"

**[vừa] TS-H — cụm Supplier import: lỗi so-sánh hoa/thường + 1 NRE báo-không-sửa** *(gộp 2 entry: D3 +10→450 test-before · S113 +19→477 test-after)*
- VIỆC: D3 test-before cho import Excel; S113 test-after cho dedup v2 + guard công-bố.
- KẾT-LUẬN: lỗi lõi = collation: dedup nằm ở SERVICE với `OrdinalIgnoreCase`, không dựa DB ⇒ fixture SQLite (BINARY) KHÔNG bắt trùng khác-hoa-thường, prod SQL Server (CI) thì bắt — `truonggiang` vs `TRUONGGIANG` ra Inserted=0/Updated=1; nếu service dùng `Ordinal` thì prod nổ 500 cả lô. Thứ tự ưu-tiên dedup: existing-MST → existing-Code → batch-MST → batch-Code → New; MST thắng, không âm thầm ghi đè (FillNulls bỏ qua Code). Guard công-bố: `PublishSupplierCommand` (KHÔNG đi qua UpdateSupplier — #73), chặn Code rỗng, chặn trùng chính-xác.
- BÀI-HỌC: khuôn dựng header xlsx qua reflection từ `ExpectedHeaderTokens` ⇒ test tự đồng-bộ khi hằng đổi; ClosedXML in-memory gán `.Value = string` GIỮ NGUYÊN chữ (0.105 không bị ép về số ⇒ mã có số 0 đứng đầu an toàn).
- BẤT-NGỜ: S113 tìm ra BUG thật và KHÔNG sửa: `NewSupplier` `Code = r.Code!.Trim()` → NRE khi `row.Code == NULL` (đường R4 nháp: parser sinh null cho ô rỗng, preview phân loại New không báo lỗi ⇒ tới confirm mới nổ ⇒ 500 cả lô, dbCount=0), chuỗi rỗng `""` thì OK; viết characterization test chốt đúng hành-vi NRE hiện tại để khi vá null-safe thì test tự lật sang NotThrow. D3 khai `ExpectedHeaderTokens` chỉ là PHỎNG ĐOÁN ≠ 30 token thật dòng 4 ⇒ Case 9 so hai chiều động, hiện `LayoutValid=false`, tự lật true khi hằng được sửa.
→ substring:"truonggiang"

**[vừa] TS-I — cụm bug-fix / spec-change / compile-fix arity** *(gộp 9 entry: D4 +8→458 · S97 +0 · S86 +3 · S85b +3 · S85 +5 · S77c rewrite 12→11 · S77 +7 · S76 +5 · S74 +5)*
- VIỆC: 9 đợt nhỏ quanh bug-fix + spec-change + đổi arity record command.
- KẾT-LUẬN: D4 đổi `ApplyDrafterBypassOnSubmitAsync` sang A1 — bypass CHỈ các cấp dưới thuần (Order < minOwn), con-trỏ DỪNG tại minOwn để người soạn ký TAY; 4 test cũ ĐỔI TÊN vì tên cũ gây hiểu nhầm. S97 = spec-change: đọc thẳng cờ `EndedByLevelFinalize` thay suy từ config, test cũ thêm param + ca tái-hiện mới `peFinButWentCeo` (cùng config nhưng cờ FALSE). S86 chứng RED-trên-code-cũ bằng `git stash` rồi khôi phục nguyên trạng. S77c = đảo chiều luật giữa chừng: SET = đủ role chức-năng / UNSET = role AND DeptManager.
- BÀI-HỌC: đổi arity của record command thì compile-fix là MỘT PHẦN của việc test (S76 vá 5 call-site + đổi assert `.ProEstimateAmount` → `.ProInitialAmount`; S74 vá 3 call-site thêm `null`) — bỏ qua thì không phải "test đỏ" mà là "không build được".
- BẤT-NGỜ: S86 — phép loại-trừ theo actor KHÔNG che được bug (actor ở Cấp 1, người tạo ở Cấp 2) nên test mới thật sự có răng; S85 — nhánh `isDrafter` ưu tiên và độc lập với `isProTp`, người vừa Drafter vừa Procurement-không-DM vẫn gỡ được CẢ 2 cờ; S77c ghi thẳng "test theo CODE" (không đụng prod).
→ substring:"ApplyDrafterBypassOnSubmitAsync"

**[thấp] TS-J — 2 ghi-chú kỹ-thuật lẻ (S57bis / S56)** *(rút narrative — token sống ở phụ-lục)*
- VIỆC: giữ 2 quan-sát không gắn với đợt test nào.
- KẾT-LUẬN: S57bis — `NotEmpty()` trên `Guid?` KHÔNG bắt `Guid.Empty` (FluentValidation 7.2 coi `default(Guid?)` == null) ⇒ dựa FK-guard ở create-handler (phòng-thủ nhiều lớp, không phải bug prod). S56 — `ExecuteUpdateAsync` đi vòng change tracker ⇒ bản đọc đang tracked bị cũ, phải `.AsNoTracking()` đọc lại; transaction đã ship `IsolationLevel.Serializable`.
- BÀI-HỌC: hai ca đều loại "API trông như có kiểm mà không kiểm" — validator im lặng và lệnh update im lặng; phải biết trước mới viết được assert đúng.
- BẤT-NGỜ: cả hai sống sót qua mọi vòng curate dù không kèm test nào — giá-trị nằm ở chỗ CHẶN một giả-định sai, không ở số lượng test.
→ substring:"ExecuteUpdateAsync"

## Phụ-lục token (coverage)
*(chép TRỌN token-list section test-specialist/baseline-history của proposal curator — lưới coverage-diff)*

- **gotcha#:** `#48` (S134 — lọc `ContextNote.Contains("[S134 pending-prior]")`) · `#73` (S87 "null=clear (pre-seed 500tr, gotcha #73)"; S113 "`PublishSupplierCommand` (KHÔNG UpdateSupplier #73)") · **các ⚠️/"Gotcha" không đánh số:** S117 "⚠️ Gotcha (self-inflicted-avoided)" · S92 "**Gotcha:**" (test xanh dù prod tắt) · S56 "gotcha:" (`ExecuteUpdateAsync`)
- **surprise (nhãn nguyên-văn):** "Surprise: D4 no-SaveChanges-in-helper → row chỉ persist sau TransitionAsync" (S134) · "⚠️ Lesson mới: INV-C **ghi đè** `sub-test-specialist-0.md` của INV-A … sub-file collision" (S133) · "**Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 … **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh**" (S153) · "đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×" (S153) · "**1 FAIL = T26 GIỮ ĐỎ = BUG PROD**" (S155) · "🔴 1 BUG-FOUND (REPORTED em-main, NOT fixed)" (S113) · "**Spec-drift FLAGGED (không phải bug)**: `ExpectedHeaderTokens` = BEST-GUESS ≠ 30 real row-4 token" (D3) · "⚠️**SEED-FK: PurchaseEvaluationQuote HARD FK PESupplierId→Supplier Restrict (cfg:135)**" (D2) · "⚠️OBSERVATION: isSystem KHÔNG reachable qua public ApproveV2Async … defensive/dead" (S72) · "⚠️`ApprovalWorkflowId` FK Restrict → PE seed PHẢI ref WF thật" (S85b) · "⚠️**actor-exclusion KHÔNG che bug**" (S86) · "⚠️Changelog.UserId Guid? no FK User → random-Guid an toàn" (S87) · "⚠️GUARD-FIRST: bypass-test PHẢI dựng PE đủ 4 ĐK" (S60) · "Bắt được lỗi spec: query THẬT = `GetPurchaseEvaluationQuery` … spec ghi nhầm" (S133 INV-A) · "[superseded by S77c for urgent SET/UNSET asymmetry]" (S69b) · "(1 FAIL GIU DO = bug prod)" (tiêu-đề S155) · "**baseline 509 KHÔNG đổi**" (S153 — xem mâu-thuẫn ở TS-A)
- **guard:** `SoftDeleteFixture` (fixture ĐẦU TIÊN wire `AuditingInterceptor`) · "2 sanity-assert chống vacuous (Phase==DaDuyet + liveRec đổi thật) TRƯỚC assert chính" (S133) · "+CONTRAST assert … (chống vacuous-pass)" (S118) · "CONTROL/đối-chứng" ở S116 (`[+1.2tỷ]` alone) · "Forbidden red-able BY-CONTRAST (non-IT vs IT-staff identical-setup)" (S54) · submit-guard `:202-208` (S116) · publish-guard (S113) · "guard R2-revised" (S116) · "wire-bug-guard" (S117) · "fail-closed order (832-851)" (S72) · "role-gate 152-157 TRƯỚC side-effect" (S74) · "GUARD-FIRST" (S60) · `PurchaseEvaluationDraftGuard`-tương-đương: "T5 guard CCM+Admin Conflict + ChoDuyet-allow" (S133 `:638`) · "authz-capability … CanReassign flag + Forbidden/Conflict guard + empty-staff 0-leak + no-mutation" (S54) · "Pattern 10 reflection static `[Authorize]`" (S118b) · "T4 `(attr?.Policy).Should().BeNull()`" (S118b — rào NGƯỢC: cấm thêm policy) · "invariant T6"-tương-đương: "Pe-module-NOT-revoked invariant" (S92) · "REPORT lead, KHÔNG tự sửa" (S155, S153) · "repo read-only … mọi mutation trong cây tạm qua seam `--root`" (S153) · "No prod touch — verify qua stash, restore exact" (S86) · "KHÔNG touch prod" (S113/S116/S117/S118/S118b)
- **root-cause:** ① S155 — "`Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất" ② S153 — "`inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng" ③ S113 — "`NewSupplier` `Code=r.Code!.Trim()` → NRE khi `row.Code==NULL`" ④ D3/S113 — "dedup ở SERVICE OrdinalIgnoreCase, KHÔNG dựa DB — SQLite fixture BINARY collation KHÔNG bắt CI-dup, prod SQL Server CI thì có" ⑤ S117 — "optional param KHÔNG auto-bind qua `MethodInfo.Invoke`" ⑥ S92 — "test invoke method-CÔ-LẬP qua reflection … KHÔNG chạy SeedAsync chain"
- **file:line:** `PeSoftDeleteFeatures.cs:126` + `:142` · `PeBudgetFreezeTests.cs:184` · `:426` `:480` `:524` `:555` `:638` `:687` `:730` `:783` `:821` + seed `:277-336` · `CreateContractFromEvaluationFeatures.cs:196` · `PurchaseEvaluationFeatures.cs:285` (UpdateDraft) + `:849-864` (FullAmount) · `PurchaseEvaluationWorkflowService.LogTransitionAsync (~:1083-1135)` · submit-guard `:202-208` · `ApproveV2Async 816-854` (strict-`<` @838) + `832-851` · role-gate `152-157` · Quote FK `cfg:135` · call-site `PeWorkItemBudgetTests 388/407/421` · `PurchaseEvaluationUrgentFeatures.cs:49-55` · `Api/AuthorizePolicyRegressionTests.cs` (15→19 [Fact]) · csproj `line 25` · `scripts/session_ctx.py mind-check`
- **tên file test (định-danh):** `PeDeleteByApproverTests.cs` · `PePendingAccumulationTests.cs` · `PeBudgetFreezeTests.cs` · `SupplierImportV2DedupTests.cs` · `SupplierPublishAndListTests.cs` · `Services/SupplierExcelImportServiceTests.cs` · `ProcurementMasterAccessSeedTests.cs` · `AdminOnlyModulesRevokeTests.cs` · `OfficeModulePermissionSeedTests.cs` · `HrmProfilePermissionSeedTests.cs` · `PeSubmitGuardAndBypassTests.cs` · `PeCcmThresholdFinalizeTests.cs` · `PeApprovedPriceFinalizeTests.cs` · `PeSuggestedPriceSetterAuthzTests.cs` · `PeUrgentToggleAuthzTests.cs` · `PeCcmBudgetPeriodSetterAuthzTests.cs` · `PeApproverNotifyExcludesDrafterTests.cs` · `PeUpdateDraftWorkflowPreserveTests.cs` · `PeMultiWinnerTests` · `PeWorkItemBudgetTests` · `PeFinalizeProjectionTests` · `PeApproverFinalizeTests` · `CreateContractFromEvaluationMultiWinnerTests.cs` · `PeListWinnerNamesProjectionTests.cs` · `ItTicketReassignAuthzTests.cs` · `SqliteDbFixture` / `TestApplicationDbContext` / `IdentityFixture` / `CapturingNotificationService`
- **định-danh run/engine:** `run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4` (diary 13.9KB) · `run 2026-07-26-S153-ctx-adopt` `[engine: opus-5]` · `wf_97d6a582-b7e` `[engine: opus-worker]` (S134) · `wf_75c6bfd3-754` (S133 INV-C, **#53 return-fail lần 3**) · `wf_d9639faa-97a` (S133 INV-A, return sạch) · Mig 54/55/56/57/58/59/60/61/65/66/67/68 được dẫn rải rác
- **nguồn-đếm (ghi-chú drafter @S170):** đĩa đo 30 heading `##` = 1 dòng "Count progression" + 29 entry-heading (S155 → S54, gồm cả S60 và heading gộp "S57bis / S56 (notes kept)"); proposal curator khai "25 entry". Entry S60 KHÔNG thuộc cụm narrative nào của curator — nó sống ở mốc mở chuỗi "254 (S60)" (TS-A) + token GUARD-FIRST ở phụ-lục này. Chi tiết → `_lienfile-notes-batchB.md` §bổ-sung.

### Bổ-sung coverage @P3-gate S170 — ⚠️ LƯỚI GREP THUẦN, KHÔNG PHẢI DỮ KIỆN (đoạn trích sau mỗi token là phần ĐẦU DÒNG verbatim chứa token — dòng archive là mega-line nên đoạn trích thường KHÔNG liên-quan token; CHỈ dùng để coverage-diff grep trúng, đọc nội-dung thì tra verbatim theo token — reviewer P3 @S170)

- `#6` — **S97 (2026-07-01) PE EndsBeforeCeo CONFIG→RUNTIME (bug anh Kiệt FDC, spec-change test) — 440 PASS no-red, NET +0 [Fact] (extend in-place):** em…
- `Mig 55` — ## S74 — PE Mig 55 CcmNote (compile-fix arity + test-after), +5…
- `Mig 56` — ## S76 — PE Mig 56 PRO-column-split (spec-change + compile-fix arity), +5…
- `Mig 57` — ## S77 — PE Mig 57 suggested-price NOTE (test-after, mirror CcmNote), +7…
- `Mig 58` — ## D2 — PE Mig 58 multi-winner (test-after + EF translation-smoke), +6 → 419…
- `Mig 59` — ## S87 — PE Mig 59 CCM-budget-period setter (test-after authz), +7…
- `Mig 60` — **S97 (2026-07-01) PE EndsBeforeCeo CONFIG→RUNTIME (bug anh Kiệt FDC, spec-change test) — 440 PASS no-red, NET +0 [Fact] (extend in-place):** em…
- `PurchaseEvaluationFeatures.cs:849` — `UpdatePeBudgetProCommand` record 3→4 param `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)`; handler set ProInitial+ProAdjust absolute-set …
