diff --git a/.claude/agent-memory/test-specialist/MEMORY.md b/.claude/agent-memory/test-specialist/MEMORY.md index 6b78df1..f2cc5ec 100644 --- a/.claude/agent-memory/test-specialist/MEMORY.md +++ b/.claude/agent-memory/test-specialist/MEMORY.md @@ -1,6 +1,7 @@ # Test-Specialist Agent — Persistent Memory -- **S166-K4a (08-01) 49 row menu KHKK 8-nhóm — 6 test, file MỚI `Application/KhkkGroupMenuSeedTests.cs`, return SẠCH `[baseline đo tươi Infra 569 → sau 575, Δ+6, 0 hồi quy · Domain 45 · tổng 620]`:** **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]` +- **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]` + - **+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]` @@ -8,14 +9,13 @@ - **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]` +- **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) 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]` +- **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 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]` +- **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 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]` +- **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). @@ -23,46 +23,17 @@ --- -## 🎯 Role baseline +> 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ó. -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. +## 📊 Baseline ĐO TƯƠI 2026-08-01 (S166): **621 = 45 Domain + 576 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. -## ⏱️ 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 @@ -73,29 +44,12 @@ Test theo CODE (single source truth), document mismatch header comment + report. ## 📅 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 +> 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 -Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): `Tests`. +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) diff --git a/.claude/agent-memory/test-specialist/archive/_INDEX.md b/.claude/agent-memory/test-specialist/archive/_INDEX.md index 3cb650f..cf98d62 100644 --- a/.claude/agent-memory/test-specialist/archive/_INDEX.md +++ b/.claude/agent-memory/test-specialist/archive/_INDEX.md @@ -6,6 +6,11 @@ - activity-s51-s52.md · 2026-06-08 (S52 P11-D Master gotcha #57 EXT) [test-before · 3 RED LIVE]: · substring:"- **2026-06-08 (S52 P11-D Master gotcha #57 EXT) [test-before · 3 RED LIVE]:" - activity-s51-s52.md · 2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after · substring:"- **2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after" - activity-s51-s52.md · 2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57 · substring:"- **2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57" +- baseline-history.md · S155 — PE xoa-khi-dang-duyet + man "Da xoa" (dot 2), +10 -> 561 (1 FAIL GIU DO = bug prod) · substring:"S155 — PE xoa-khi-dang-duyet + man \"Da xoa\" (dot 2), +10 -> 561" +- baseline-history.md · S153 — T4 ctx-adopt FAULT-INJECT may `mind-check` · substring:"S153 — T4 ctx-adopt FAULT-INJECT may `mind-check`" +- baseline-history.md · S134 — PE luy-ke TAM TINH TEST-C, +8 -> 528 · substring:"S134 — PE luy-ke TAM TINH TEST-C, +8 -> 528" +- baseline-history.md · S133 INV-C — PE budget-freeze T1-T9, +10 -> 519 · substring:"S133 INV-C — PE budget-freeze T1-T9, +10 -> 519" +- baseline-history.md · S133 INV-A — PE budget-freeze T0 REPRODUCE (test-before bug-fix, RED that) · substring:"S133 INV-A — PE budget-freeze T0 REPRODUCE (test-before bug-fix, RED t" - baseline-history.md · S118b — ReportsController company-wide-report authz REGRESSION (test-after SECURITY), +4 -> 509 · substring:"S118b — ReportsController company-wide-report authz REGRESSION (test-a" - baseline-history.md · S118 — Procurement master-access seeder SCOPE-ISOLATION regression (test-after SECURITY), +4 -> 505 · substring:"S118 — Procurement master-access seeder SCOPE-ISOLATION regression (te" - baseline-history.md · S117 — PE ProMinMax duyet-theo-KHOANG range-price (test-before-merge FINANCIAL), +5 -> 500 · substring:"S117 — PE ProMinMax duyet-theo-KHOANG range-price (test-before-merge F" diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4b.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4b.md new file mode 100644 index 0000000..aafe2a4 --- /dev/null +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k4b.md @@ -0,0 +1,40 @@ +IMPLFE-K4B: IN-PROGRESS + +# Lane FE K4b — nối `?group=` + vá-3/5/6 (S166, 2026-08-01) + +Nối finding-A của gate K4a ("`?group=` chết-lâm-sàng" = FLAG-2 lượt trước: leaf trỏ đúng URL nhưng KHÔNG ai đọc). +SPEC PIN: spec-cụm2 vá 3 (queryKey 2 khoá) · vá 5 (`phase` derive thay mount-only) · vá 6 (Create preset từ URL) + K4b-7 (URL G1 mang `group=1`). + +## §0 Kế hoạch đo trước khi gõ (khuôn S164/S166) +- [ ] Đọc `KhkkListPage.tsx` TRỌN vùng state/query ×2 app — có mirror không (SHA) +- [ ] Đọc `KhkkCreatePage.tsx` vùng state `approvalGroup` ×2 app +- [ ] Grep staticMap 6 leaf G1 ở fe-admin (đề bài dặn: có thì mirror, không thì khai) +- [ ] Xác nhận BE nhận `?group=` (K2 wire `[FromQuery(Name="group")]`) — Read-only + +## §1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ + +### 1.1 Baseline SHA (cả 2 page ĐANG mirror ×2 app ⇒ được phép `cp`) +| file | sha256 trước K4b | +|---|---| +| `KhkkListPage.tsx` ×2 | `80ebd79dba1356e8f1512f798ab105eca312d84aae2744b7ed852ff667f60f3b` | +| `KhkkCreatePage.tsx` ×2 | `677f4ab803b0b7ac73ede9b2d5871a3243f4cbe90153a218b320d642c6431268` | + +### 1.2 🔴 TIỀN-ĐỀ ĐỀ-BÀI CẦN BẺ: vá-5 "derive thay useState" nguyên văn sẽ **GIẾT bộ lọc chip** +`phase` KHÔNG chỉ đến từ URL — nó còn là state do người dùng bấm: `KpiCard onClick :153-156 setPhase(c.value)` (6 chip Tất cả/Soạn/Chờ/Đã/Trả lại/Từ chối). Comment `:11-12` khai rõ chủ đích: *"Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng nên menu không sáng nhầm"* (bài S155). +⇒ Nếu làm `const phase = urlPhase` thuần thì `setPhase` mất chỗ đứng, **6 chip thành nút chết** — hỏng CÂM, build vẫn xanh. +⇒ Giải đúng = **state override + reset theo danh tính URL** (khuôn React "adjust state during render", KHÔNG `useEffect`, không nháy 2 pass): +`chipPhase: KhkkPhaseValue|null|undefined` — `undefined` = "chưa bấm chip, dùng URL"; `null` = "user chủ động chọn Tất cả" (2 nghĩa khác nhau, không gộp được). URL đổi ⇒ `chipPhase=undefined` + `page=1`. + +### 1.3 🔴 BE `/deleted` KHÔNG nhận `group` (rào: không sửa BE) +`ContractSigningPlansController.cs:59-66 ListDeleted(page, pageSize, search, sortDesc)` — **0 tham số group**, khác hẳn `List :42 [FromQuery(Name="group")] int? group`. +⇒ `Khkk_G{n}_Deleted → /khkk/list?group=n&view=deleted` sẽ hiện phiếu xóa CỦA MỌI NHÓM. Không client-side lọc được vì `total`/phân trang do server đếm trên tập chưa lọc ⇒ lọc ở FE sẽ ra số trang nói dối. ⇒ Xử: **gửi `group` chỉ ở nhánh không-xóa** + **không để tiêu đề CLAIM nhóm ở màn đã-xóa** (không nói dối) + FLAG lane BE. Xem FLAG-1. + +### 1.4 fe-admin CÓ 6 leaf G1 trong staticMap, nhưng `Khkk_WfView` ĐI ĐƯỜNG KHÁC +`fe-admin/Layout.tsx:35` `Khkk_WfView → /system/approval-workflows-v2/ContractSigningPlan` (Designer), 5 leaf còn lại `:36-40` trỏ `/khkk/*` như fe-user. +⇒ mirror `?group=1` cho **5 leaf**; `Khkk_WfView` admin **KHÔNG thêm** `group` (Designer không đọc tham số này — chính ghi chú K4a đã khai). Khai lệch, không mirror mù. + +### 1.5 Xác nhận đường BE cho nhánh chính +`List :42` `[FromQuery(Name="group")] int? group` → `ListContractSigningPlansQuery(..., group)` `:49`. ⇒ FE gửi `params.group` là ăn. + +## §2 THỰC THI + diff --git a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-test-k4a.md b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-test-k4a.md index 96ad246..895a936 100644 --- a/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-test-k4a.md +++ b/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-test-k4a.md @@ -1,4 +1,4 @@ -TEST-K4A: DONE — 6/6 PASS · Infra 569→575 · Domain 45 · tổng 620 · 0 hồi quy · 0 prod-code touched +TEST-K4A: DONE — 7/7 PASS (6 gốc + T7 finding-D) · Infra 569→576 · Domain 45 · tổng 621 · 0 hồi quy · 0 prod-code touched # Lane test K4a — test-after 49 row menu KHKK 8-nhóm (S166, 2026-08-01) @@ -95,6 +95,30 @@ bộ lọc revoke thì suite vẫn xanh. - [x] §D.2 Chọn chỗ đặt: `KhkkGroupMenuSeedTests.cs` (file của lane K4a) — đã có sẵn mảng `Expected49` + 7 key nhóm 1 + harness seeder; đặt ở `AdminOnlyModulesRevokeTests.cs` sẽ phải chép lại danh sách 49 key (nguồn thứ 2 = nguy cơ lệch). Vẫn MIRROR khuôn 2-vế của file đó. -- [ ] §D.3 Viết + chạy — CHƯA ĐO +- [x] §D.3 ĐÃ viết `T7 Revoke_DoesNotTouch_KhkkGroups` (cuối `KhkkGroupMenuSeedTests.cs`). + Kịch bản 1 lượt, 3 chiều phân biệt: + 1. seed menu THẬT + site-2 grant THẬT cho 3 role (Admin + Drafter + DeptManager); + 2. cấp tay 2 key **trong** tập ẩn (`Off_DanhBa`, `Personal`) cho cả 3 role = **CONTROL DƯƠNG**; + 3. tick thêm `CanCreate` cho `Khkk_G5_Create` (mô phỏng admin bật cờ ghi trên ma trận); + 4. chạy `RevokeTemporarilyHiddenModulesAsync`; + 5. **Vế 1** (control dương): 2 key kia của 2 role non-Admin về **false cả 4 cờ** ⇒ chứng + revoker CÓ CHẠY (nếu đỏ = phép đo hỏng, không phải Khkk hỏng); + **Admin giữ true** ⇒ chứng vế `RoleId != adminRoleId` còn sống; + **Vế 2**: 56 key Khkk × 2 role = **112 row sống sót**, `CanRead` true, row KHÔNG bị xoá, + cờ ghi đã tick vẫn true. + Đế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ữ Run #423. +- [x] §D.4 **CHỨNG T7 CÓ RĂNG bằng PROBE VI-PHÂN** (không được chạm prod ⇒ không fault-inject + vào `DbInitializer`): file tạm `ZzProbeTeethTemp.cs` dựng lại ĐÚNG hình câu truy vấn revoke + (`:2554-2568`) + thêm **đúng 1 vế** `|| MenuKey.StartsWith("Khkk")` rồi đo trên DB thật. + Kết quả ĐO: **trước = 112 row Khkk CanRead=true · sau bộ-lọc-bị-nới = 0** ⇒ đúng tập row mà + T7 assert sẽ rơi hết ở thế giới hỏng ⇒ T7 phân biệt được 2 thế giới, KHÔNG phải assert đúng + ở mọi thế giới. Probe chạy 1/1 PASS rồi **XOÁ**; verify 2 chiều: `git status -- tests/` chỉ + còn 1 file `??` (KhkkGroupMenuSeedTests.cs) và suite = 569+**7** (không phải +8) ⇒ 0 dư lượng. -END sub-test-k4a — (tạm) 6/6 PASS, suite 620 — ĐANG THÊM test finding-D, chưa chốt +## §9 — SỐ CHỐT (đo tươi, lần cuối, sau khi đã xoá probe) + +- Filtered `~KhkkGroupMenuSeed`: **7/7 PASS**. +- **Infra: 576 PASS / 0 FAIL** (baseline 569 + 7) · **Domain: 45 PASS / 0 FAIL**. +- **Tổng suite = 621.** + +END sub-test-k4a — VERDICT=7/7 PASS (+7 test, suite 621 = 45 Domain + 576 Infra, 0 hồi quy, 0 prod-code touched)