---
name: baseline-history
description: L2 archive — verbose test-count history chain + aged FIFO entries (S54→D2). On-demand only; MEMORY.md index keeps one-liners.
metadata:
  type: project
---

# Baseline count history (verbose) + aged FIFO

> Moved out of `MEMORY.md` index 2026-06-26 (index over 24KB read cap). Each entry was a full test-design note; detail also recoverable from the test files + session logs. Newest first.

## Count progression (one-line chain)
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)**.

## D2 — PE Mig 58 multi-winner (test-after + EF translation-smoke), +6 → 419
`CreateContractFromEvaluationMultiWinnerTests.cs` (Application ns, 5) cho `CreateContractFromEvaluationCommand` single→multi-winner trả `List<Guid>` (1 HĐ per đơn vị IsWinner). Handler 4-dep `(db, ICurrentUser, IContractWorkflowService, IContractCodeGenerator)` wire THẬT (mirror CreateContractCommandApplicableTypeTests S33): ContractCodeGenerator(db,clock)+ContractWorkflowService — BeginTransactionAsync SERIALIZABLE chạy SẠCH SQLite, KHÔNG try/skip. IdentityFixture. 5 case: (a)≥2 winner→2 ids·2 Contract·Phase=DangSoanThao·SupplierId khớp winner·**GiaTri per-winner = SUM ThanhTien báo giá của ĐÚNG winner (q.PESupplierId==winnerRow.Id)** 140/260·2 mã DISTINCT·pe.ContractId==ids[0] / (b)single→**GiaTri = SUM details.ThanhTienNganSach (LEGACY 800, NOT quote-total 140)**·1 HĐ·TenHopDong null→fallback TenGoiThau / (c)idempotency→ConflictException`*đã tạo HĐ rồi*`+count 2 / (d)0 winner→Conflict`*chưa chọn NCC/TP trúng thầu*`+0 HĐ / (e)not-DaDuyet→Conflict`*DaDuyet*`+0 HĐ. ⚠️**SEED-FK: PurchaseEvaluationQuote HARD FK PESupplierId→Supplier Restrict (cfg:135)** → mỗi Quote ref 1 PESupplier-row thật (random Guid→SQLite 19); Contract.SupplierId/ProjectId no FK vật lý nhưng handler load Project(NotFound)+supplierMap→seed real. `PeListWinnerNamesProjectionTests.cs` (1) TRANSLATION-SMOKE cho EF subquery `WinnerSupplierNames` collection-projection → **EF TRANSLATE OK no InvalidOperationException trên SQLite**: 2/1/0-winner. SqliteDbFixture read-only. No prod bug.

## S87 — PE Mig 59 CCM-budget-period setter (test-after authz), +7
`PeCcmBudgetPeriodSetterAuthzTests.cs` cho `SetPeCcmBudgetPeriodCommand` — Section B 3-cột Dự án|PRO|CCM, CCM nhập "NS kỳ này" cột riêng độc-lập PRO. Handler 2-dep: NotFound→role-gate Admin|CostControl else Forbidden`*CCM*`→absolute-set CcmBudgetPeriodAmount→Changelog→Save. 7 test: Admin/CostControl set · Drafter/Procurement→Forbidden+no-set · value=0 valid+validator(0-valid/âm-invalid) · null=clear (pre-seed 500tr, gotcha #73)+validator null-valid · unknownPE→NotFound TRƯỚC authz. Validator `GreaterThanOrEqualTo(0).When(HasValue)`. ⚠️Changelog.UserId Guid? no FK User → random-Guid an toàn. No prod bug.

## S86 — PE drafter-notify-exclusion REGRESSION (bug-fix test-alongside), +3
em main fix `PurchaseEvaluationWorkflowService.LogTransitionAsync` (~:1083-1135) khối notify "Phiếu cần bạn duyệt" +filter `&& l.ApproverUserId != evaluation.DrafterUserId` → người tạo phiếu KHÔNG nhận chuông cần-duyệt phiếu mình tạo. `PeApproverNotifyExcludesDrafterTests.cs` (Services ns) 3: (1)CORE 1-Step-2-Level Cấp1=approverL1(actor) Cấp2={drafter,otherL2}; actorL1 Approve→advance→NeedApproval recipients EXCLUDE drafter INCLUDE otherL2 / (2)Cấp2=CHỈ drafter→recipients EMPTY / (3)SANITY terminal DaDuyet drafter VẪN nhận chuông KẾT-QUẢ. ⚠️**actor-exclusion KHÔNG che bug** (actor=Cấp1, drafter=Cấp2). **RED-on-old-code PROVEN qua git stash**. Spy=`CapturingNotificationService` (records recipients+title). No prod touch (verify qua stash, restore exact).

## S85b — PE UpdateDraft WF-preserve ROOT-FIX bug-class S42 (test-after bug-fix), +3
em main fix `UpdatePurchaseEvaluationDraftCommandHandler` (PurchaseEvaluationFeatures.cs:285) ABSOLUTE-SET→NULL-SAFE `if(request.ApprovalWorkflowId is not null)`. `PeUpdateDraftWorkflowPreserveTests.cs` 3: (1)PRESERVE Nháp WF=W + null request → GIỮ W + sanity update / (2)CHANGE W1→W2 (≠null)→đổi / (3)bonus BudgetPeriodAmount null-safe S61. ⚠️`ApprovalWorkflowId` FK Restrict → PE seed PHẢI ref WF thật (`SeedWorkflowAsync` ApplicableType=DuyetNcc). No prod bug sau fix.

## S85 — PE urgent DRAFTER-self-unset (test-after authz), +5
em main restructure `PurchaseEvaluationUrgentFeatures.cs` thêm nhánh UNSET cho NGƯỜI KHAI (`isDrafter = currentUser.UserId == entity.DrafterUserId`) → drafter GỠ clear CẢ 2 cờ, SET vẫn role-only. EXTEND `PeUrgentToggleAuthzTests.cs` 11→16: FakeCurrentUser.UserId→init + SeedPeAsync +drafterUserId. NEW 5: Drafter UNSET→clear cả 2 no-Forbidden / Finance UNSET→Forbidden`*GỠ*` / Drafter SET→Forbidden`*đánh dấu*` / Drafter UNSET chỉ-ByCcm→clear cả 2 / Drafter+Procurement-no-DM UNSET→clear CẢ 2 qua isDrafter (isDrafter ưu tiên/độc lập isProTp). No prod bug.

## S77c — urgent-toggle SYMMETRIC→ASYMMETRIC CORRECTION
anh chốt FINAL gate BẤT-ĐỐI-XỨNG theo `request.IsUrgent`; em main sửa prod `PurchaseEvaluationUrgentFeatures.cs:49-55` `isPro = IsUrgent ? hasPro : (hasPro && isDeptManager)` (+isCcm). REWRITE `PeUrgentToggleAuthzTests.cs` 12→11: **SET = role-chức-năng-đủ** (plain Procurement/CostControl SET→OK; bỏ DeptManager-restriction) / **UNSET = role AND DeptManager** (plain-PRO/CCM UNSET→Forbidden+no-mutate; PRO+DM/CCM+DM UNSET→clear-own preserve-other; Admin→both) / Drafter/Finance SET→Forbidden / UnknownPe→NotFound. No prod touch — test theo CODE (S34).

## S77 — PE Mig 57 suggested-price NOTE (test-after, mirror CcmNote), +7
`UpdatePeSuggestedPricePro/CcmCommand` +`Note` (string?) absolute-set→`Pro/CcmSuggestedPriceNote` (incl null=clear); rides SAME role-gate as price (PRO/Admin · CCM/Admin) fail-closed TRƯỚC side-effect; validator `Note.MaximumLength(1000)`. Handler ctor 2-dep + record `Note=null` default → 5 existing call-sites still compile, KHÔNG cần vá arity. NEW 7 → `PeSuggestedPriceSetterAuthzTests.cs`: PRO-note+Min/Max persist (note KHÔNG clobber price) / Admin-PRO / CCM-note+price / null-clear PRO (pre-seed note→null, AsNoTracking re-read) / null-clear CCM / non-priv→Forbidden+unchanged / PRO↔CCM independent + validator 1001-invalid·1000-valid. No prod bug.

## S76 — PE Mig 56 PRO-column-split (spec-change + compile-fix arity), +5
`UpdatePeBudgetProCommand` record 3→4 param `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)`; handler set ProInitial+ProAdjust absolute-set (KHÔNG còn ProEstimateAmount). Validator ProInitial>=0, ProAdjustment ÂM OK, ProNote max1000. Handler ctor 2-dep UNCHANGED. Capability `FullAmount` (PurchaseEvaluationFeatures.cs:849-864): hasCcm?CCM:proFull(ProInitial+ProAdjust); FullIsEstimate=!hasCcm&&hasPro; DTO +2 field. Compile-fix 5 PRO call-site +arg ProAdjust + assert `.ProEstimateAmount`→`.ProInitialAmount` + full-fallback seed 500→ProInitial. NEW 5: PRO set both ÂM-adjust persist / validator ProInitial<0 invalid / ProAdjust ÂM valid / full=proFull(150) / full=ÂM-adjust(70 no-clamp). No prod bug — ProEstimateAmount=LEGACY backfill→ProInitial.

## S74 — PE Mig 55 CcmNote (compile-fix arity + test-after), +5
`PeWorkItemBudget.CcmNote` (nvarchar1000) + `UpdatePeBudgetCcmCommand` record 3→4 `(PeId,Initial,Adjustment,CcmNote)` + DTO +CcmNote. Handler ctor 2-dep UNCHANGED. Compile-fix 3 call-site (PeWorkItemBudgetTests 388/407/421) +`null` arg4. NEW 5 (region 4b mirror ProNote): CostControl/Admin set→persist / null=clear (pre-seed prove) / Procurement→Forbidden+no-mutate (role-gate 152-157 TRƯỚC side-effect, AsNoTracking re-read) / all-persist-together. No prod bug.

## S72 — PE Mig 54 anh Kiệt FDC (spec-change + 2 test-after FINANCIAL), +28
Mig 54 PE fields: ProSuggestedMin/MaxPrice + CcmSuggestedPrice + ApprovedPriceAmount + ApprovedPriceSource; TransitionAsync/ApproveV2Async +3 param finalizeByCcmDelegation(bool)+approvedPriceAmount(decimal?)+approvedPriceSource(string?). **① CCM-finalize AUTO→OPT-IN — UPDATE `PeCcmThresholdFinalizeTests` 6→11:** chỉ finalize khi flag=true + fail-closed order (832-851: threshold-null→Conflict / role≠CostControl→Forbidden / winnerQuoteTotal>=ceoThreshold strict-`<`→Conflict) → ApplyApprovedPriceOnFinalize(human price BẮT BUỘC)→DaDuyet. **② NEW `PeApprovedPriceFinalizeTests` 10 — ApplyApprovedPriceOnFinalize private-static:** valid-price→bind / null-price→Conflict NOT-finalized / garbage-source→Conflict / Theory 4 source {Ncc,ProMin,ProMax,Ccm} / **isSystem null-price→no-throw qua REFLECTION** (⚠️OBSERVATION: isSystem KHÔNG reachable qua public ApproveV2Async — approve gate decision==Approve vs isSystem cần AutoApprove; PE no SLA-job → defensive/dead, test UNIT). Reflection unwrap TargetInvocationException→inner. **③ NEW `PeSuggestedPriceSetterAuthzTests` 13:** PRO/CCM setter handler 2-dep authz + validator + NotFound-TRƯỚC-authz. No prod bug — OPT-IN an-toàn-hơn AUTO.

## S69b — PE 2 feature anh Kiệt FDC (test-before-merge SECURITY+FINANCIAL), +14
**FEATURE B value-threshold CCM-finalize (`PeCcmThresholdFinalizeTests.cs` 5, ApproveV2Async 816-854):** CCM duyệt + ceoThreshold!=null + `winnerQuoteTotal < ngưỡng` STRICT-less-than (838) + chưa-slot-cuối → DaDuyet skip-CEO. Cover: <ngưỡng→DaDuyet / ==ngưỡng→advance / >ngưỡng→advance / threshold-null→advance / non-CCM→advance / CCM-last-slot→DaDuyet via normal-advance no-double. **FEATURE A urgent-toggle authz (`PeUrgentToggleAuthzTests.cs` 9, 4-dep):** PRO→ByPro / CCM→ByCcm / Admin→both / else→Forbidden. multi-role PRO+CCM no-Admin→else-if short-circuit chỉ ByPro. unknown→NotFound. No prod bug (strict-`<` + else-if priority intentional). [superseded by S77c for urgent SET/UNSET asymmetry]

## S69 — Office golive permission-seed regression (test-after SECURITY), +6
`OfficeModulePermissionSeedTests.cs` mirror HrmProfilePermissionSeedTests reflection (2 private-static revoke→seed via GetMethod NonPublic|Static; MenuItem TRƯỚC Permission FK Cascade). KHÁC HRM: Office grant Read AND Create trên 16-key allow-list. Load-bearing: excluded-3 (OffPhongHopManage/OffAttendanceReport/OffChamCong) stay hidden + upgrade-only preserves admin-raised. No prod bug.

## S60 — UAT anh Kiệt 2 feature PE submit branch (test-after), +14
`PeSubmitGuardAndBypassTests.cs`. **F1 Section 3 guard (8):** submit branch build `missing` 4 mục → ConflictException prefix `'Chưa đủ thông tin mục 3 ...'` join `' · '`. Cover thiếu cả 4 / winner-only / quote=0 / budget / comparison / attachment-gắn-NCC(PES_Id!=null) KHÔNG đếm / đủ-manual / đủ-BudgetId. **F2 drafter-bypass (6, V2-only):** k=drafterSlots.Max(Order) → auto Cấp 1..k. ⚠️GUARD-FIRST: bypass-test PHẢI dựng PE đủ 4 ĐK. Seed: SeedWinnerWithQuoteAsync / SeedComparisonAttachment(PES_Id=null) / SeedWorkflowAsync(Guid[][]). Opinion-only-ownSlot invariant. No prod bug.

## S57bis / S56 (notes kept)
- S57bis spec-drift: `NotEmpty()` on `Guid?` does NOT catch `Guid.Empty` (FV 7.2 treats default(Guid?)==null) → relies on create-handler FK-guard (defense-in-depth, no prod bug).
- S56 gotcha: `ExecuteUpdateAsync` BYPASS change tracker → stale-tracked-read needs `.AsNoTracking()` re-read; shipped tx = IsolationLevel.Serializable per em-main post-review.

## S54 — ItTicket reassign authz (test-before-merge SECURITY), +13
`ItTicketReassignAuthzTests.cs`. Pattern: authz-capability = seed 2-dept (IT+KT) + fake ICurrentUser role/dept matrix → CanReassign flag + Forbidden/Conflict guard + empty-staff 0-leak + no-mutation. Forbidden red-able BY-CONTRAST (non-IT vs IT-staff identical-setup). Data-driven handler-authz = enforcement point (KHÁC Pattern 10 reflection static `[Authorize]`). No prod bug.
