7.5 KiB
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 Added → Modified):
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.Remove → StateManager.CascadeDelete →
HandleConceptualNulls) — 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-665CHẠ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ự tayRemoveđể 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.slnx → 0 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
ForbiddenExceptiontrước sửa,CurrentApprovalLevelOrder == 2sau 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.