- BE: GetAssignableItStaffQuery {canReassign,staff} capability endpoint + AssignItTicketHandler authz Admin-OR-dept-IT (Forbidden) + assignee-must-IT (Conflict); controller /assign hạ [Authorize(Roles=Admin)]→[Authorize] (handler enforce fine-grained data-driven)
- FE: fe-admin + fe-user ItTicketsPage SHA256-identical (reverse S53 divergence), nút gate by canReassign, dropdown từ /assignable-staff (không /users)
- Test: +13 authz guard (203→216 PASS), reviewer PASS (role-string Admin chain-verified real)
- No migration (DepartmentId reuse), no menu change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 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 191 tests = 191 PASS (58 Domain + 133 Infra) ← S52 +5 (3 ItTicket codegen + 2 AttendanceReport). gotcha #57 RED đã GREEN (em main Mig 45 fix landed → baseline post-fix 186, +5 S52 = 191).
Run: dotnet test SolutionErp.slnx --nologo --verbosity minimal
⚠️ 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-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]. -
2026-05-30 (S43 P11-B Wave3 LeaveBalance): +8 test
tests/.../Application/LeaveBalanceTests.cs→ 152 PASS (Infra 86→94). Deduction hook (ApproveLeaveRequestHandler terminal) full: deduct single-level (create row from DaysPerYear), only-at-terminal multi-level (advance no-deduct + 1× terminal), accumulate UPSERT (5+2=7 no new row), negative allowed (Used20>Entitled12 → Remaining−8 no throw), Reject+Return no-deduct (split 5a/5b), GetMyLeaveBalances lazy synth (2 active type filter inactive), AdjustLeaveBalance upsert. ⚠️ FOUND + FIXED 2 pre-existing RED in S42 template (Approve_LastLevel_TransitionsToDaDuyet+Approve_EmptyComment_StoresPlaceholder): Wave 1 deduction hook (uncommitted, prod) làm terminal insert LeaveBalance FK→LeaveTypes Restrict FAIL vì BuildLeave dùngLeaveTypeId=Guid.NewGuid(). NOT prod bug (prod đơn luôn pin LeaveType thật) — fix tại test: BuildLeave +optional leaveTypeId, seed LeaveType ở 2 test đó. Baseline thật trước S43 = 142-pass/2-RED (KHÔNG phải 144-green). REPORTED em main. -
2026-06-01 (S45 HRM coverage gaps + Holiday drift) [em main proxy]: +27 test → 181 PASS (Infra 96→123). 3 file: HrmConfigHolidayTests (7 — composite UNIQUE Create/Update, ⭐self-update giữ key đổi Name no-false-positive, soft-delete exclusion) + EmployeeSatelliteTests (10 — 5× FK-invariant parent
AnyAsync(!IsDeleted)guard + soft-delete + cascade-non-behavior Case5 + EF modelDeleteBehavior.Cascadeconfig assertion) + AuthorizePolicyRegressionTests extend (10 — HrmConfigs bare-[Authorize]+writesRoles=Admin; Employees class-Policy=Hrm_HoSo.Read+per-action). FOUND drift (test theo CODE = single source): Holiday DB UNIQUE (Year,Date) unfiltered vs handler!IsDeleted→ recreate-on-soft-deleted-slotDbUpdateException(500). REPORTED → em main fixed Mig 43.HasFilter("[IsDeleted]=0")(Case 7 flipped assert SUCCESS). New pattern: EF model-metadata assertiondb.Model.FindEntityType(typeof(X)).GetForeignKeys()...DeleteBehaviorlock schema intent. ⚠️ gotcha #57 backlog: LeaveType.Code + ShiftPattern.Code vẫn unfiltered.
⚠️ 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.