[CLAUDE] Docs: S91 closeout — go-live wipe + seed-gate + user-change-password + backup (gotcha #75/#76)

Closeout phiên go-live 26/06 (4 commit prod-verified đã push: guide-fix 159b69d · wipe ba42c59
· seed-gate 55494ad · user-change-password e81e87f + backup local).
- STATUS/HANDOFF +S91 entry, state re-ground: Mig 59 · 88 bang · 431 test (45D+386I) · gotcha 76
  · menu 54 · bundle admin DgQyuG7f/user BmbQon23 · prod 17 user/7 NCC/0 PE-HD
- gotchas #75 (SeedDemoMasterData ungated per-code -> resurrect NCC demo moi restart)
  + #76 (deploy render appsettings.Production.json tu .example, KHONG file gitignored)
- session log 2026-06-26-S91 + feedback_wipe_durability_check_reseed (user memory)
- harvest cicd-monitor + test-specialist diary (self-written, verified)
- .gitignore +backups/ +*.bak

Docs/memory-only -> CI skip (paths-ignore docs/** + **/*.md + .gitignore).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-26 13:07:01 +07:00
parent e81e87ff9f
commit f98a743074
9 changed files with 148 additions and 28 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,59 @@
---
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 backfillProInitial.
## S74 — PE Mig 55 CcmNote (compile-fix arity + test-after), +5
`PeWorkItemBudget.CcmNote` (nvarchar1000) + `UpdatePeBudgetCcmCommand` record 34 `(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 setpersist / null=clear (pre-seed prove) / ProcurementForbidden+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 AUTOOPT-IN UPDATE `PeCcmThresholdFinalizeTests` 611:** chỉ finalize khi flag=true + fail-closed order (832-851: threshold-nullConflict / roleCostControlForbidden / 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ưỡngDaDuyet / ==ngưỡngadvance / >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.