# 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: 440 tests = 440 PASS (45 Domain + 395 Infra) as of S97 - Count history + verbose per-session test-design notes (S54→D2): [archive/baseline-history.md](archive/baseline-history.md) (L2, on-demand). - Latest (D2): +6 PE Mig 58 multi-winner — `CreateContractFromEvaluationMultiWinnerTests.cs` (5, 1→N HĐ per IsWinner, GiaTri per-winner=quote-total / single=legacy-budget-sum) + `PeListWinnerNamesProjectionTests.cs` (1, EF collection-projection translation-smoke). No prod bug. > 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. 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().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 `` 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 (one-line FIFO — verbose in [archive/baseline-history.md](archive/baseline-history.md)) - **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-main BE thêm `PurchaseEvaluation.EndedByLevelFinalize` (Mig 60) set true DUY NHẤT nhánh level-finalize `ApproveV2Async`. ListApproved (DaDuyet-only) đọc THẲNG field (`CreateContractFromEvaluationFeatures.cs:196`); List/Inbox ternary `Phase==DaDuyet ? field : subquery`. **Spec-change RED handled:** `PeFinalizeProjectionTests` seed-helper +param `endedByLevelFinalize` → case cũ `peFin` set true; **REPRO case mới `peFinButWentCeo`**: CÙNG wfFin config-có-finalize NHƯNG field=FALSE → EndsBeforeCeo=FALSE (bug: config-true nhưng lên CEO thật). FinalizeStep/LevelName VẪN từ config-subquery (nhãn cấp, không phải cờ). **EF-translate OK** (ListApproved handler no-throw). Service `PeApproverFinalizeTests` +3 assert: #1 finalize→true / #2 advance-CEO→false / #6 opt-out→false (khớp projection repro). No prod bug. - **S92 (2026-06-29) +3 admin-only-modules revoke (test-before-merge SECURITY) — 431→434 PASS, no red:** anh chốt 5 nhóm menu (NHÂN SỰ/VĂN PHÒNG SỐ/CÁ NHÂN/HỢP ĐỒNG/DANH MỤC) CHỈ Admin thấy. Em-main mở rộng `RevokeTemporarilyHiddenModulesAsync` predicate +Contracts/Ct_*/Master/Suppliers/Projects/Departments/Catalog* (cộng dồn S58 Hrm*/Off*/Personal) + TẮT 2 re-grant S65/S69 + narrow `InReviewScope→Pe_*`. `AdminOnlyModulesRevokeTests.cs` (3): NonAdmin-S92Modules-4cờ-false LOAD-BEARING + Admin-untouched + Pe-module-NOT-revoked invariant (root `PurchaseEvaluations` + `Pe_DuyetNcc` không match predicate → giữ user). Reflection harness mirror Office/Hrm seed test. **Gotcha:** OfficeModulePermissionSeedTests + HrmProfilePermissionSeedTests VẪN PASS dù prod đã TẮT 2 call grant — vì test invoke method-CÔ-LẬP qua reflection (private static giữ cho reversibility), KHÔNG chạy SeedAsync chain; thêm 1 dòng header note mỗi file. (ChangePassword S91 10-test ĐÃ merge → trong baseline 431.) - **S87** +7 PE Mig 59 CCM-budget-period setter authz (`PeCcmBudgetPeriodSetterAuthzTests.cs`, role-gate Admin|CostControl, null=clear gotcha #73). → 402. - **S86** +3 PE drafter-notify-exclusion REGRESSION (bug-fix test-alongside; `PeApproverNotifyExcludesDrafterTests.cs`, RED-proven-via-git-stash, CapturingNotificationService spy). - **S85b** +3 PE UpdateDraft WF-preserve ROOT-FIX (null-safe guard, ApprovalWorkflowId FK Restrict seed-real). **S85** +5 PE urgent DRAFTER-self-unset (isDrafter clear-both). - **S77c** urgent-toggle SYMMETRIC→ASYMMETRIC correction (SET=role-đủ / UNSET=role AND DeptManager). **S77** +7 suggested-price NOTE (rides price-gate, no-arity-fix). **S76** +5 PRO-column-split (ProInitial/ProAdjust, ProAdjust ÂM-OK). **S72** +28 Mig 54 (CCM-finalize OPT-IN + ApprovedPrice private-static-reflection + isSystem-unreachable OBSERVATION). - **S69b** +14 PE CCM-threshold-finalize (strict-`<` boundary) + urgent-toggle authz (test-before-merge). **S69** +6 Office permission-seed regression (reflection harness, 16-key Read+Create). - **S60** +14 PE submit Section-3 guard + drafter-bypass V2-only (GUARD-FIRST; opinion-ownSlot invariant; `PeSubmitGuardAndBypassTests.cs`). - **S54** +13 ItTicket reassign authz (2-dept matrix, Forbidden red-able BY-CONTRAST, empty-staff 0-leak). **S51-S52** (L2 `archive/activity-s51-s52.md`) filtered-unique RED→Mig45/47 + WorkflowApps codegen + Serializable-on-SQLite NON-ISSUE. --- ## ⚠️ 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`.