14 KiB
Test-Specialist Agent — Persistent Memory
-
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.csAdd changelog (:126) →Remove(pe)(:142) →CascadeDeleteTiming.Immediatecascade ngay tại Remove; dependent đangAddedbị cascade ⇒ Detached (không phải Deleted) ⇒ 0 row;AuditingInterceptorchạ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 wireAuditingInterceptor— BẮT BUỘC khi đoRemove()=xóa-mềm, vìTestApplicationDbContextKHÔ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: fixturedraft-tsMALFORMED ra exit 0 —inject_Aneo## MIND-1sau khirenum(...,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/reprtừ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;_endgiả_pause=9vs không = chứng nguồn-đếm-là-ĐĨA, dùngdiff2 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]: +8PePendingAccumulationTests.csC1-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 (filterContextNote.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) FluentAssertionsContainInOrder(params T[])+ because-string overload-clash khi T=Guid → bọcnew[]{...}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 (
:426T1 đủ-11-cột ·:480T2 CCM-threshold ·:524T3 all-steps ·:555T4 V1-legacy assert KHÔNG-ApprovedPrice ·:638T5 guard CCM+Admin Conflict + ChoDuyet-allow ·:687T6 AdjustBudget DaDuyet+TuChoi ·:730T7 live-vs-frozen flags/editability ·:783T8 null-safe no-WorkItem + no-pair ·:821T9 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.mdcủ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 QUAsvc.TransitionAsync(V2 1-Bước-1-CấpAllowApproverFinalize=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 RAGsearch_memoryjust-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 (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)
- ✅ DONE S45 — HrmConfig Holiday composite UNIQUE (Year,Date): 7 test (
HrmConfigHolidayTests.cs) + surfaced Mig 43 filtered-index fix - ✅ DONE S45 — EmployeeSatellite FK invariant + soft-delete + cascade: 10 test (
EmployeeSatelliteTests.cs) - ✅ DONE S45 — gotcha #44 authz regression EmployeesController + HrmConfigsController: 10 test (extend
AuthorizePolicyRegressionTests.cs) - Phase 10.3 Proposal ApproveV2 (S37) + Workflow Apps skeleton (S38) — test-after khi UAT confirm
- gotcha #57 (S51+S52 REPRODUCED): HRM
HrmConfigFilteredUniqueTests2 RED (LeaveType+Shift+OtPolicy) → em main Mig 45.HasFilterGREEN. S52 EXT MasterMasterCatalogFilteredUniqueTests3 RED (Department cfg:18 / Project:19 / Supplier:24 bare.IsUnique()) → pending em main fix migration. Vehicle+Driver (Mig 44) ĐÃ filtered. Pattern: seedIsDeleted=trueslot + Create cùng Code → assert active==1 +IgnoreQueryFiltersall==2.
📅 Recent activity (one-line FIFO — verbose in archive/baseline-history.md)
- S118b +4 ReportsController company-wide authz (Pattern 10 reflection;
Dashboard+ExportContractsgateReports.Read,MyDashboardCỐ Ý 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.InvokeKHÔ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)
- ❌ 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.