Files
solution-erp/.claude/agent-memory/test-specialist/MEMORY.md
pqhuy1987 17b23a418a
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
[CLAUDE] Docs: Harness-4 two-tier runtime-VERIFIED (spawn-test 2 chiều post-restart) + email-back AI_INFRA
- Spawn-test 2 chiều S57bis: H1 tooling-auditor (demote pin) self-report claude-opus-4-8[1m] + H2 harvest-curator (promote inherit) self-report claude-fable-5[1m] → nấc executed-file/PENDING-RESTART → RUNTIME-VERIFIED (adap-report §2/§5 + STATUS row). [1m] 1M-resolve SE tự verify.
- Email update 2026-06-11-se-to-ai_infra-harness-4-runtime-verified (nac sent, sha ecf1d587, honest n=1/chiều, hmw.js executed-file giữ) + _index OUTBOUND.
- Lesson env: CCD harness cache agent frontmatter — restart CLI mới ăn (2 data-point 06-10/06-11).
- Bundle 06-10 carry: 7 agent pin opus-4-8 + 4 inherit + hmw.js tier-map H4.5 + agents/README two-tier + 2 adap-report + email 06-10 + agent-memory delta (KEEP-ALL-5 H2-verified) + investigator L1→L2 archive curate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:12:22 +07:00

17 KiB
Raw Blame History

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 RAG search_memory just-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 240 tests = 240 PASS (58 Domain + 182 Infra) ← S57bis +12 (PeWorkItemGuardTests: 3 validator + 4 create-FK-guard + 5 update-null-safe). Pre = 228 (S56).

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.

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)

  1. DONE S45 — HrmConfig Holiday composite UNIQUE (Year,Date): 7 test (HrmConfigHolidayTests.cs) + surfaced Mig 43 filtered-index fix
  2. DONE S45 — EmployeeSatellite FK invariant + soft-delete + cascade: 10 test (EmployeeSatelliteTests.cs)
  3. DONE S45 — gotcha #44 authz regression EmployeesController + HrmConfigsController: 10 test (extend AuthorizePolicyRegressionTests.cs)
  4. Phase 10.3 Proposal ApproveV2 (S37) + Workflow Apps skeleton (S38) — test-after khi UAT confirm
  5. gotcha #57 (S51+S52 REPRODUCED): HRM HrmConfigFilteredUniqueTests 2 RED (LeaveType+Shift+OtPolicy) → em main Mig 45 .HasFilter GREEN. S52 EXT Master MasterCatalogFilteredUniqueTests 3 RED (Department cfg:18 / Project:19 / Supplier:24 bare .IsUnique()) → pending em main fix migration. Vehicle+Driver (Mig 44) ĐÃ filtered. Pattern: seed IsDeleted=true slot + Create cùng Code → assert active==1 + IgnoreQueryFilters all==2.

📅 Recent activity (last 10 FIFO)

  • 2026-06-11 (S57bis P2 PE WorkItemId guard Mig 49 — test-after, code đã đúng sẵn): +12 test tests/.../Application/PeWorkItemGuardTests.cs228→240 PASS (58 Domain + Infra 170→182, 0 fail). PE Guid? WorkItemId loose-Guid (KHÔNG FK vật lý, convention giống ProjectId). Cover-map 3 trục: (1) Validator ×3CreatePurchaseEvaluationCommandValidator.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; reuse NoOpNotificationService internal 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ên Guid? (nullable) chỉ bắt null, KHÔNG bắt Guid.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 wiId true 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ặc ChangeTracker.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 prefix DT/CT, Vehicle=7 prefix DX/XE. Travel/Vehicle KHÔNG trừ balance → không seed LeaveType. Helper mới SeedWorkflowForTypeAsync(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.1 WordprocessingDocument.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ánh MainPart!.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 (code LeaveOtApprovalFeatures.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.cs203→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-effect AssignedToUserId.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) + fake ICurrentUser role/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 row IsDeleted=true slot Code="DUP1" → Create{Dept|Project|Supplier}CommandHandler(db) cùng Code → assert NotThrowAsync + active==1 + IgnoreQueryFilters all==2. 3 RED = DbUpdateException → SQLite Error 19 UNIQUE constraint failed: {Departments|Projects|Suppliers}.Code (app-check AnyAsync(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 .HasFilter 3 config → flip GREEN. ⚠️ all-count PHẢI IgnoreQueryFilters() (khác HRM ref dùng raw Count(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.cs200 PASS (Infra 133→142). Round-robin: seed Department Code="IT" + 2 user A/B IsActive trong 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ặc IsActive=false KHÔNG assign. SLA-due: Priority Urgent→+4h / High→+8h / Medium→+24h / Low→+72h (assert e.SlaDueAt==CreatedAt+SlaWindow[priority]). Regression P11-F: create vẫn gen ^IT/\d{4}/\d{3}$. ItTicketSlaJob BackgroundService 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 prefix IT/{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.GenerateMaDonTuAsync dùng BeginTransactionAsync(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). Handler CreateItTicketHandler(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 → BuildHoliday dùng DateOnly.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 Users u.DepartmentId==deptId, userMeta dùng DefaultIfEmpty nê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.cs185 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 !IsDeleted PASS → 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 row IsDeleted=true thủ 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.cs152 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 → Remaining8 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ùng LeaveTypeId=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.


⚠️ 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

🔄 Curate trigger

Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): Tests.