Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-implementer-backend-4.md
2026-07-27 11:55:41 +07:00

7.5 KiB
Raw Blame History

sub-implementer-backend-4 — WAVE 3: vá 6 test đỏ (hạng mục F, S155)

Run: 2026-07-27-S155-pe-delete-approver · vai: implementer-backend · ngày 2026-07-27


BƯỚC 1 — ĐO TRƯỚC KHI SỬA (không sửa mù)

Lệnh: dotnet test SolutionErp.slnx --nologo -v minimal

Failed!  - Failed: 6, Passed: 499, Skipped: 0, Total: 505 - SolutionErp.Infrastructure.Tests.dll (net10.0)

(Domain: 45 PASS — không đỏ.)

6 đỏ ĐÚNG như bảng lead giao. Thông điệp lỗi THẬT (trích từ output):

# Test Exception THẬT Ném tại
1 F1c_SafeUpdate_LogsChangelogWithReason_OnlyToUnfinishedPes DbUpdateConcurrencyException: expected to affect 1 row(s), but actually affected 0 row(s) ApprovalWorkflowV2AdminFeatures.cs:806 (SaveChangesAsync)
2 F12_SafeUpdate_AddsPersonToExistingLevel_RunningPeAcceptsNewApproverImmediately y hệt #1 :806
3 F16_ProposalWorkflow_SafeUpdate_Succeeds_AndWritesNoPeChangelog y hệt #1 :806
4 F14_RemoveApproverWithLiveOpinion_BlockedPolitely_NotFkViolation InvalidOperationException: association ... 'ApprovalWorkflowLevel' and 'PurchaseEvaluationLevelOpinion' ... has been severed, but the relationship is ... required UpdateAwDefinitionTests.cs:580 (dòng CHỨNG-NHÂN của CHÍNH TEST, không phải handler)
5 F14_F5a_OpinionBelongsToDeletedPe_RemovalAllowed_AndOrphanPurged y hệt #4 handler :755 db.ApprovalWorkflowLevels.Remove(gone)
6 ZZ_DIAG_AddLevel giàn giáo chẩn-đoán, tự ném Exception để in tracker

Dump quyết định (từ ZZ_DIAG):

ENTRIES=[ApprovalWorkflowLevel/Modified/Id=60537ebc-...]
TRACKER=[ApprovalWorkflow/Modified || ApprovalWorkflowLevel/Modified || User/Unchanged ×3
         || Role/Unchanged || IdentityUserRole/Unchanged || ApprovalWorkflowStep/Unchanged
         || ApprovalWorkflowLevel/Unchanged]

BƯỚC 2 — ROOT CAUSE ĐO ĐƯỢC (không chép giả thuyết)

LỚP B (3 test) — nguyên nhân KHÔNG phải AsNoTracking

Giả thuyết lead: "vùng ghi changelog dùng db.PurchaseEvaluations.AsNoTracking() rồi đem cập nhật ⇒ affected 0". BÁC BỎ bằng đọc mã: :778-783 chỉ Select(p => new { p.Id, p.Phase }) — projection sang anonymous type, KHÔNG hề gán/ghi lại PE nào. Không có UPDATE nào phát cho PurchaseEvaluations. Và F16 (module Đề xuất) thậm chí KHÔNG vào nhánh changelog PE (:774-776 lọc DuyetNcc/DuyetNccPhuongAn) mà vẫn đỏ y hệt ⇒ thủ phạm nằm NGOÀI vùng changelog.

Root cause THẬT (khớp đúng dump AddedModified):

src/Backend/SolutionErp.Domain/Common/BaseEntity.cs:5

public Guid Id { get; set; } = Guid.NewGuid();   // ⇐ khoá LUÔN có giá trị ngay khi `new`

Handler thêm dòng Cấp mới bằng navigation-add trên entity cha ĐANG ĐƯỢC THEO DÕI (:730-747): step.Levels.Add(new ApprovalWorkflowLevel { ... }).

Khi DetectChanges phát hiện một entity LẠ trong collection của cha đã-tracked, EF quyết Added hay Modified bằng heuristic "khoá đã set ⇒ coi là đã tồn tại". Vì BaseEntity gán sẵn Id = Guid.NewGuid(), khoá KHÔNG BAO GIỜ là default ⇒ EF chọn Modified ⇒ phát UPDATE ApprovalWorkflowLevels SET ... WHERE Id = @p cho một dòng CHƯA TỒN TẠI ⇒ 0 row ⇒ DbUpdateConcurrencyException.

Bằng chứng đối chứng: POST tạo mới (CreateAwDefinitionCommandHandler) KHÔNG dính vì nó db.ApprovalWorkflows.Add(wf) — add từ GỐC ⇒ cả đồ thị bị ép Added, heuristic không chạy.

Fix: đăng ký dòng mới TƯỜNG MINH qua DbSet.Add (ép state Added, bỏ qua heuristic).

LỚP A (2 test) — ExecuteDelete không đụng change-tracker (đúng hướng lead) + 1 ca KHÁC

PurgeOrphanOpinionsAsync (:870-902) dùng ExecuteDeleteAsync — đi thẳng SQL, không cập nhật change-tracker. Bản sao PurchaseEvaluationLevelOpinion mà test đã Add+Save vẫn nằm trong tracker ở state Unchanged. Tới db.ApprovalWorkflowLevels.Remove(gone) (:755), EF chạy cascade-check TỨC THÌ (CascadeDeleteTiming.Immediate = mặc định), thấy dependent còn tracked + FK Restrict + non-nullable ⇒ ném InvalidOperationException NGAY TẠI Remove() (stack: InternalDbSet.RemoveStateManager.CascadeDeleteHandleConceptualNulls) — chưa kịp tới SaveChanges.

🔴 PHÂN BIỆT 2 CA (không được gộp):

  • F14 (phiếu CÒN SỐNG): handler PHẢI chặn 409. Đo được: guard :658-665 CHẠY ĐÚNG, mọi assert phía handler đều xanh. Chỗ nổ là dòng 580 của chính test (khối CHỨNG-NHÂN tự tay Remove để chứng minh FK là thật) — xem BƯỚC 5.
  • F14_F5a (phiếu ĐÃ xoá mềm): handler PHẢI cho qua + purge. Đây mới là lỗi production thật.

BƯỚC 3 — CÁC THAY ĐỔI

(1) ApprovalWorkflowV2AdminFeatures.cs — vá LỚP B (dòng Cấp mới bị đoán nhầm là "đã tồn tại")

Vùng toAdd (:730-759 sau sửa): tách new ApprovalWorkflowLevel {...} ra biến rồi đăng ký tường minh:

db.ApprovalWorkflowLevels.Add(newLevel);   // ép state Added, không qua heuristic khoá-đã-set
step.Levels.Add(newLevel);                 // giữ nav in-memory nhất quán cho phần đọc phía sau

Đúng khuôn đã có sẵn trong repo: PeWorkItemBudgetFeatures.cs:44 cũng db.X.Add(rec) tường minh.

(2) ApprovalWorkflowV2AdminFeatures.cs — vá LỚP A (ExecuteDelete bỏ quên change-tracker)

Thêm helper DetachTrackedOpinionsOn(IApplicationDbContext, HashSet<Guid>) (đặt cạnh PurgeOrphanOpinionsAsync) + gọi NGAY TRƯỚC vòng db.ApprovalWorkflowLevels.Remove(gone). Quét ((DbContext)db).ChangeTracker.Entries() theo tên cột ApprovalWorkflowLevelId ⇒ phủ đủ cả 7 bảng chữ ký (PE + HĐ + Proposal/Leave/Ot/Travel/Vehicle — đã kiểm: grep -rl ApprovalWorkflowLevelId src/.../Domain/ ra ĐÚNG 7 file, không dư entity nào khác) mà không phải liệt kê tay từng DbSet. Cast ((DbContext)db) là khuôn đã có trong repo (PeWorkItemBudgetFeatures.cs:52, PurchaseEvaluationFeatures.cs:125) ⇒ KHÔNG phải đổi IApplicationDbContext.

An toàn: tới điểm gọi, guard F-5 (:658-673) đã đọc DB bằng AsNoTracking và xác nhận các dòng đó có 0 chữ ký còn hiệu lực ⇒ bản sao nào còn tracked cũng là rác ⇒ detach = làm tracker khớp lại DB (không nuốt dữ liệu thật).

(3) UpdateAwDefinitionTests.cs — GỠ HẲN ZZ_DIAG_AddLevel

Xoá trọn [Fact] ZZ_DIAG_AddLevel (26 dòng giàn giáo, ném Exception để in tracker). KHÔNG đụng bất kỳ [Fact] nào khác.


BƯỚC 4 — ĐO SAU KHI VÁ (chạy riêng lớp test)

dotnet build SolutionErp.slnx0 Warning · 0 Error.

dotnet test --filter FullyQualifiedName~UpdateAwDefinitionTests:

Failed! - Failed: 1, Passed: 14, Skipped: 0, Total: 15
  • LỚP B: F12 · F1c · F16 → XANH (F12 xanh với assert NGUYÊN VẸN — kể cả nửa contrast ForbiddenException trước sửa, CurrentApprovalLevelOrder == 2 sau sửa, và chữ ký gắn đúng dòng Cấp mới của bob).
  • LỚP A: F14_F5a → XANH.
  • ZZ_DIAG → biến mất (15 test còn lại, trước là 16).
  • F14 → VẪN ĐỎ, xem BƯỚC 5.