# Test-Specialist Agent — Persistent Memory

> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
> **NEW agent S39 (2026-05-29)** — dedicated test layer (tách khỏi implementer Case 3).

---

## 🎯 Role baseline

WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQLite TestApplicationDbContext + IdentityFixture. Tools: Read, Edit, Write, Bash, Grep, Glob + 5 RAG. Skills: `contract-workflow` + `permission-matrix`.

## 🚫 Split boundary
- ✅ MINE: `tests/SolutionErp.{Domain,Infrastructure}.Tests/**`
- ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix
- ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority

## 📊 Baseline 354 tests = 354 PASS (45 Domain + 309 Infra)  ← S77c CORRECTION urgent-toggle SYMMETRIC→ASYMMETRIC (S77b symmetric tests RED vs prod): 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 = IsUrgent ? hasCcm : (hasCcm && isDeptManager)`; Forbidden-msg nhánh-theo-IsUrgent (SET `*đánh dấu phiếu gấp*` / UNSET `*GỠ cờ gấp*`). REWRITE `PeUrgentToggleAuthzTests.cs` 12→11: **SET = role-chức-năng-đủ** (plain Procurement SET→OK ByPro / plain CostControl SET→OK ByCcm / Admin SET→both) — bỏ hết 2b/2c/2d DeptManager-alone-restriction của S77b (không còn áp cho SET); **UNSET = role AND DeptManager** (plain-PRO UNSET→Forbidden`*GỠ*`+no-mutate flag-still-true pre-seed urgent / plain-CCM UNSET→Forbidden / PRO+DM UNSET→clear-ByPro CCM-preserved / CCM+DM UNSET→clear-ByCcm PRO-preserved / Admin UNSET→both-clear); 9a Drafter-SET→Forbidden`*đánh dấu*` / 9b Finance-SET→Forbidden / 9c UnknownPe→NotFound TRƯỚC authz. No prod touch — prod ĐÃ asymmetric, test theo CODE (S34). Prev S77b 354 (symmetric, superseded)  ← S77 +7 PE Mig 57 suggested-price NOTE (test-after, mirror S74 §4b CcmNote): `UpdatePeSuggestedPricePro/CcmCommand` +`Note` (string?) absolute-set→`Pro/CcmSuggestedPriceNote` (overwrite always 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 `(db, ICurrentUser)` 2-dep + record arity UNCHANGED on existing args (Note=null default → 5 existing `new UpdatePeSuggestedPrice*Command(...)` call-sites still compile, KHÔNG cần vá). NEW 7 → `PeSuggestedPriceSetterAuthzTests.cs`: PRO-note+Min/Max persist (note KHÔNG clobber price) / Admin-PRO-note / CCM-note+price persist / null-clear PRO (pre-seed note→null=cleared) / null-clear CCM / non-priv→Forbidden+note&price unchanged (no partial-write) / PRO↔CCM note independent + validator 1001-char invalid·1000 valid. No prod bug — code đúng spec (note absolute-set + rides price gate, intentional). Prev 344  ← S76 +5 PE Mig 56 PRO-column-split (spec-change+compile-fix arity): UpdatePeBudgetProCommand record 3→4 param `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)`; handler set ProInitial+ProAdjust absolute-set (KHÔNG còn ProEstimateAmount). Vá 5 PRO call-site `new UpdatePeBudgetProCommand(...)` thêm arg ProAdjust + rename assert `.ProEstimateAmount`→`.ProInitialAmount` (handler giờ set ProInitial). Capability `FullAmount`: hasCcm?CCM:proFull(ProInitial+ProAdjust); FullIsEstimate=!hasCcm&&hasPro; DTO +2 field cuối ProInitial/ProAdjust. NEW 5: PRO set both incl ProAdjust ÂM persist / validator ProInitial<0 invalid + ProAdjust ÂM valid (mirror CCM no-sign-constraint) / full=proFull(100+50=150)+DTO-surface / full=proFull ÂM-adjust(100-30=70 no-clamp). Full-fallback seed `ProEstimateAmount=500`→`ProInitialAmount=500` (capability đọc ProInitial). **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED**. ProEstimateAmount=LEGACY (Mig 56 backfill→ProInitial, FE bỏ). No prod bug. Prev 339  ← S74 +5 PE Mig 55 CcmNote (compile-fix arity + test-after): UpdatePeBudgetCcmCommand record +`string? CcmNote` (4 param) → fix 3 existing `new UpdatePeBudgetCcmCommand(...)` thêm `null` arg4 (PeWorkItemBudgetTests.cs line 388/407/421); NEW 5 test mirror ProNote: CostControl-set / Admin-set / null=clear-absolute-set (pre-seed prove not vốn-dĩ-null) / Procurement→Forbidden no-mutate (fail-closed role-gate TRƯỚC EnsureTrackedAsync+side-effect, AsNoTracking re-read) / Initial+Adjustment+CcmNote all-persist-together. **Handler ctor `(db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record +CcmNote. No prod bug. Prev 334  ← S72 +28 PE Mig 54 (spec-change+test-after): PeCcmThresholdFinalizeTests 6→11 (AUTO→OPT-IN finalizeByCcmDelegation) + NEW PeApprovedPriceFinalizeTests 10 (giá chốt) + NEW PeSuggestedPriceSetterAuthzTests 13 (2 setter role-gate). Prev 306  ← S69b +14 PE 2 feature anh Kiệt FDC (test-before-merge SECURITY/FINANCIAL): `PeCcmThresholdFinalizeTests.cs` (5, Services ns, value-threshold CCM-finalize ApproveV2Async) + `PeUrgentToggleAuthzTests.cs` (9, Application ns, urgent-toggle role authz). Prev 292 ← S69 +6 Office golive permission-seed (`OfficeModulePermissionSeedTests.cs`, test-after, mirror HrmProfilePermissionSeedTests S67). Prev 286 ← S67 +23 HRM test-after [DepartmentTreeTests 8 cycle-guard/rollup/orphan + PeHoSoLinkTests 9 absolute-set (⚠️spec-drift: HoSoLink gửi null=CLEAR, KHÔNG null-safe như Budget*/WorkItemId) + HrmProfilePermissionSeedTests 6 reflection private-static revoke→seed chain]. **em main PROXY-RECORD** — return truncated #53 (chết lúc update MEMORY), 3 file delivered + `dotnet test` 286 PASS verify-on-disk. Prev 263 (S61 +22 PeWorkItemBudget −14 BudgetPolicy; Domain 58→45 drop Budget module). Pre = 254 (S60).
> Pattern S67: private-static seed/init → invoke qua REFLECTION (`GetMethod(name, NonPublic|Static)` + `Invoke(null, [db, roleManager, NullLogger.Instance])`); seed MenuItem rows TRƯỚC Permission (FK MenuKey→MenuItem.Key Cascade, SQLite Error 19 nếu thiếu). Cycle-guard test: SqliteDbFixture đủ (no User); rollup-count test cần IdentityFixture (đếm User.DepartmentId active).
Run: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1` (MSBuild OOM → serialize build)

### ⚠️ Pattern: deduction hook FK → seed LeaveType cho terminal test (S43)
LeaveBalance→LeaveType `Restrict` FK. ApproveLeaveRequest terminal (DaDuyet) insert LeaveBalance → test tới DaDuyet PHẢI seed 1 LeaveType + LeaveRequest.LeaveTypeId=type.Id (random Guid→FK fail SQLite 19). Non-terminal (advance/reject/return/OtRequest) KHÔNG cần. BuildLeave optional `leaveTypeId` default random. Year=StartDate.Year. Negative OK (no quota guard). Query lazy-synth Entitled=DaysPerYear khi 0 row.

## ⏱️ Timing rules (docs/rules.md §7)
- Feature mới = test-after (UAT ổn → viết, Phase 9 skip per `feedback_uat_skip_verify`)
- Bug fix = test-before BẮT BUỘC (reproduce → fix)
- Critical algo = test-before merge (codegen/guard/financial/security)
- Skip: DTO mapping, CRUD master, FE snapshot

## 📋 Patterns proven (apply confidently)

### Pattern 10 Reflection authz regression (~50 LOC)
Catch class-level `[Authorize(Policy=...)]` regression: `typeof(Ctrl).GetCustomAttribute<AuthorizeAttribute>().Policy.Should().Be(...)`. KHÔNG WebApplicationFactory heavy. Cho gotcha #44 silent 403.

### Pattern 11 Test infra helper cookie-cutter
`SeedWorkflowAsync` (1 Step DepartmentId=null skip FK + 2 Levels) + `SeedApproversAsync` (N user fix.CreateUserAsync). Reusable PE/Contract/Proposal workflow test.

### Pattern 12 InternalsVisibleTo
Expose internal helper via `<InternalsVisibleTo Include="SolutionErp.Infrastructure.Tests" />` csproj.

### Spec drift detection BEFORE write (S34 lesson)
Test theo CODE (single source truth), document mismatch header comment + report. Vd soft-delete UNIQUE: code chặn opt-out → test theo code, flag drift.

### gotcha #48 SQLite tie-break
`OrderByDescending(CreatedAt).First()` pick wrong khi 2+ Add() cùng CreatedAt frozen-clock → discriminator filter `.Where(Summary.Contains("Chuyển phase"))` BEFORE OrderBy.

## 🎯 Coverage gap backlog (priority — Reviewer flagged S36)
1. ✅ **DONE S45** — HrmConfig Holiday composite UNIQUE (Year,Date): 7 test (`HrmConfigHolidayTests.cs`) + surfaced Mig 43 filtered-index fix
2. ✅ **DONE S45** — EmployeeSatellite FK invariant + soft-delete + cascade: 10 test (`EmployeeSatelliteTests.cs`)
3. ✅ **DONE S45** — gotcha #44 authz regression EmployeesController + HrmConfigsController: 10 test (extend `AuthorizePolicyRegressionTests.cs`)
4. Phase 10.3 Proposal ApproveV2 (S37) + Workflow Apps skeleton (S38) — test-after khi UAT confirm
5. **gotcha #57 (S51+S52 REPRODUCED):** HRM `HrmConfigFilteredUniqueTests` 2 RED (LeaveType+Shift+OtPolicy) → em main Mig 45 `.HasFilter` GREEN. **S52 EXT Master** `MasterCatalogFilteredUniqueTests` 3 RED (Department cfg:18 / Project:19 / Supplier:24 bare `.IsUnique()`) → pending em main fix migration. Vehicle+Driver (Mig 44) ĐÃ filtered. Pattern: seed `IsDeleted=true` slot + Create cùng Code → assert active==1 + `IgnoreQueryFilters` all==2.

## 📅 Recent activity (last 10 FIFO)

- **2026-06-19 (S77 PE Mig 57 suggested-price NOTE — test-after, mirror S74 §4b CcmNote):** +7 test `tests/.../Application/PeSuggestedPriceSetterAuthzTests.cs` → **344→351 PASS** (45 Domain + Infra 299→306, 0 fail). BE done+builds (Mig 57 `AddPeSuggestedPriceNotes`, em main; entity `PurchaseEvaluation` +`ProSuggestedPriceNote`/`CcmSuggestedPriceNote` string? nvarchar(1000)). **SPEC:** `UpdatePeSuggestedPriceProCommand` +`Note` (string?, 4th param, default null) → absolute-set `pe.ProSuggestedPriceNote` (overwrite always incl null=clear, line 67); `UpdatePeSuggestedPriceCcmCommand` +`Note` (3rd param) → `pe.CcmSuggestedPriceNote` (line 127). Note **rides SAME role-gate as price** (PRO/Admin · CCM/Admin) — gate fail-closed AFTER NotFound, BEFORE mutate (PeSuggestedPriceFeatures.cs: NotFound 51-52 → role-gate 55-60/117-122 → set 65-67/126-127). Validator `Note.MaximumLength(1000)`. **⚠️ Handler ctor `(IApplicationDbContext db, ICurrentUser)` 2-dep UNCHANGED + record `Note=null` default → 5 existing `new UpdatePeSuggestedPrice*Command(...)` call-sites still compile, KHÔNG cần vá arity** (khác S74/S76 phải vá). **NEW 7:** (1) PRO Procurement set note + Min/Max all-persist (note KHÔNG clobber price) / (1b) Admin set PRO note / (2) CCM CostControl set note + price / (3) null-clear PRO (pre-seed note="cũ" trên tracked-pe → Note=null → cleared, AsNoTracking re-read; same-context tracked-instance pattern §4b) / (3b) null-clear CCM / (4) non-priv (CostControl→PRO-cmd) → Forbidden + note&price unchanged (no partial-write, fail-closed) / (5) PRO↔CCM note independent (set both seq, neither clobbers) + validator 1001-char invalid · 1000 valid both cmds. **No prod bug** — code đúng spec (note absolute-set + rides existing price gate, intentional; mirror Pro/CcmNote ngân sách S61/S74). Reuse `FakeCurrentUser(params roles)` + `SeedPeAsync` helper sẵn có trong file (S73). Tag [s77, pe-mig57, suggested-price-note, absolute-set-null-clear, note-rides-price-gate, fail-closed-no-mutate, no-arity-fix-needed, mirror-ccmnote, test-after].
- **2026-06-19 (S76 PE Mig 56 PRO-column-split — SPEC-CHANGE + compile-fix arity + test-after):** +5 test `PeWorkItemBudgetTests.cs` → **339→344 PASS** (45 Domain + Infra 294→299, 0 fail). BE done+builds (Mig 56 `AddProBudgetSplitToPeWorkItemBudget`, em main). **SPEC:** `UpdatePeBudgetProCommand` record 3→**4 param** `(PeId, ProInitialAmount, ProAdjustmentAmount, ProNote)` (mirror CCM Initial/Adjustment); handler set `rec.ProInitialAmount + rec.ProAdjustmentAmount` absolute-set (KHÔNG còn `ProEstimateAmount`). Validator `ProInitialAmount>=0` when HasValue, `ProAdjustmentAmount` KHÔNG ràng dấu (ÂM OK), ProNote max1000. **⚠️ Handler ctor `(IApplicationDbContext db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record đổi arity. Capability `PeBudgetSummaryDto.FullAmount` (PurchaseEvaluationFeatures.cs:849-864): `hasPro = ProInitial∥ProAdjust not-null`; `proFull = ProInitial+ProAdjust`; `full = hasCcm ? CCM(Initial+Adjustment) : proFull`; `FullIsEstimate = !hasCcm && hasPro`. DTO +2 field cuối `ProInitialAmount`,`ProAdjustmentAmount`. **Compile-fix:** 5 PRO call-site `new UpdatePeBudgetProCommand(...)` thêm arg ProAdjust (regex `\([^,]+,[^,]+,[^,)]+\)` = 0 hit sau vá xác nhận hết 3-arg). **Assertion-vá:** 5 PRO-handler test `.ProEstimateAmount`→`.ProInitialAmount` (handler giờ set ProInitial). **Full-fallback seed-vá:** capability đọc ProInitial → `ProEstimateAmount=500m`→`ProInitialAmount=500m` (2 chỗ region 5 + 1 CanEdit seed). **NEW 5:** (1) PRO set both ProInitial+ProAdjust ÂM(-20tr)+note all-persist 1-lệnh / (2) validator ProInitial<0 invalid (khác ProAdjust) / (3) validator ProAdjust ÂM valid (mirror CCM no-sign-constraint) / (4) full=proFull(100+50=150)+DTO surface ProInitial/ProAdjust riêng / (5) full=proFull ÂM-adjust(100-30=70 no-clamp = tổng đại số). CCM-present test thêm seed PRO(500+20) chứng minh PRO bị bỏ qua trong full. **No prod bug** — code đúng spec (PRO mirror CCM, ProAdjust ÂM intentional, ProEstimateAmount=LEGACY Mig 56 backfill→ProInitial FE bỏ). Tag [s76, pe-mig56, pro-column-split, spec-change, compile-fix-arity, proadjust-negative-allowed, profull-no-clamp, mirror-ccm, full-fallback, test-after].
- **2026-06-18 (S74 PE Mig 55 CcmNote — compile-fix arity + test-after):** +5 test `PeWorkItemBudgetTests.cs` → **334→339 PASS** (45 Domain + Infra 289→294, 0 fail). Mig 55 additive: `PeWorkItemBudget.CcmNote` (string? nvarchar(1000)) + `UpdatePeBudgetCcmCommand` record 3→**4 param** `(PeId, InitialAmount, AdjustmentAmount, CcmNote)` + DTO `PeBudgetSummaryDto +CcmNote` (slot after AdjustmentAmount). **⚠️ Handler ctor `UpdatePeBudgetCcmCommandHandler(IApplicationDbContext db, ICurrentUser)` 2-dep UNCHANGED** — chỉ COMMAND record đổi arity, KHÔNG đụng handler signature. **Compile-fix:** grep `new UpdatePeBudgetCcmCommand` toàn tests/ = 3 hit (line 388/407/421 PeWorkItemBudgetTests), thêm `null` arg4 mỗi cái. **NEW 5 test (region 4b, mirror ProNote pattern S61):** (1) CostControl set CcmNote→persist / (2) Admin set→persist / (3) **null=clear absolute-set** (pre-seed CcmNote="cũ" → null request → BeNull, chứng minh CLEAR không phải vốn-dĩ-null) / (4) **Procurement→ForbiddenException + record no-mutate** (fail-closed: role-gate line 152-157 TRƯỚC WorkItemId-check + EnsureTrackedAsync + side-effect; AsNoTracking re-read assert CcmNote/Initial/Adjustment giữ nguyên) / (5) Initial+Adjustment+CcmNote all-persist-together 1 lệnh. Handler `rec.CcmNote = request.CcmNote` absolute-set; changelog "ghi chú CCM cập nhật" khi `oldCcmNote != request.CcmNote`. **No prod bug** — code đúng spec (mirror ProNote, fail-closed role-gate giống Initial/Adjustment). Tag [s74, pe-mig55, ccmnote, compile-fix-arity, absolute-set-null-clear, fail-closed-no-mutate, mirror-pronote, test-after].
- **2026-06-18 (S72 PE Mig 54 anh Kiệt FDC — SPEC-CHANGE + 2 test-after FINANCIAL):** +28 test → **306→334 PASS** (45 Domain + Infra 261→289, 0 fail). BE Mig 54 committed+builds. Mig 54 fields PE: `ProSuggestedMin/MaxPrice` + `CcmSuggestedPrice` + `ApprovedPriceAmount` + `ApprovedPriceSource`; `TransitionAsync`/`ApproveV2Async` +3 param `finalizeByCcmDelegation`(bool) + `approvedPriceAmount`(decimal?) + `approvedPriceSource`(string?). **① SPEC-CHANGE CCM-finalize AUTO→OPT-IN — UPDATE `PeCcmThresholdFinalizeTests` 6→11 (Services ns):** S69b AUTO (gói<ngưỡng+CCM → tự DaDuyet im-lặng) NAY chỉ finalize khi `finalizeByCcmDelegation=true` + đủ ĐK fail-closed (check theo thứ tự code 832-851: threshold-null→Conflict / role≠CostControl→Forbidden / `winnerQuoteTotal>=ceoThreshold`→Conflict, strict-`<` giữ nguyên) → ApplyApprovedPriceOnFinalize(BẮT BUỘC giá chốt human)→DaDuyet. ApproveAsync helper +3 optional param. Cover: flag-true+<ngưỡng+giá→DaDuyet skip-CEO + bind ApprovedPrice / **flag-FALSE+<ngưỡng→KHÔNG finalize advance-CEO (đổi-chính)** / flag-true ==ngưỡng→Conflict + >ngưỡng→Conflict (no-mutation reload-assert) / flag-true non-CCM→Forbidden / flag-true threshold-null→Conflict / flag-true slot-cuối+giá→DaDuyet 1-Approval no-double / flag-true đủ-ĐK null-giá→Conflict("giá chốt"). **② NEW `PeApprovedPriceFinalizeTests` 10 (Services ns) — ApplyApprovedPriceOnFinalize (private-static):** terminal DaDuyet normal-advance human valid-price→bind / null-price human→Conflict NOT-finalized(ChoDuyet reload) / garbage-source→Conflict / Theory 4 valid-source {Ncc,ProMin,ProMax,Ccm} each-set + **isSystem null-price→no-throw no-set qua REFLECTION** (⚠️OBSERVATION report: isSystem KHÔNG reachable qua public ApproveV2Async — approve-branch gate `decision==Approve` còn isSystem cần `AutoApprove`; PE no SLA-auto-job chỉ Contract SlaExpiryJob → isSystem-exempt = defensive/dead qua V2-approve; test contract mức UNIT) + đối-chứng isSystem-false→Conflict + isSystem-true amount+garbage-source→Conflict. Reflection unwrap TargetInvocationException→inner. **③ NEW `PeSuggestedPriceSetterAuthzTests` 13 (Application ns) — 2 setter handler 2-dep db+ICurrentUser mirror PeWorkItemGuard:** PRO-cmd Procurement/Admin set Min/Max·CostControl/Drafter→Forbidden+no-set·validator Min>Max invalid+negative invalid+single/null valid·unknown-PE→NotFound(existence TRƯỚC authz). CCM-cmd CostControl/Admin set·Procurement→Forbidden·validator negative invalid·NotFound. **No prod bug** — code đúng spec (OPT-IN finalize an-toàn-hơn AUTO, fail-closed order intentional). FakeCurrentUser configurable-roles. Tag [s72, pe-mig54, ccm-finalize-opt-in, spec-change, approved-price, applyapprovedprice-private-static-reflection, issystem-unreachable-observation, suggested-price-setter-authz, fail-closed, test-after].
- **2026-06-17 (S69b PE 2 feature anh Kiệt FDC — test-before-merge SECURITY+FINANCIAL workflow):** +14 test → **292→306 PASS** (45 Domain + Infra 247→261, 0 fail). BE done+builds, mirror harness PeSubmitGuardAndBypassTests/PeWorkItemGuardTests. **FEATURE B value-threshold CCM-finalize (`PeCcmThresholdFinalizeTests.cs` 5, Services ns, ApproveV2Async line 816-854):** NV duyệt role=CostControl + `aw.CeoApprovalThreshold!=null` + `winnerQuoteTotal < ngưỡng` + chưa-slot-cuối → Phase=DaDuyet bỏ CEO + pointers/SLA null. **⭐ BOUNDARY load-bearing: predicate `winnerQuoteTotal < ceoThreshold` STRICT-less-than (line 838)** → gói==đúng-ngưỡng = KHÔNG finalize = advance. Cover: (1)⭐LOAD-BEARING CCM<ngưỡng mid-wf→DaDuyet skip-CEO pointers-null + chỉ CCM-slot opinion no CEO-opinion / (2) ==ngưỡng→advance Bước2(CEO) stays-ChoDuyet SLA+7d / (2b) >ngưỡng→advance / (3) threshold-null→advance kể-cả-CCM+gói-1đ (backward-compat) / (4) non-CCM(PRO)<ngưỡng→advance (chỉ CostControl trigger, nhận-diện-theo-role) / (5) CCM-at-last-slot<ngưỡng→DaDuyet via NORMAL-advance (guard `!(idx==last&&lvl==max)` skip finalize-branch, nhánh advance terminal cũng DaDuyet — no double, 1 Approve row). Harness: dựng PE TRỰC TIẾP ở ChoDuyet pin pointer slot CCM (skip submit guard) + drive 1 Approve; `SeedWorkflowAsync(stepApprovers, ceoThreshold)`. **FEATURE A urgent-toggle authz (`PeUrgentToggleAuthzTests.cs` 9, Application ns, SetPurchaseEvaluationUrgentCommandHandler 4-dep db+ICurrentUser+UserManager+INotificationService):** role→cờ: PRO→IsUrgentByPro / CCM→IsUrgentByCcm / Admin→CẢ2 / else→ForbiddenException. Notify-CEO best-effort try/catch — KHÔNG assert (NoOpNotificationService nuốt; CreateUserAsync idempotent-register role nên GetUsersInRoleAsync(Director) no-throw). Cover: PRO-only-ByPro(Ccm-untouched) / CCM-only-ByCcm / Admin-both / Drafter→Forbidden+no-mutation / Finance→Forbidden / PRO-turn-off clears-only-Pro Ccm-preserved / **multi-role PRO+CCM no-Admin→else-if short-circuit chỉ ByPro (LOCK behavior)** / unknown-PE→NotFound. **No prod bug** — cả 2 feature code đúng spec (strict-`<` intentional rollout-safe, else-if priority Admin>PRO>CCM intentional). FakeCurrentUser configurable-roles ctor. Reuse NoOpNotificationService internal qua `using ...Tests.Services`. Tag [s69b, pe-ccm-threshold-finalize, value-threshold, strict-less-than-boundary, role-based-routing, urgent-toggle-authz, forbidden-no-mutation, else-if-short-circuit, test-before-merge].

- **2026-06-17 (S69 Office golive permission-seed regression — test-after SECURITY invariant, public Văn phòng số):** +6 test `tests/.../Application/OfficeModulePermissionSeedTests.cs` → **286→292 PASS** (45 Domain + Infra 241→247, 0 fail). Mirror `HrmProfilePermissionSeedTests` (S67) — SAME reflection harness (invoke 2 private-static `RevokeTemporarilyHiddenModulesAsync` + `SeedAllRolesOfficeModulePermissionsAsync` qua `GetMethod(name, NonPublic|Static).Invoke(null, [db, rm, NullLogger.Instance])`; SqliteDbFixture/IdentityFixture; seed MenuItem rows TRƯỚC Permission FK Cascade). **KHÁC HRM:** Office grant mở **CanRead AND CanCreate** (HRM read-only) trên allow-list **16 key**; HRM chỉ 2 key. Chain = revoke (StartsWith("Off")→all false non-Admin) → office-grant (allow-list→read+create, upgrade-only). **Cover:** (1) chain non-Admin allow-list-16 → read+create=true + **excluded-3 stay hidden** (`OffPhongHopManage`/`OffAttendanceReport`/`OffChamCong` ⭐ LOAD-BEARING security assert) / (2) allow-list Update+Delete stay false / (3) no-leak HRM-dashboard+Personal stay hidden / (4) Admin not-revoked keeps all incl excluded-3 / (5) create-missing-row read+create=true update/delete=false + excluded NOT created / (6) **upgrade-only preserves admin-raised Update/Delete=true** (office-grant chỉ đụng Read/Create, KHÔNG hạ). **No prod bug** — seed logic đúng spec (excluded-3 confirmed hidden, upgrade-only không phá quyền admin). Tag [s69, office-golive, permission-seed, security-invariant, excluded-3-hidden, read+create-grant, upgrade-only, reflection-private-static, test-after].

- **2026-06-12 (S60 UAT anh Kiệt — 2 feature PE submit branch, test-after build PASS):** +14 test `tests/.../Services/PeSubmitGuardAndBypassTests.cs` → **240→254 PASS** (58 Domain + Infra 182→196, 0 fail). Mirror `PurchaseEvaluationWorkflowServiceGuardTests` (IdentityFixture+SQLite, reuse `NoOpNotificationService` internal). **F1 Section 3 guard (8):** submit branch (DangSoanThao/TraLai→ChoDuyet) build `missing` list 4 mục → ConflictException msg gộp prefix `'Chưa đủ thông tin mục 3 "Đơn vị NCC/TP được chọn"...'` + join `' · '`. Cover: thiếu cả 4 / winner-only / winner+quote=0 / budget (cả null+manual=0) / comparison / **attachment gắn NCC (PES_Id!=null) KHÔNG đếm bảng so sánh = vẫn Conflict** (predicate PES_Id==null) / đủ-4-manual-budget→ChoDuyet / đủ-4-BudgetId→ChoDuyet. **F2 drafter-bypass (6, V2-only `ApplyDrafterBypassOnSubmitAsync`):** k=drafterSlots.Max(Order) bước đầu → auto Cấp 1..k. Cover: drafter=TP(2/2)+2bước→StepIdx=1/Lvl=1+opinion 1 row slot TP+2 AutoApprove / drafter=NV(1/2)→Lvl=2 cùng bước+opinion slot NV / drafter ngoài bước đầu→KHÔNG bypass StepIdx=0 Lvl=1 0-auto / 1-bước+drafter cấp cuối→DaDuyet pointers null SLA null / V1(awId null)→submit OK no-bypass no-crash / TraLai-resubmit→bypass áp lại opinion UPSERT 1 row + approval cộng dồn 2 vết. **⚠️ GUARD-FIRST:** mọi bypass-test PHẢI dựng PE đủ 4 ĐK Section 3 (winner+quote>0+manual-budget+comparison-attach) qua guard. **Seed pattern S60:** `SeedWinnerWithQuoteAsync`(PES+Detail+Quote ThanhTien) map winner→quote sum · `SeedComparisonAttachment`(PES_Id=null) · `SeedWorkflowAsync(Guid[][] stepApprovers)` build multi-step V2 1-NV/cấp. **Opinion-only-ownSlot invariant:** bypass cấp NV skip KHÔNG ghi opinion (chỉ Approval AutoApprove + Changelog vết); assert `opinions.HaveCount(1)` + `ApprovalWorkflowLevelId==drafterSlot.Id`. **No prod bug** — code đúng spec, test theo CODE (S34 rule). Tag [s60, pe-submit-guard, section3-completeness, drafter-bypass, v2-only, guard-first, opinion-ownslot-only, test-after].
  <!-- S57bis (PE WorkItemId guard Mig 49) + S56 (golive-harden) aged out of FIFO S77 — detail in session logs -->
  <!-- S57bis spec-drift note kept: 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 kept: ExecuteUpdateAsync BYPASS change tracker → stale-tracked-read needs .AsNoTracking() re-read; shipped tx = IsolationLevel.Serializable per em-main post-review -->

- **2026-06-08 (S54 ItTicket reassign authz — test-before-merge SECURITY) [harvested by em main — agent MEMORY write mis-landed, B2/B3]:** +13 test `tests/.../Application/ItTicketReassignAuthzTests.cs` → **203→216 PASS** (58 Domain + Infra 145→158, 0 fail). **GetAssignableItStaff (6):** Admin→CanReassign=true + 2 IT-active ordered FullName (Cao<Truong) no KT/inactive leak · IT-staff→true · non-IT non-admin (KT)→false + **empty staff (0-leak assert)** · dept-null→false+empty · inactive-IT-excluded · UserId null→Unauthorized. **AssignItTicket (7):** non-IT non-admin→**ForbiddenException** + side-effect `AssignedToUserId.Should().BeNull()` (no-mutation) · Admin+assignee∈IT→success · IT-staff+assignee∈IT→success · assignee∉IT(KT)→**ConflictException** "Người được giao phải thuộc tổ IT." · assignee inactive→NotFound · ticket not found→NotFound · null→Unauthorized. **Pattern mới: authz-capability test = seed 2-dept (IT+KT) + fake `ICurrentUser` role/dept matrix; assert canReassign flag + Forbidden/Conflict guard; empty-staff = 0-leak.** Forbidden red-able **by-contrast** (case5 non-IT vs case7 IT-staff identical-setup → chỉ khác caller-identity; rule cấm sửa prod để chứng minh RED). **No prod bug** — handler-level data-dependent authz (caller-dept vs IT-dept) = CORRECT pattern, KHÔNG phải gotcha #44 silent-403 gap (Pattern 10 reflection-regression chỉ cho static `[Authorize(Policy)]`; data-driven authz PHẢI ở handler = enforcement point, test cover tại đó). Tag [s54, it-ticket-reassign, authz-capability, forbidden-conflict-guard, test-before-merge, 0-leak].
- **S51-S52 (aged to L2 — `archive/activity-s51-s52.md`):** P11-C/D/E/F + gotcha #57 EXT — filtered-unique RED (LeaveType/Shift + Master Dept/Proj/Supplier bare `.IsUnique()` → em main `.HasFilter` Mig 45/47) · ItTicket round-robin+SLA-due · WorkflowApps codegen + Attendance day-type · Serializable-on-SQLite = NON-ISSUE proven.
---

## ⚠️ Anti-patterns (DO NOT)
1. ❌ Touch production code → REPORT bug · 2. ❌ Skip MEMORY · 3. ❌ Test không chạy (dotnet test must PASS) · 4. ❌ `git add -A` · 5. ❌ Push remote · 6. ❌ Assertion trivial

## 🔄 Curate trigger
Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): `Tests`.
