Yêu cầu anh Kiệt FDC (sau họp sếp). Mig 53 AddPeUrgentAndCeoApprovalThreshold — 3 AddColumn, no new table (Mig 52→53). Rollout an toàn: cột nullable, ngưỡng null = giữ luồng duyệt cũ 100% cho tới khi admin set.
B — CCM duyệt-final theo NGƯỠNG GIÁ TRỊ ("gói CEO phân quyền theo giá trị"):
- ApprovalWorkflow += CeoApprovalThreshold (decimal?, admin nhập trong Workflow Designer).
- ApproveV2Async: actor role CostControl (CCM) + winnerQuoteTotal (tổng giá NCC được chọn) < ngưỡng → DaDuyet luôn (bỏ CEO); ≥ ngưỡng → đẩy lên CEO như cũ. Ngưỡng null = luồng tuyến tính cũ. Q4 chốt nhận diện theo ROLE người duyệt.
- reviewer PASS 0 blocker: cascade-safe (Off/role không lan), tested load-bearing (CCM dưới ngưỡng → DaDuyet skip CEO).
A — cờ gấp per-vai (visibility-only, Q3 KHÔNG đổi luồng):
- PE += IsUrgentByPro (PRO đỏ) / IsUrgentByCcm (CCM xanh).
- Endpoint PUT /purchase-evaluations/{id}/urgent role-gated (Procurement→ByPro, CostControl→ByCcm, Admin→cả 2, khác→Forbidden) + notify CEO (Director) khi MỚI bật (best-effort).
FE ×2 app: Workflow Designer ô "Ngưỡng giá trị gói CEO" (fe-admin) + PE detail nút bật/tắt cờ gấp đỏ/xanh theo role + badge GẤP + hint "giá trị gói vs ngưỡng → CCM duyệt-final/cần CEO" + PE list badge gấp.
DTO: PE detail += isUrgentByPro/Ccm + winnerQuoteTotal + ceoApprovalThreshold; list += isUrgentByPro/Ccm; workflow V2 += ceoApprovalThreshold.
+14 test (292→306): PeCcmThresholdFinalizeTests 5 (B routing) + PeUrgentToggleAuthzTests 9 (A authz). Build slnx 0/0 · npm build ×2 0 err · dotnet test 306 PASS.
C (sau duyệt xong chuyển phiếu đến dự án) — chờ anh Kiệt làm chi tiết form, CHƯA làm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
23 KiB
Test-Specialist Agent — Persistent Memory
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 306 tests = 306 PASS (45 Domain + 261 Infra) ← S69b +14 PE 2 feature anh Kiệt FDC (test-before-merge SECURITY/FINANCIAL): PeCcmThresholdFinalizeTests.cs (5, Services ns, value-threshold CCM-finalize ApproveV2Async) + PeUrgentToggleAuthzTests.cs (9, Application ns, urgent-toggle role authz). Prev 292 ← S69 +6 Office golive permission-seed (OfficeModulePermissionSeedTests.cs, test-after, mirror HrmProfilePermissionSeedTests S67). Prev 286 ← S67 +23 HRM test-after [DepartmentTreeTests 8 cycle-guard/rollup/orphan + PeHoSoLinkTests 9 absolute-set (⚠️spec-drift: HoSoLink gửi null=CLEAR, KHÔNG null-safe như Budget*/WorkItemId) + HrmProfilePermissionSeedTests 6 reflection private-static revoke→seed chain]. em main PROXY-RECORD — return truncated #53 (chết lúc update MEMORY), 3 file delivered + dotnet test 286 PASS verify-on-disk. Prev 263 (S61 +22 PeWorkItemBudget −14 BudgetPolicy; Domain 58→45 drop Budget module). Pre = 254 (S60).
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 (đếm User.DepartmentId active). 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. ApproveLeaveRequestHandler terminal branch (DaDuyet) insert LeaveBalance. Test đi tới DaDuyet PHẢI seed 1 LeaveType row + LeaveRequest.LeaveTypeId = type.Id (KHÔNG random Guid → FK fail SQLite Error 19). Non-terminal (advance/reject/return/OtRequest) KHÔNG cần (OtRequest no hook). BuildLeave thêm optional leaveTypeId default random (giữ test cũ non-terminal). Year = StartDate.Year. Negative allowed (no quota guard → Remaining<0 OK). 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 (last 10 FIFO)
-
2026-06-17 (S69b PE 2 feature anh Kiệt FDC — test-before-merge SECURITY+FINANCIAL workflow): +14 test → 292→306 PASS (45 Domain + Infra 247→261, 0 fail). BE done+builds, mirror harness PeSubmitGuardAndBypassTests/PeWorkItemGuardTests. FEATURE B value-threshold CCM-finalize (
PeCcmThresholdFinalizeTests.cs5, Services ns, ApproveV2Async line 816-854): NV duyệt role=CostControl +aw.CeoApprovalThreshold!=null+winnerQuoteTotal < ngưỡng+ chưa-slot-cuối → Phase=DaDuyet bỏ CEO + pointers/SLA null. ⭐ BOUNDARY load-bearing: predicatewinnerQuoteTotal < ceoThresholdSTRICT-less-than (line 838) → gói==đúng-ngưỡng = KHÔNG finalize = advance. Cover: (1)⭐LOAD-BEARING CCM<ngưỡng mid-wf→DaDuyet skip-CEO pointers-null + chỉ CCM-slot opinion no CEO-opinion / (2) ==ngưỡng→advance Bước2(CEO) stays-ChoDuyet SLA+7d / (2b) >ngưỡng→advance / (3) threshold-null→advance kể-cả-CCM+gói-1đ (backward-compat) / (4) non-CCM(PRO)<ngưỡng→advance (chỉ CostControl trigger, nhận-diện-theo-role) / (5) CCM-at-last-slot<ngưỡng→DaDuyet via NORMAL-advance (guard!(idx==last&&lvl==max)skip finalize-branch, nhánh advance terminal cũng DaDuyet — no double, 1 Approve row). Harness: dựng PE TRỰC TIẾP ở ChoDuyet pin pointer slot CCM (skip submit guard) + drive 1 Approve;SeedWorkflowAsync(stepApprovers, ceoThreshold). FEATURE A urgent-toggle authz (PeUrgentToggleAuthzTests.cs9, Application ns, SetPurchaseEvaluationUrgentCommandHandler 4-dep db+ICurrentUser+UserManager+INotificationService): role→cờ: PRO→IsUrgentByPro / CCM→IsUrgentByCcm / Admin→CẢ2 / else→ForbiddenException. Notify-CEO best-effort try/catch — KHÔNG assert (NoOpNotificationService nuốt; CreateUserAsync idempotent-register role nên GetUsersInRoleAsync(Director) no-throw). Cover: PRO-only-ByPro(Ccm-untouched) / CCM-only-ByCcm / Admin-both / Drafter→Forbidden+no-mutation / Finance→Forbidden / PRO-turn-off clears-only-Pro Ccm-preserved / multi-role PRO+CCM no-Admin→else-if short-circuit chỉ ByPro (LOCK behavior) / unknown-PE→NotFound. No prod bug — cả 2 feature code đúng spec (strict-<intentional rollout-safe, else-if priority Admin>PRO>CCM intentional). FakeCurrentUser configurable-roles ctor. Reuse NoOpNotificationService internal quausing ...Tests.Services. Tag [s69b, pe-ccm-threshold-finalize, value-threshold, strict-less-than-boundary, role-based-routing, urgent-toggle-authz, forbidden-no-mutation, else-if-short-circuit, test-before-merge]. -
2026-06-17 (S69 Office golive permission-seed regression — test-after SECURITY invariant, public Văn phòng số): +6 test
tests/.../Application/OfficeModulePermissionSeedTests.cs→ 286→292 PASS (45 Domain + Infra 241→247, 0 fail). MirrorHrmProfilePermissionSeedTests(S67) — SAME reflection harness (invoke 2 private-staticRevokeTemporarilyHiddenModulesAsync+SeedAllRolesOfficeModulePermissionsAsyncquaGetMethod(name, NonPublic|Static).Invoke(null, [db, rm, NullLogger.Instance]); SqliteDbFixture/IdentityFixture; seed MenuItem rows TRƯỚC Permission FK Cascade). KHÁC HRM: Office grant mở CanRead AND CanCreate (HRM read-only) trên allow-list 16 key; HRM chỉ 2 key. Chain = revoke (StartsWith("Off")→all false non-Admin) → office-grant (allow-list→read+create, upgrade-only). Cover: (1) chain non-Admin allow-list-16 → read+create=true + excluded-3 stay hidden (OffPhongHopManage/OffAttendanceReport/OffChamCong⭐ LOAD-BEARING security assert) / (2) allow-list Update+Delete stay false / (3) no-leak HRM-dashboard+Personal stay hidden / (4) Admin not-revoked keeps all incl excluded-3 / (5) create-missing-row read+create=true update/delete=false + excluded NOT created / (6) upgrade-only preserves admin-raised Update/Delete=true (office-grant chỉ đụng Read/Create, KHÔNG hạ). No prod bug — seed logic đúng spec (excluded-3 confirmed hidden, upgrade-only không phá quyền admin). Tag [s69, office-golive, permission-seed, security-invariant, excluded-3-hidden, read+create-grant, upgrade-only, reflection-private-static, test-after]. -
2026-06-12 (S60 UAT anh Kiệt — 2 feature PE submit branch, test-after build PASS): +14 test
tests/.../Services/PeSubmitGuardAndBypassTests.cs→ 240→254 PASS (58 Domain + Infra 182→196, 0 fail). MirrorPurchaseEvaluationWorkflowServiceGuardTests(IdentityFixture+SQLite, reuseNoOpNotificationServiceinternal). F1 Section 3 guard (8): submit branch (DangSoanThao/TraLai→ChoDuyet) buildmissinglist 4 mục → ConflictException msg gộp prefix'Chưa đủ thông tin mục 3 "Đơn vị NCC/TP được chọn"...'+ join' · '. Cover: thiếu cả 4 / winner-only / winner+quote=0 / budget (cả null+manual=0) / comparison / attachment gắn NCC (PES_Id!=null) KHÔNG đếm bảng so sánh = vẫn Conflict (predicate PES_Id==null) / đủ-4-manual-budget→ChoDuyet / đủ-4-BudgetId→ChoDuyet. F2 drafter-bypass (6, V2-onlyApplyDrafterBypassOnSubmitAsync): k=drafterSlots.Max(Order) bước đầu → auto Cấp 1..k. Cover: drafter=TP(2/2)+2bước→StepIdx=1/Lvl=1+opinion 1 row slot TP+2 AutoApprove / drafter=NV(1/2)→Lvl=2 cùng bước+opinion slot NV / drafter ngoài bước đầu→KHÔNG bypass StepIdx=0 Lvl=1 0-auto / 1-bước+drafter cấp cuối→DaDuyet pointers null SLA null / V1(awId null)→submit OK no-bypass no-crash / TraLai-resubmit→bypass áp lại opinion UPSERT 1 row + approval cộng dồn 2 vết. ⚠️ GUARD-FIRST: mọi bypass-test PHẢI dựng PE đủ 4 ĐK Section 3 (winner+quote>0+manual-budget+comparison-attach) qua guard. Seed pattern S60:SeedWinnerWithQuoteAsync(PES+Detail+Quote ThanhTien) map winner→quote sum ·SeedComparisonAttachment(PES_Id=null) ·SeedWorkflowAsync(Guid[][] stepApprovers)build multi-step V2 1-NV/cấp. Opinion-only-ownSlot invariant: bypass cấp NV skip KHÔNG ghi opinion (chỉ Approval AutoApprove + Changelog vết); assertopinions.HaveCount(1)+ApprovalWorkflowLevelId==drafterSlot.Id. No prod bug — code đúng spec, test theo CODE (S34 rule). Tag [s60, pe-submit-guard, section3-completeness, drafter-bypass, v2-only, guard-first, opinion-ownslot-only, test-after]. -
2026-06-11 (S57bis P2 PE WorkItemId guard Mig 49 — test-after, code đã đúng sẵn): +12 test
tests/.../Application/PeWorkItemGuardTests.cs→ 228→240 PASS (58 Domain + Infra 170→182, 0 fail). PEGuid? WorkItemIdloose-Guid (KHÔNG FK vật lý, convention giống ProjectId). Cover-map 3 trục: (1) Validator ×3 —CreatePurchaseEvaluationCommandValidator.Validate(cmd)plain API (KHÔNG có FluentValidation.TestHelper package): null→invalid+error trên WorkItemId / present→valid. (2) Create-FK-guard ×4 — handler 4-dep instantiate THẬT trên SQLite (new PurchaseEvaluationWorkflowService(db,dt,notify,um)+new PurchaseEvaluationCodeGenerator(db,dt)— Serializable-tx non-issue SQLite proven S52; reuseNoOpNotificationServiceinternal từ ...Services ns + IdentityFixture): bogus-Guid→Conflict / inactive→Conflict / active→OK+saved.WorkItemId==active.Id. (3) Update-null-safe ×5 (bug-class S42 picker) —UpdatePurchaseEvaluationDraftCommandHandler(db,cu)2-dep nhẹ: request.WorkItemId=null→GIỮ w1 KHÔNG null-hoá (⭐ core) / W2-active→đổi / bogus→Conflict+giữ w1 (AsNoTracking re-read DB-truth) / inactive→Conflict / same-as-existing→skip-lookup-success. ⚠️ SPEC-DRIFT FOUND (test theo CODE, S34 rule):NotEmpty()trênGuid?(nullable) chỉ bắtnull, KHÔNG bắtGuid.Empty(FV 7.2 so default(Guid?)==null) → Guid.Empty PASS validator. KHÔNG phải lỗ hổng — create handler FK-guard (is Guid wiIdtrue cho Empty + AnyAsync false) chặn → Conflict. Test LOCK behavior (1 validator-test assert Empty pass + 1 handler-test chứng minh defense-in-depth catch). REPORT em main: validator một mình không reject Guid.Empty, dựa handler. No prod bug — code đúng spec, defense-in-depth layered. Tag [s57bis, p2, pe-workitemid, mig49, validator-plain-api, null-safe-partial-update, guid-empty-nullable-notempty-drift, defense-in-depth]. -
2026-06-09 (S56 GOLIVE-HARDEN TEST stage — 4 pre-golive fixes, test-after build): +12 test → 216→228 PASS (58 Domain + Infra 158→170, 0 fail). Build stage đã land prod fixes (CONTRACT từ build, signatures UNCHANGED). #3 LeaveBalance lost-update fix: handler terminal nay increment
db.LeaveBalances.Where(...).ExecuteUpdateAsync(s=>s.SetProperty(b=>b.UsedDays, b=>b.UsedDays+p.NumDays))server-side + 1 explicit tx (READ COMMITTED, NO IsolationLevel). ⭐ GOTCHA: ExecuteUpdateAsync BYPASS change tracker → instance bal tracked (Add STEP1 hoặc pre-seed cùng context) GIỮ UsedDays PRE-increment. 4 test cũ LeaveBalanceTests (case 1/2/3/4 line 163/201/240/269) FAIL ở baseline = stale-tracked-read, KHÔNG regression (spec TEST GUIDANCE đã tiên đoán). Fix =.AsNoTracking()re-read (hoặcChangeTracker.Clear()). +2 new:TwoSeparateRequests_BothTerminal_UsedDaysAccumulates_NotOverwrites(3+5=8 chứng minh increment accumulate KHÔNG overwrite = race-free invariant) +Approve_AlreadyDaDuyet_ReApprove_ThrowsConflict_NoDoubleDeduct(early guard Status!=DaGuiDuyet:296 → exactly-once, balance vẫn 3 not 6). #4 Travel/Vehicle ApproveV2 smoke (WorkflowAppApproveV2Tests.cs +4): mỗi module Submit→Approve→DaDuyet happy + outsider→Forbidden. ApplicableType Travel=9 prefixDT/CT, Vehicle=7 prefixDX/XE. Travel/Vehicle KHÔNG trừ balance → không seed LeaveType. Helper mớiSeedWorkflowForTypeAsync(type,code,...approverIds). #5 ItTicket existence-oracle (ItTicketReassignAuthzTests.cs +2): authz reorder (Forbidden TRƯỚC NotFound) — non-IT non-admin nhận Forbidden cho ticketId tồn tại VÀ không tồn tại (cặp 5b/5c phản hồi giống nhau = no oracle leak). Reorder KHÔNG vỡ test cũ (Case5 đã expect Forbidden; TicketNotFound dùng Admin caller pass authz hợp lệ). #6 DocxRenderer (Forms/DocxRendererTests.cs NEW +4): 0 test trước đó. MainDocumentPart null→InvalidOperationException("*MainDocumentPart*")(OpenXml 3.5.1WordprocessingDocument.Create(path,type)tạo package RỖNG no main part) + placeholder replace happy + unknown-key giữ literal + null-value→empty. ⚠️ test helper ExtractBodyText: tránhMainPart!.Document.Body!(CS8602 warning) → dùng?.Document?.Body+.Should().NotBeNull(). No prod bug found — tất cả fixes là build-stage, tôi WRITE test theo CONTRACT. Tag [s56, golive-harden, executeupdate-tracker-bypass, asnotracking-reread, travel-vehicle-smoke, existence-oracle, docxrenderer]. ↳ [em main post-review S56] Shipped tx =IsolationLevel.Serializable(codeLeaveOtApprovalFeatures.cs:369), KHÔNG READ COMMITTED — entry's '(READ COMMITTED, NO IsolationLevel)' = build-stage snapshot, superseded post-review (SQLite test path unaffected — codegen Serializable already green). -
2026-06-08 (S54 ItTicket reassign authz — test-before-merge SECURITY) [harvested by em main — agent MEMORY write mis-landed, B2/B3]: +13 test
tests/.../Application/ItTicketReassignAuthzTests.cs→ 203→216 PASS (58 Domain + Infra 145→158, 0 fail). GetAssignableItStaff (6): Admin→CanReassign=true + 2 IT-active ordered FullName (Cao<Truong) no KT/inactive leak · IT-staff→true · non-IT non-admin (KT)→false + empty staff (0-leak assert) · dept-null→false+empty · inactive-IT-excluded · UserId null→Unauthorized. AssignItTicket (7): non-IT non-admin→ForbiddenException + side-effectAssignedToUserId.Should().BeNull()(no-mutation) · Admin+assignee∈IT→success · IT-staff+assignee∈IT→success · assignee∉IT(KT)→ConflictException "Người được giao phải thuộc tổ IT." · assignee inactive→NotFound · ticket not found→NotFound · null→Unauthorized. Pattern mới: authz-capability test = seed 2-dept (IT+KT) + fakeICurrentUserrole/dept matrix; assert canReassign flag + Forbidden/Conflict guard; empty-staff = 0-leak. Forbidden red-able by-contrast (case5 non-IT vs case7 IT-staff identical-setup → chỉ khác caller-identity; rule cấm sửa prod để chứng minh RED). No prod bug — handler-level data-dependent authz (caller-dept vs IT-dept) = CORRECT pattern, KHÔNG phải gotcha #44 silent-403 gap (Pattern 10 reflection-regression chỉ cho static[Authorize(Policy)]; data-driven authz PHẢI ở handler = enforcement point, test cover tại đó). Tag [s54, it-ticket-reassign, authz-capability, forbidden-conflict-guard, test-before-merge, 0-leak]. -
2026-06-08 (S52 P11-D Master gotcha #57 EXT) [test-before · 3 RED LIVE]: +3 test
tests/.../Application/MasterCatalogFilteredUniqueTests.cs(run--filter MasterCatalogFilteredUnique→ Failed 3/Passed 0). Department+Project+Supplier.IsUnique()BARE (Dept cfg:18 / Proj:19 / Supp:24) chưa[IsDeleted]=0— cùng class gotcha #57. Mirror EXACT GROUP B HrmConfigFilteredUniqueTests: seed rowIsDeleted=trueslot Code="DUP1" →Create{Dept|Project|Supplier}CommandHandler(db)cùng Code → assertNotThrowAsync+ active==1 +IgnoreQueryFiltersall==2. 3 RED =DbUpdateException → SQLite Error 19 UNIQUE constraint failed: {Departments|Projects|Suppliers}.Code(app-checkAnyAsync(Code==X)chạy QUA HasQueryFilter → loại soft-deleted → PASS → Add+SaveChanges → DB UNIQUE bare đếm cả row xoá → throw). NOT test lỗi — REPORTED em main fix migration.HasFilter3 config → flip GREEN. ⚠️ all-count PHẢIIgnoreQueryFilters()(khác HRM ref dùng rawCount(Code==X)trên DbSet đã có HasQueryFilter → trả 1 not 2 = sai; tôi sửa = active-count plain DbSet, all-count IgnoreQueryFilters). 3 handler clean(IApplicationDbContext db)1-dep. KHÔNG đụng Configuration/Domain/migration. Tag [s52, p11-d, gotcha-57, master-catalog, filtered-unique, test-before, RED]. -
2026-06-08 (S52 P11-D Wave2 round-robin + SLA-due) [proxy by em main: agent killed session-limit trước MEMORY step]: +9 test
ItTicketAssignSlaTests.cs→ 200 PASS (Infra 133→142). Round-robin: seed Department Code="IT" + 2 user A/BIsActivetrong IT + A có 1 ticket Open → Create → assign B (load 0<1); tie A=B →ThenBy(Id); edge no-dept-IT / no-user-IT → unassigned; user ngoài IT hoặcIsActive=falseKHÔNG assign. SLA-due: Priority Urgent→+4h / High→+8h / Medium→+24h / Low→+72h (asserte.SlaDueAt==CreatedAt+SlaWindow[priority]). Regression P11-F: create vẫn gen^IT/\d{4}/\d{3}$.ItTicketSlaJobBackgroundService SKIP unit-test (breach-query inline, khó test trực tiếp — REPORTED). Baseline 191→200 (58 Domain + 142 Infra). Tag [s52, p11-d, round-robin, sla-due, regression]. -
2026-06-08 (S52 P11-E + P11-F WorkflowApps/Attendance test-after): +5 test → 191 PASS (Infra 128→133). 2 file
tests/.../Application/: ItTicketCodeGenTests (3 — MaTicket regex^IT/\d{4}/\d{3}$+ sequential 001→002 cùng prefixIT/{year}LastSeq++ + per-year-prefix 2027 reset 001) + AttendanceReportTests (2 — full aggregate day-type/weighted + DepartmentId filter). ⭐ Serializable-on-SQLite GOTCHA = NON-ISSUE (confirmed):WorkflowAppCodeGen.GenerateMaDonTuAsyncdùngBeginTransactionAsync(IsolationLevel.Serializable)chạy SẠCH trên SQLite — provider map isolation level gracefully (no throw), format+seq+per-year đều hold KHÔNG cần try/skip. Đã proven sẵn bởi WorkflowAppApproveV2Tests (DT/LR path). HandlerCreateItTicketHandler(db, cu, clock)= 3 dep MediatR. Day-type test pattern (P11-E core): holiday check chạy TRƯỚC weekend/weekday → seed 2026-06-01 (thứ Hai) vào holidaySet → assert phân Holiday dù là weekday (override day-of-week). Holiday.Date=DateOnly →BuildHolidaydùngDateOnly.FromDateTime. OtWeighted = 2×1.5+3×2.0+1×3.0=12.0m. DepartmentId filter: seed 2 Department row + 2 user khác dept → query deptA chỉ trả 1 row (handler join Usersu.DepartmentId==deptId, userMeta dùngDefaultIfEmptynên dept row optional nhưng seed cho DepartmentName assert). No prod bug. ⚠️ MSBuild OOM chạy full parallel → dùng-maxcpucount:1 -p:BuildInParallel=false(env resource, KHÔNG test fail). Tag [s52, p11-e, p11-f, codegen, day-type, serializable-sqlite-ok, test-after]. -
2026-06-08 (S51 P11-C HMW Wave2 filtered-unique gotcha #57): +4 test
tests/.../Application/HrmConfigFilteredUniqueTests.cs→ 185 total = 183 PASS + 2 RED (Infra 123→127). Mirror HolidayTests Case 7 (seed soft-deleted Code-slot → Create same Code → assert success + active==1 + all==2). 2 GREEN Vehicle+Driver (Mig 44 config ĐÃ filtered → 2 catalog mới đúng). 2 RED INTENTIONAL = gotcha #57 REPRODUCED (test-before):CreateLeaveType_OnSoftDeletedCodeSlot...→SQLite Error 19 UNIQUE constraint failed: LeaveTypes.Code+CreateShift_OnSoftDeletedCodeSlot...→ShiftPatterns.Code(bare.IsUnique()đếm cả row soft-deleted; handler app-check!IsDeletedPASS → Add+SaveChanges → DbUpdateException). NOT test lỗi — REPORTED em main fix Mig 45.HasFilter("[IsDeleted]=0")cho 2 config → flip GREEN. ⚠️ Soft-delete trong test (giống Holiday): AuditingInterceptor (prod soft-delete Deleted→Modified+IsDeleted=true) KHÔNG wire trong SqliteDbFixture →Remove+SaveChanges= HARD delete (không test được). PHẢI seed rowIsDeleted=truethủ công để mô phỏng slot bị chiếm. Handlers chỉ cần IApplicationDbContext →new CreateXxxHandler(db). Tag [s51, p11-c, gotcha-57, filtered-unique, test-before].
⚠️ 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.