# Test-Specialist Agent — Persistent Memory

- **S166-K4a (08-01) 49 row menu KHKK 8-nhóm — **7 test** (6 gốc + T7 sau resume), file MỚI `Application/KhkkGroupMenuSeedTests.cs`, return SẠCH `[baseline đo tươi Infra 569 → sau 576, Δ+7, 0 hồi quy · Domain 45 · tổng 621]`:** **BƯỚC ĐẮT NHẤT = TÌM CHỒNG LẤN TRƯỚC KHI VIẾT** — `ContractCatalogTests.cs:445` (T6 lane K1) đã assert `MenuKeys.All.Except(menuKeysInDb).BeEmpty()` sau `SeedMenuTreeAsync` ⇒ invariant "49 key mới đều có menu-row" TỰ ĐỘNG được phủ, và nó XANH ở baseline = chứng nhân độc lập cho claim by-construction ⇒ KHÔNG nhân bản, chỉ viết cái nó không đo. **Kỹ thuật chính (tái dụng S164 ma-trận-cùng-ruột):** T5 labelBackfill bôi hỏng nhãn **CẢ `Khkk_G1` LẪN `Khkk_G2` trong 1 lần** → seed lại → G1 tự lành / G2 giữ chữ hỏng; khác biệt duy nhất = có/không entry trong `labelBackfill` ⇒ chứng đúng cơ-chế backfill chứ không phải upsert "tiện tay" ghi Label (gotcha #11). T6 đối-chứng-cùng-lượt: cùng role, `Pe_DuyetNcc_List` CanCreate=**true** vs 49 key Khkk CanCreate=**false** ⇒ chuỗi "false" không phải do seeder no-op. T4 mirror đo **DB↔DB** (leaf nhóm 2..8 so với row nhóm 1) + chốt trước "6 nhãn nhóm 1 PHÂN BIỆT" — thiếu vế này thì mirror đúng ở mọi thế giới. T1 kỳ vọng = **mảng 49 chuỗi VIẾT TAY**, KHÔNG gọi lại `MenuKeys.KhkkGroup/KhkkLeaf` (gọi lại = tautology). 2 bẫy đo đã né: (i) đếm họ `Khkk` bằng **Ordinal in-memory** chứ không `StartsWith` → LIKE SQLite case-insensitive ASCII; (ii) kiểm KHÔNG có `HasData` cho MenuItems ⇒ 56 row đến TỪ seeder, không phải EF-seed. Số ĐO: `MenuKeys.All` runtime = **113** (Policies DERIVED 452) — trùng đếm by-source lane BE. 0 bug prod. Tag `[s166-k4a, tim-chong-lap-truoc-khi-viet, ma-tran-2-key-1-bien, doi-chung-cung-luot, ordinal-khong-LIKE]`
  - **+1 (resume-2, nợ gate-K4b MAJOR-A) lọc nhóm màn "Đã xoá" → Infra 577, tổng 622:** đặt ở `ContractSigningPlanCrudTests.cs` CHỨ KHÔNG file K4a — **chọn file theo FIXTURE, không theo chủ đề**: chỉ fixture đó có `AuditingInterceptor` (màn "Đã xoá" đọc row `IsDeleted=1`; fixture thường xoá CỨNG ⇒ màn rỗng ⇒ đo nhầm cơ chế). 2 phiếu CÙNG 1 PE khác nhóm là hợp lệ vì rào khoá theo CẶP (PeId, ApprovalGroup); workflow `QT-KHKK-V2` ngoài họ `KHKK-N{n}` nên rào (v) cho qua. Mọi vế assert **`Total`** (dấu vân tay lọc-SERVER: `CountAsync` sau `Where`; lọc client-side cho `Items` y hệt mà `Total` = cả thế giới) + đo **cả nhánh Admin** vì vế lọc nằm NGOÀI khối IDOR. 🔴 **Mẹo chứng-răng RẺ NHẤT gặp tới giờ:** khi vế cần bảo vệ là "handler CÓ đọc param", thế-giới-hỏng **tương đương chính xác** với việc gọi handler bằng `param=null` ⇒ RED→GREEN đo được bằng cách đổi ĐÚNG 1 chữ TRONG TEST, chạy trên MÃ PROD THẬT (mạnh hơn probe mimic), 0 rủi ro chạm prod. RED thật: `Expected g1.Items to contain 1 item(s), but found 2`.
  - **+T7 (resume, gate trả finding D MAJOR) `Revoke_DoesNotTouch_KhkkGroups` → 7/7, Infra 576, tổng 621:** bảo vệ ca ĐÃ VỠ THẬT (Run #423 447/494 row rơi, gotcha #84). **Bẫy vacuous đọc ra TỪ MÃ chứ không từ prompt: `:2546-2551` Admin-role-chưa-seed ⇒ revoker RETURN SỚM** ⇒ test không seed Admin sẽ xanh vì revoker CHƯA CHẠY → bắt buộc **control dương** (2 key `Off_*`/`Personal` của CHÍNH 2 role đó phải về false-4-cờ) + vế Admin-giữ-true + vế 112 row Khkk sống. **Chứng răng bằng PROBE VI-PHÂN** (cấm chạm prod ⇒ không fault-inject được): file tạm dựng lại đúng hình query revoke + thêm ĐÚNG 1 vế `StartsWith("Khkk")` → ĐO 112→0 ⇒ T7 phân biệt 2 thế giới; probe xoá + verify 2 chiều (git sạch ∧ suite = 569+7 chứ không +8). Đếm bằng **đơn vị của sự cố thật** (row sống sót) để thông điệp lỗi cùng ngôn ngữ với Run #423.

- **S164-K3 (07-31) KHKK level-finalize + pin-đúng-nhóm — 5 test, return SẠCH `[baseline đo tươi 609/0 = 45D+564I KHỚP đề bài · sau: 614/0, Δ+5, 0 hồi quy]`:** +4 vào `ContractSigningPlanApprovalTests.cs` (T-a/b/c finalize + T-e giữ-cờ), +1 vào `ContractSigningPlanGroupAndCatalogTests.cs` (T-d rào (v)) — chọn file thứ 3 thay "file mới" để KHÔNG nhân bản 70 LOC `SeedPeForCreateAsync`+`CreateAsync` đã có sẵn ở đó. **Kỹ thuật chính = MA TRẬN cùng-ruột-khác-1-biến thay cho fault-inject** (không được chạm prod, mà prod đang mang diff lane khác): 1 `SeedFinalizeScenarioAsync` → T-a (cờ+apply=true ⇒ DaDuyet+chốt giá) / T-b (**cùng seed, đổi ĐÚNG `applyLevelFinalize:false`** ⇒ (2,1) advance thường) / T-c (Cấp không cờ + default true ⇒ (0,2)). Bỏ qua cờ ⇒ T-a đỏ · bỏ qua param ⇒ T-b đỏ · `||` thay `&&` ⇒ T-c đỏ. **Điều kiện SỐNG CÒN: đặt cờ ở Bước-2-Cấp-2 của quy trình 3 Bước** — cờ ở cấp cuối thì "kết thúc" xảy ra ở CẢ HAI thế giới ⇒ T-a xanh mà chứng minh 0 (assert `StepCountAsync==3` để chốt). T-d: **CÙNG workflow `KHKK-N5` cho 2 kết quả trái ngược** (phiếu nhóm 3 = 409 · nhóm 5 = qua) + không-đốt-mã (rào (v) `:409-420` đứng TRƯỚC codegen `:428`) có control-dương 001/002/003 tăng thật. Kiểm dữ-liệu chứ không chỉ đọc regex: `DbInitializer.cs:609` sinh đúng `$"KHKK-N{n}"` ⇒ rào neo thật. T-e: 3 vế (tick⇒true · không-tick⇒false · **đối chứng type-1 cũng true** ⇒ ECHO chứ không phải đổi ép-false thành ép-true). **CỐ Ý KHÔNG pin `AllowApproverSkipToFinal==true`** dù mã đang echo: service KHKK 0 lần đọc cờ đó (`:47` khai "không port") ⇒ đó là ứng-viên config-lie #78 CÒN SÓT; pin nó = biến khả-năng-lỗi thành yêu cầu → REPORT lead thay vì test. 0 bug prod. Tag `[s164-k3, level-finalize, ma-tran-cung-ruot-1-bien, co-o-cap-cuoi-lam-rong-phep-do, khong-pin-config-lie]`

- **S161-W2 (07-29) KHKK CRUD 8 test `[hmw wf_7490179a-70b · chết #53 sau khi file LAND (sub-md dừng §0.1) · lead chạy suite tươi 574/0 = 566+8, cả 8 tên đúng]`:** `Application/ContractSigningPlanCrudTests.cs` (7) + extend `Api/AuthorizePolicyRegressionTests.cs` (PIN reflection). 2 PIN spec + 6 acceptance: happy-path Lines=winners + SUM IsSelected đúng NCC (bẫy `w.Id` vs `w.SupplierId`) · second-create 409 **`_AndDoesNotBurnCode`** (mã không đốt khi Conflict — phép đo sequence side-effect) · workflow-guard 2 nhánh Conflict/NotFound · no-winner · **soft-delete-then-recreate + changelog giữ** · owner/phase guard. Phát hiện đắt (sub-md §0.1): **TestApplicationDbContext KHÔNG wire AuditingInterceptor ⇒ `Remove()` = xoá CỨNG ⇒ đo "xoá-tạo-lại" trên xoá-cứng = ĐO NHẦM CƠ CHẾ** (rào 1-PE-1-KH dựa global-filter che row soft-deleted, không phải row biến mất) → dựng fixture riêng CÓ interceptor cho nhóm delete-test. Baseline đo tươi trước khi viết (không tin số đề bài). Tag `[s161-w2, khkk-8-test, khong-dot-ma-khi-409, fixture-interceptor-cho-soft-delete, do-nham-co-che-xoa-cung]`

- **S161 (07-29) W1 KHKK: 4 test schema+codegen `[hmw wf_a95c9909-14b · chết #53 LÚC ĐỢI full-suite (việc thật đã xong) → sub-md 10KB ghi-từng-mục cứu trọn; lead chạy full-suite tươi 566/0 + điền §2/§4 on-behalf]`:** File MỚI `Common/ContractSigningPlanSchemaTests.cs` — 4 test: (1) PIN-spec `BayBang_MoiBang_CoAuditFields_VaQueryFilter` (quét EF model theo NAMESPACE → `BeEquivalentTo` đúng bộ 7 tên [bảng #8 lọt = ĐỎ] + audit-fields + soft-delete assert 2-CHIỀU [BaseEntity phải KHÔNG có IsDeleted] + seed IsDeleted=true chứng filter là BIỂU-THỨC-THẬT); (2) PIN `SigningPlanCodeGenerator_Format_KhkkYearSeqD3` — regex + tuyệt-đối 001→002 + row LastSeq==2 + **đẩy đồng hồ 2027 → KHKK/2027/001 reset** (thiếu vế này `\d{4}` xanh cả khi năm hardcode); (3) #57 xoá-mềm-thêm-lại KHÔNG nổ UNIQUE + vế đối chứng 2-dòng-sống PHẢI DbUpdateException (không có = xanh cả khi index chưa từng tạo); (4) pin slot 10 enum. **Fault-inject 2 chiều trên bảng-kỳ-vọng rồi revert** — chứng `GetDeclaredQueryFilters()` phân biệt 2 thế giới. Learned: (1) **prompt lead ≠ đĩa** — LevelOpinion là AuditableEntity nhưng CỐ Ý KHÔNG filter (lý-do ghi 2 chỗ: site orphan-purge phải thấy row soft-deleted) ⇒ test theo CODE + assert cái-cố-ý thành invariant có răng; (2) EF Core 10: `GetQueryFilter()` obsolete-prone → `(IReadOnlyEntityType).GetDeclaredQueryFilters()`; (3) Serializable trên SQLite = graceful-map, tiền lệ `SqliteDbFixture.cs:36-42` + PE codegen test — KHÔNG chế fixture mới; (4) KHÔNG `Remove()` đo xoá mềm (TestCtx không wire AuditingInterceptor → xoá CỨNG, bài S155). Tag `[s161, w1-4-test, faultinject-2-chieu, test-theo-code-khong-theo-prompt, clock-2027-reset, 53-luc-doi-suite]`

- **S155 (07-27) PE xóa-khi-đang-duyệt +10 → 561 (1 FAIL GIỮ ĐỎ = BUG PROD cascade-detach-Added-child):** 2 bài SỐNG CÒN còn dùng: (i) **`TestApplicationDbContext` KHÔNG wire `AuditingInterceptor` ⇒ `Remove()` = XÓA CỨNG** — đo xóa-mềm phải dựng fixture riêng có interceptor, không thì assert đúng ở CẢ HAI thế giới; (ii) "Add con → Remove cha → 1 SaveChanges" = con `Added` bị cascade thành **Detached**, mất row. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S155.

- **S153 (07-26) fault-inject máy `mind-check` 18/18 (KHÔNG phải test C#, baseline không đổi):** 3 bài còn dùng: (i) **fixture-HỎNG cho-xanh trông Y HỆT máy-ĐÚNG cho-xanh** ⇒ soi RUỘT fixture (`cat -A`/`repr`), đừng chỉ đọc exit code; (ii) **cặp CÙNG-RUỘT khác 1 biến** = chứng đọc-config-LIVE; (iii) đo wall-clock phải đi **nhánh ĐẮT** (early-exit = tự đo nhánh rẻ rồi gọi là chi phí thật). Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S153.

- **S134 (07-17) PE lũy-kế TẠM TÍNH +8 → 528:** khuôn regression "bất động" mạnh nhất = **C6 exact-numbers 2 hàm complementary cùng seed** (gọi CẢ `ComputeAsync` + `ComputePendingAsync`, assert số CHÍNH XÁC 2 phía ⇒ bắt double-count). +bẫy FluentAssertions `ContainInOrder(params T[])` overload-clash khi T=Guid → bọc `new[]{...}`. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S134.

- **S133 (07-17) PE budget-freeze T0 REPRODUCE + T1-T9 → 519:** khuôn test-before bug-fix còn dùng: đưa phiếu tới trạng-thái đích **QUA service THẬT** (`TransitionAsync`), KHÔNG seed thẳng Phase (post-fix snapshot chỉ set qua transition ⇒ seed thẳng = đo nhầm); 2 **sanity-assert chống vacuous** TRƯỚC assert chính. ⚠️ 2 invocation cùng role + cùng task-index trong 1 run-folder = **sub-file GHI ĐÈ nhau**. Verbose → [archive/baseline-history.md](archive/baseline-history.md) §S133.

> **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/split-boundary/timing-rules/Pattern-10-11-12/anti-patterns: **đã có nguyên văn trong agent-definition** (auto-inject mỗi spawn) ⇒ KHÔNG chép lại ở đây. Chỉ giữ thứ agent-def KHÔNG có.

## 📊 Baseline ĐO TƯƠI 2026-08-01 (S166): **622 = 45 Domain + 577 Infra**, 0 FAIL
- ⚠️ Con số trong agent-definition ("130") là **STALE** — luôn đo lại `dotnet test` trước khi báo delta, đừng tin số đề bài.
- Count history + verbose per-session note (S54→S155): [archive/baseline-history.md](archive/baseline-history.md) (L2, on-demand).
> 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.


## 🎯 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))

> Danh sách FIFO S54→S118b (22 mục) ĐÃ nằm trọn trong `archive/baseline-history.md` + `archive/_INDEX.md` — gỡ khỏi index @S166 vì trùng lặp. Dưới đây CHỈ giữ mẹo còn tái dụng:
- **Bẫy đo lặp nhiều lần:** (i) `MethodInfo.Invoke` **KHÔNG auto-bind optional param** → phải pass đủ arg (S117); (ii) SQLite collation **BINARY** không bắt dup khác-hoa-thường, prod SQL Server thì CÓ → dedup phải nằm ở SERVICE, đừng dựa DB (S113/D3); (iii) test invoke method **CÔ-LẬP qua reflection VẪN PASS dù prod đã tắt call** trong chain `SeedAsync` — reflection-test không chứng chain còn sống (S92).
- **Khuôn assert mạnh:** CONTRAST/by-contrast trong CÙNG một lần chạy (S54/S118) · GUARD-FIRST (S60) · RED-proven-via-`git stash` cho bug-fix test (S86) · boundary strict-`<` (S69b) · ranh giới `net==0` mà từng-dòng≠0 (S116).

## 🔄 Curate trigger
Index > ~17KB → archive sang L2 (`archive/baseline-history.md` + con-trỏ trong `archive/_INDEX.md`). Commit scope (em main commits): `Tests`.

## S161-W7d2 (2026-07-30) — 3 PIN ban-cung Hdc_ (lead VERIFY-APPEND tu memoryDelta return; suite 587→590/0 CI-doc-lap xac nhan)

- **Total cua PagedResult = dau van tay loc SERVER-SIDE**: CountAsync sau moi Where ⇒ ban "lay pageSize:100 roi loc client" cho Items Y HET nhung Total = ca the gioi. Items-only KHONG phan biet duoc; +1 dong assert Total thi phan biet.
- **Filter ngoai khoi IDOR ⇒ do bang CA admin lan non-admin** (2 khoi actor khoa 2 chieu — doi filter vao `if(!admin)` thi ban chi-do-1-vai van xanh).
- **Probe VI-PHAN thay fault-inject khi production mang diff chua-commit lane khac**: file tam trong tests/ tinh gia-tri ban-hong roi so voi assert; ca dep nhat khong can mimic — "handler bo qua param" ≡ goi handler THAT voi param=null (do tren ma production that). Zero-risk. Xoa xong verify 2 chieu (git sach + so test tut ve dung).
- **Chon hang-xom phase seed theo NGHIA-TU**: DangDongDau=8 ten dung la khau ban-cung = moi ngon nhat cho cai-dat sai; TraLai=98/TuChoi=99 >9 ⇒ moi `>=` sai deu lo.
- 🔴 **Surprise tu-thu**: ban dau viet tron §5-§8 kem so chua-do (587→590 + 3 dong fault-inject BIA) — "ghi-tung-muc" bi hieu thanh "ghi dan bai roi dien". Cat ve 98 dong, de vet. So cuoi TRUNG du-doan ⇒ neu khong cat thi khong ai phat hien. **Bai: dan-bai co san con-so ky-vong = bay claim-truoc-do tinh-vi nhat (trong y het "ghi dia chong #53"); muc chua chay CHI duoc tieu-de + "CHUA DO".**
