Files
solution-erp/.claude/agent-memory/test-specialist/MEMORY.md
2026-07-27 13:30:58 +07:00

14 KiB
Raw Blame History

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.cs Add changelog (:126) → Remove(pe) (:142) → CascadeDeleteTiming.Immediate cascade ngay tại Remove; dependent đang Added bị cascade ⇒ Detached (không phải Deleted) ⇒ 0 row; AuditingInterceptor chạ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 wire AuditingInterceptor — BẮT BUỘC khi đo Remove()=xóa-mềm, vì TestApplicationDbContext KHÔ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: fixture draft-ts MALFORMED ra exit 0 — inject_A neo ## MIND-1 sau khi renum(...,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/repr từ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; _end giả _pause=9 vs không = chứng nguồn-đếm-là-ĐĨA, dùng diff 2 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]: +8 PePendingAccumulationTests.cs C1-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 (filter ContextNote.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) FluentAssertions ContainInOrder(params T[]) + because-string overload-clash khi T=Guid → bọc new[]{...} 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 (:426 T1 đủ-11-cột · :480 T2 CCM-threshold · :524 T3 all-steps · :555 T4 V1-legacy assert KHÔNG-ApprovedPrice · :638 T5 guard CCM+Admin Conflict + ChoDuyet-allow · :687 T6 AdjustBudget DaDuyet+TuChoi · :730 T7 live-vs-frozen flags/editability · :783 T8 null-safe no-WorkItem + no-pair · :821 T9 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.md củ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 QUA svc.TransitionAsync (V2 1-Bước-1-Cấp AllowApproverFinalize=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 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: 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.

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 (one-line FIFO — verbose in archive/baseline-history.md)

  • S118b +4 ReportsController company-wide authz (Pattern 10 reflection; Dashboard+ExportContracts gate Reports.Read, MyDashboard CỐ Ý 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.Invoke KHÔNG auto-bind optional param → helper phải pass đủ 5 args) → 500.
  • S116 +9 PE báo giá SỐ ÂM (guard Sum()<=0All(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)

  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.