# Test-Specialist Agent — Persistent Memory

- **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) ĐỢT 2 PE xóa-khi-đang-duyệt + màn "Đã xóa" [run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4, diary 13.9KB ghi-trong-lúc-làm]:** +10 `PeDeleteByApproverTests.cs` (T21 lũy kế **4 HƯỚNG KHÁC NHAU** · T22/T22b/T22c màn Đã xóa · T23/T24/T24b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog). Full **561 = 560 PASS / 1 FAIL** (45D + 516I); 0 hồi quy (Infra cũ 506 nguyên vẹn). **1 FAIL = T26 GIỮ ĐỎ = BUG PROD**: `PeSoftDeleteFeatures.cs` Add changelog (:126) → `Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** (không phải Deleted) ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất ⇒ không cứu. Đo bằng ChangeTracker diag (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng-lớp: site DUY NHẤT có hình "Add con → Remove cha → 1 SaveChanges". REPORT lead, KHÔNG tự sửa. **Hạ tầng MỚI:** `SoftDeleteFixture` (ngay trong file test, ~90 LOC) = fixture ĐẦU TIÊN của repo wire `AuditingInterceptor` — BẮT BUỘC khi đo `Remove()`=xóa-mềm, vì `TestApplicationDbContext` KHÔNG wire ⇒ mặc định **XÓA CỨNG** ⇒ màn "Đã xóa" rỗng ⇒ assert đúng ở CẢ HAI thế giới (đúng bẫy vacuous). Số học T21 nhớ kỹ: xóa phiếu ChoDuyet ⇒ PrevSubmitted↓ · PendingSelected↓ · **PrevSelected + PendingSubmitted BẤT ĐỘNG** (seed thêm peer DaDuyet + TraLai để 2 số "không đổi" ≠ 0 ở cả 2 phía). Tag `[s155, pe-delete-by-approver, cascade-detach-added-child, softdelete-fixture, 4-huong]`

- **S153 (07-26) T4 ctx-adopt FAULT-INJECT máy `mind-check` #8-#17+#21 [run 2026-07-26-S153-ctx-adopt · lane trước đứt sau #7, file đĩa nguyên → tiếp được] `[engine: opus-5]`:** **18/18 PASS 0 FAIL**. KHÔNG phải test C# (**baseline 509 KHÔNG đổi**) — đối tượng = `scripts/session_ctx.py mind-check`, mọi mutation trong cây tạm qua seam `--root`, repo read-only. Bắt **1 GAP THẬT**: ở closed-mode **XOÁ con-trỏ → cờ/exit 0** còn **GIỮ con-trỏ đang-chờ trung thực → TRƯỢT/exit 1** ⇒ đường-rẻ-nhất-để-xanh là đường SAI (chữ luật cấm, gradient thưởng) → REPORT em main, KHÔNG tự sửa. **Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 — `inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh** ⇒ phải soi RUỘT fixture (`cat -A`/`repr` từng dòng), KHÔNG chỉ đọc exit code. 2 kỹ thuật xài lại: (i) **cặp CÙNG-RUỘT khác 1 biến** — cap=32 vs cap=1 trên cùng tệp = chứng đọc-config-LIVE; `_end` giả `_pause=9` vs không = chứng nguồn-đếm-là-ĐĨA, dùng `diff` 2 output để chứng dòng đo BYTE-IDENTICAL; (ii) **đo wall-clock phải gồm nhánh ĐẮT** — lệnh literal đi nhánh early-exit (repo chưa có `_mind`) = tự đo nhánh rẻ rồi gọi là chi phí thật; đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×, giao lead điền lại. Tag `[s153, t4-fault-inject, mind-check, fixture-bug-vacuous-pass, cung-ruot-1-bien]`

- **S134 (07-17) PE lũy-kế TẠM TÍNH TEST-C [wf_97d6a582-b7e · return sạch 0-garble] `[engine: opus-worker]`:** +8 `PePendingAccumulationTests.cs` C1-C8 (file MỚI, mirror seed PeBudgetFreezeTests :277-336): C1 ChoDuyet+winner→delta2+PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 nháp list-only/TraLai delta1 · C4 window CreatedAt · C5 delta âm signed · **C6 complementary-no-double-count: gọi CẢ ComputeAsync + ComputePendingAsync cùng seed, assert EXACT-NUMBERS 2 phía (PrevSelected 90tr/1 bất động ⟂ PendingSelected 50tr/1)** · C7 frozen→0/null qua query handler · C8 D4 changelog positive+contrast (filter `ContextNote.Contains("[S134 pending-prior]")` #48). Full **528/0** (45D+483I, +8). Learned: (1) **direct-call accumulator = synthetic curPeId + CreatedAt muộn, KHÔNG cần row phiếu-đang-xét** (chỉ filter Id!=/CreatedAt<); (2) regression "bất động" mạnh nhất = C6-style exact-numbers 2 hàm cùng seed; (3) FluentAssertions `ContainInOrder(params T[])` + because-string overload-clash khi T=Guid → bọc `new[]{...}` trước. Surprise: D4 no-SaveChanges-in-helper → row chỉ persist sau TransitionAsync. Tag `[s134, pependingaccumulation, c6-exact-numbers-complementary, 8-test]`

- **S133 INV-C T1-T9 [wf_75c6bfd3-754 · #53 return-fail lần 3 → disk + lead tự chạy suite; entry on-behalf]:** +9 test vào PeBudgetFreezeTests (`:426` T1 đủ-11-cột · `:480` T2 CCM-threshold · `:524` T3 all-steps · `:555` T4 V1-legacy assert KHÔNG-ApprovedPrice · `:638` T5 guard CCM+Admin Conflict + ChoDuyet-allow · `:687` T6 AdjustBudget DaDuyet+TuChoi · `:730` T7 live-vs-frozen flags/editability · `:783` T8 null-safe no-WorkItem + no-pair · `:821` T9 overwrite + SnapshotAt refresh). FULL suite lead-verified exit-0: **Domain 45 + Infra 474 = 519, 0 fail** (464+10 khớp). ⚠️ Lesson mới: INV-C **ghi đè** `sub-test-specialist-0.md` của INV-A — cùng role + cùng task-index qua 2 invocation chung run-folder = sub-file collision; INV-A content sống nhờ journal + diary. Multi-invocation cùng run → cần label/index phân biệt (báo lead khi gặp lại). Tag `[s133, t1-t9, subfile-collision-multi-invocation]`
- **S133 (07-17) PE budget-freeze T0 REPRODUCE [wf_d9639faa-97a INV-A, return sạch]:** RED thật `Failed 1/1` — expected ProInitial 100tr, found 999tr @`PeBudgetFreezeTests.cs:184`. Flow chuẩn: đưa phiếu tới DaDuyet QUA `svc.TransitionAsync` (V2 1-Bước-1-Cấp `AllowApproverFinalize=true` + giá chốt), KHÔNG seed thẳng Phase (post-fix snapshot CHỈ set qua transition); mutate pair qua UpdatePeBudgetPro/Ccm (2 handler KHÔNG phase-guard = đúng cơ chế bug); 2 sanity-assert chống vacuous (Phase==DaDuyet + liveRec đổi thật) TRƯỚC assert chính. Bắt được lỗi spec: query THẬT = `GetPurchaseEvaluationQuery` (bundle `.BudgetSummary`), spec ghi nhầm GetPurchaseEvaluationDetailQuery → lead sửa spec. Sau INV-B: T0 GREEN trong 44/44 filter. Tag `[s133, pe-budget-freeze, t0-red-green, anti-vacuous-sanity-assert]`

> **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: 509 tests = 509 PASS (45 Domain + 464 Infra) as of S118b (was 505@S118; +4 Infra ReportsController company-wide-report authz-regression, filtered-run 19/19, full 509/509)
- 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<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 (one-line FIFO — verbose in [archive/baseline-history.md](archive/baseline-history.md))

- **S118b** +4 ReportsController company-wide authz (Pattern 10 reflection; `Dashboard`+`ExportContracts` gate `Reports.Read`, `MyDashboard` CỐ Ý bare `[Authorize]`) → 509.
- **S118** +4 Procurement master-access seeder SCOPE-ISOLATION (grant KHÔNG rò sang role khác + CONTRAST assert chống vacuous-pass) → 505.
- **S117** +5 PE ProMinMax duyệt-theo-KHOẢNG (boundary min==max; ⚠️ `MethodInfo.Invoke` KHÔNG auto-bind optional param → helper phải pass đủ 5 args) → 500.
- **S116** +9 PE báo giá SỐ ÂM (guard `Sum()<=0`→`All(x==0)`; ranh giới net==0 mà từng-dòng≠0; CCM threshold giữ NET) → 495.
- **S113** +19 Supplier import v2 dedup precedence MST→Code (dedup ở SERVICE, không dựa DB-collation) + publish-guard; 🔴 BUG NRE `Code!.Trim()` khi Code==NULL (REPORTED em-main).
- **D4** +8 PE sign-off A1 (drafter bypass CHỈ cấp NV dưới THUẦN, pointer dừng tại minOwn) + EDGE-5 terminal-reject + opinion-history changelog.
- **D3** +10 Supplier Excel-import case-collation (dedup SERVICE `OrdinalIgnoreCase`; SQLite BINARY KHÔNG bắt CI-dup, prod SQL thì có) + header-xlsx-từ-REFLECTION pattern.
- **S97** ±0 PE EndsBeforeCeo CONFIG→RUNTIME spec-change (repro case mới: config-có-finalize NHƯNG field=false = phiếu thật lên CEO).
- **S92** +3 admin-only-modules revoke; gotcha: seed-test VẪN PASS dù prod đã TẮT call grant — vì test invoke method CÔ-LẬP qua reflection, không chạy SeedAsync chain.
- **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`.

## 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".**
