wal: flush 20260727T1155
This commit is contained in:
@ -0,0 +1,130 @@
|
||||
# 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`
|
||||
```csharp
|
||||
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-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**:
|
||||
```csharp
|
||||
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
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user