20 KiB
Test-Specialist Agent — Persistent Memory
- 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 (2026-07-14) ReportsController company-wide-report authz REGRESSION (test-after SECURITY — reviewer catch) — 505→509 PASS (+4 Infra), no-red, NO prod bug: EXTEND
Api/AuthorizePolicyRegressionTests.cs(Pattern 10 reflection, +4 [Fact] → file 15→19). Fix S118: 2 endpoint company-wideDashboard+ExportContracts(lộ tài chính HĐ toàn cty: tổng giá trị, top NCC/dự án, xu-hướng-12-tháng, export-Excel) NAY gate[Authorize(Policy="Reports.Read")](chỉ Admin, khớp S92);MyDashboardCỐ Ý giữ mở (chỉ class-level[Authorize]trần, scope currentUser). Trước: cả controller class-level[Authorize]trần → mọi user đăng nhập gọi được = lộ tài chính. T1 class-level[Authorize]Policy=null+Roles=null (bare — nếu hardcode Policy sẽ khóa my-dashboard). T2/T3 Dashboard+ExportContracts action-Policy==Reports.Read(gỡ→NotBeNull-fail = tái-lộ toàn cty). T4 MyDashboard(attr?.Policy).Should().BeNull()— cho phép bare[Authorize]vô-hại NHƯNG chặn mọi Policy (thêm policy→khóa dashboard cá-nhân role không-Admin). csproj ĐÃ ref Api (line 25, từ S45) — KHÔNG vướng reference. KHÔNG touch prod. -
S118 (2026-07-14) Procurement master-access seeder SCOPE-ISOLATION regression (test-after SECURITY — anh Kiệt FDC) — 501→505 PASS (+4 Infra), no-red, NO prod bug: new
ProcurementMasterAccessSeedTests.cs(4 [Fact]) guard seeder MỚISeedProcurementMasterAccessAsync(private static, DbInitializer) — NGOẠI-LỆ role Procurement mở lại Danh mục SAU S92 revoke ("chỉ Admin thấy"). Chain thật: Revoke→SeedProcurement. T1 upgrade-path Procurement sau revoke→seed đúng ma trận (Suppliers R+C+U D=false + 8 read-only chỉ R). T2 ⭐SCOPE-ISOLATION (core) DeptManager đứng RIÊNG (KHÔNG kèm Procurement): pre-grant→revoke→seed → 9 key VẪN all-false (row tồn tại giữ vết) = grant KHÔNG rò role khác; +CONTRAST assert Procurement THẬT được nâng (chống vacuous-pass). T3 insert-path DB mới no-revoke → INSERT 9 row, Delete=false + count==9. T4 idempotent seed ×2 → bất biến + count==9 no-dup + role khác vẫn ẩn. Reflection harness MIRRORAdminOnlyModulesRevokeTests(NonPublic|Static, sig (ApplicationDbContext,RoleManager,ILogger), FK seed MenuItem→Role→Permission). No user cần (perm role-level). Reflection KHÔNG vướng — KHÔNG cần đổi visibility. KHÔNG touch prod. -
S117 (2026-07-14) PE "duyệt theo KHOẢNG" ProMinMax range-price TEST-BEFORE-MERGE (financial-critical) — 495→500 PASS (+5 Infra, file 10→15), no-red, NO prod bug: feature anh Kiệt: PRO có cả Min&Max → người duyệt cấp cuối chọn CẢ HAI (source="ProMinMax") →
ApprovedPriceAmount=Min,ApprovedPriceMaxAmount=Max. Bất-biếnMax!=null ⟺ Source=="ProMinMax". Prod đã land:ApplyApprovedPriceOnFinalize(...,decimal? maxAmount=null)+ValidApprovedPriceSources+"ProMinMax";TransitionAsync/ApproveV2Async+approvedPriceMaxAmount(SAU source). 5 new [Fact] qua reachable terminal path (mirror case-1 SeedSingleApproverWorkflow, KHÔNG gọi private): (a) happy Min100/Max130 set cả 2 · (a-boundary) min==max OK lock guard>vs>=(revert→RED) · (b) thiếu Max→Conflict "phải có cả giá Min và giá Max" not-finalized · (c) min>max→Conflict "không được lớn hơn giá Max" · (d) nguồn ĐƠN+Max→Conflict "kèm giá Max" wire-bug-guard. +regression assertMax==nullvào Theory 3b (4 nguồn đơn). ⚠️ Gotcha (self-inflicted-avoided): prod method 5-param nay →InvokeApplyreflection helper phải pass 5 args ({...,maxAmount}); optional param KHÔNG auto-bind quaMethodInfo.Invoke→ nếu giữ 4 args =TargetParameterCountExceptionphá unit-test 4/4b/4c. Đã fix helper. KHÔNG touch prod. -
S116 (2026-07-13) PE báo giá SỐ ÂM regression-lock (test-before cho guard R2-revised — critical-algo/financial) — 486→495 PASS (+9 Infra), no-red, NO prod bug: feature anh Kiệt cho ThanhTien ÂM (phát-sinh-giảm/hoàn tiền NCC = giá THẬT). BE ĐÃ đổi submit-guard
:202-208Sum()<=0→Count==0(chưa chọn)/else All(x==0)(chưa có giá). 4 nhóm test khớp hành-vi MỚI: T1PeWorkItemBudgetTests(+1) quote ÂM −20tr KHÔNG chạm PeWorkItemBudget record (FullAmount ngân sách bất biến) NHƯNG CHẢY vào CurrentProposalTotal=−20tr (chứng tách-biệt cột đề-xuất vs ngân-sách, "0 code" kiến-trúc). T2PeMultiWinnerTests(+2) winnerQuoteTotal=SUM CÓ DẤU khác-winner(+50,−20=30) + cùng-winner-2-dòng(80,−30=50). T3PeSubmitGuardAndBypassTests(+4, +2 helperSeedWinnerWithQuotesAsyncparams +BuildV2PeReadyToSubmitWithQuotesAsync): all-zero[0,0]→BLOCKED "chưa có giá"; pure-neg[−500k]→ChoDuyet; mixed net>0→ChoDuyet; 🔒 mixed net==0 từng-dòng≠0 [+500k,−500k]→ChoDuyet = ranh giới phân-biệt All(==0) vs Sum()<=0 (revert→RED). T4PeCcmThresholdFinalizeTests(+2, +helper 2-signed-quote): NET threshold — [+1.2tỷ,−0.3tỷ]=0.9tỷ<ngưỡng→CCM finalize no-throw + CONTRAST [+1.2tỷ] alone=1.2tỷ≥ngưỡng→Conflict (dòng âm LOAD-BEARING, R-CEO giữ NET). KHÔNG touch prod. -
S113 (2026-07-12) Supplier import v2 dedup + publish-guard TEST-AFTER (critical-algo + security) — 458→477 PASS (+19 Infra), no-red, 🔴 1 BUG-FOUND (REPORTED em-main, NOT fixed): 2 new files
Services/SupplierImportV2DedupTests.cs(13) +Application/SupplierPublishAndListTests.cs(6). Existing S112 file UNTOUCHED (re-baked ExpectedHeaderTokens=30 \n-token → real tokens khớp → Case9 tự flip LayoutValid=true). Dedup precedence existing-MST→existing-Code→batch-MST→batch-Code→New: strip-ws-normalize MST-match · 2-blank-MST=2-insert (blank không index) · same-MST-batch=1-insert+fill-null-skip · Code-backstop CI (S112: dedup ở SERVICE OrdinalIgnoreCase, SQLite BINARY KHÔNG bắt "backstop"≈"BACKSTOP" → xanh=chứng không dựa DB) · MST→A/Code→B = MST-precedence WINS, no silent-clobber (FillNulls skip Code → A.Code giữ, B untouched). PublishPublishSupplierCommand(KHÔNG UpdateSupplier #73): gate D2 Publish+blank-Code→Conflict "Cần Mã NCC" · unique-exact→Conflict "đã tồn tại" (⚠️x.Code==newCodeDB-collation, SQLite bắt exact-case only — prod SQL CI bắt CI-dup; asymmetry vs import, observation không bug) · unpublish always. ListSuppliers Published t/f/null · GetSupplier DTO.IsPublic. 🔴 BUG:NewSupplierCode=r.Code!.Trim()→ NRE khi row.Code==NULL (path R4 nháp: parser sinh null ô rỗng + preview classify New no-error → confirm NRE → 500 CẢ batch dbCount=0). Empty ""=OK (T8/T14). Characterization test CHỐT NRE → fix null-safe thì flip NotThrow+draft. -
D4 (2026-07-12) PE sign-off A1 + EDGE-5 + opinion-history TEST-BEFORE-MERGE (critical-algo bug-fix + security) — 450→458 PASS (+8 Infra, 4 updated in-place), no-red, NO prod bug: BE đổi
ApplyDrafterBypassOnSubmitAsyncsang A1 trongPeSubmitGuardAndBypassTests.cs. 4 updated + RENAMED (tên cũ misleading): drafter bypass CHỈ cấp NV dưới THUẦN (Order<minOwn); pointer DỪNG tại minOwn (người soạn ký TAY qua ApproveV2Async); KHÔNG auto-sign opinion chính chủ / KHÔNG auto-terminal-on-submit / KHÔNG auto nhảy Bước. minOwn=1 (drafter Cấp1 hoặc non-contiguous Cấp1+3) → bypass RỖNG (opinion=0, AutoApprove=0). 8 new: A1 happy-path (drafter ký Cấp2 comment THẬT — canned "duyệt tự động" đã GỠ khỏi prod, grep 0-match) · non-contiguous full-manual-chain đúng thứ tự · EDGE-5 ×4 (non-admin Reject từ DaDuyet/TuChoi → Conflict "trạng thái kết thúc"; Admin + system[actorUserId=null+AutoApprove] MIỄN → fall-through "không hỗ trợ" chứng minh qua-guard) · opinion-history ×2 (re-sign đè opinion non-empty → đúng 1 Changelog Workflow/Update EntityId=level.Id ContextNote chứa comment cũ; first-sign / đè-rỗng → 0 row, filter theo EntityId==levelId loại được Trả-lại-changelog EntityId=null). Helpers:ApproveAsync(approvedPrice khi ký cấp CUỐI terminal — ApplyApprovedPriceOnFinalize Conflict nếu thiếu, isSystem miễn) +ReturnToDrafterAsync(AllowReturnToDrafter default TRUE trên Level). Empty-opinion chỉ reachable qua seed-trực-tiếp (normalizedComment placeholder-hoá whitespace). -
D3 (2026-07-12) Supplier Excel-import BE (Phase B) TEST-BEFORE-MERGE (CRITICAL-ALGO + case-collation bug-class) — 440→450 PASS (+10 Infra), no-red, NO prod bug: new
Services/SupplierExcelImportServiceTests.cs(10 [Fact]). Case-collation (THE bug): ConfirmAsync lowercasetruonggiangvs DBTRUONGGIANG→ Inserted=0/Updated=1/count=1 (dedup ở SERVICEOrdinalIgnoreCase, KHÔNG dựa DB — SQLite fixture BINARY collation KHÔNG bắt CI-dup, prod SQL Server CI thì có → nếu Ordinal = 500 batch); Preview classify Update. Fill-nulls SAFE (non-null giữ / null điền). All-or-nothing (1 hard-error→Committed=false + DB unchanged). MapType unknown→NhaCungCap+warning · emoji-strip status → DangHoatDong · valid-layout absolute-index parse (LinkGuq RAW backslash) · no-column-drift (col9 empty→col12 vẫn TaxCode) · #REF! literal→null. Pattern (reuse): header xlsx dựng từExpectedHeaderTokensqua REFLECTION (GetField(NonPublic|Static)) → valid-layout test tự đồng bộ khi tokens sửa; ClosedXML in-memXLWorkbook.AddWorksheet+ws.Cell(r,c).Value=stringGIỮ TEXT (0.105 no numeric-coercion → leading-zero safe). Spec-drift FLAGGED (không phải bug):ExpectedHeaderTokens= BEST-GUESS ≠ 30 real row-4 token → Case 9 dynamicLayoutValid==(NormalizeJoin(real)==NormalizeJoin(reflected))robust 2-chiều, hiện LayoutValid=false; close-review sẽ sửa tokens → test tự flip true. -
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-finalizeApproveV2Async. ListApproved (DaDuyet-only) đọc THẲNG field (CreateContractFromEvaluationFeatures.cs:196); List/Inbox ternaryPhase==DaDuyet ? field : subquery. Spec-change RED handled:PeFinalizeProjectionTestsseed-helper +paramendedByLevelFinalize→ case cũpeFinset true; REPRO case mớipeFinButWentCeo: 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). ServicePeApproverFinalizeTests+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
RevokeTemporarilyHiddenModulesAsyncpredicate +Contracts/Ct_/Master/Suppliers/Projects/Departments/Catalog (cộng dồn S58 Hrm*/Off*/Personal) + TẮT 2 re-grant S65/S69 + narrowInReviewScope→Pe_*.AdminOnlyModulesRevokeTests.cs(3): NonAdmin-S92Modules-4cờ-false LOAD-BEARING + Admin-untouched + Pe-module-NOT-revoked invariant (rootPurchaseEvaluations+Pe_DuyetNcckhô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)
- ❌ 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.