# sub-test-specialist-2 — W2 KHKK: test (đợt-2)

> Run: `2026-07-29-S161-khkk-w2-crud` · vai: test-specialist · index **2** (index 0/1 = 2 lane implementer đợt-1; KHÔNG đè).
> Luật: ghi TỪNG MỤC ngay sau khi làm xong mục đó (W1 5/7 + W2-đợt-1 2/2 chết #53 — file này là thứ duy nhất sống).
> Ranh giới: CHỈ ghi `tests/**`. Test đỏ vì bug production ⇒ DỪNG + báo, KHÔNG tự sửa.

---

## §0 — BASELINE ĐO TƯƠI (không tin số trong đề bài)

Lệnh: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1`
(HEAD lúc bắt đầu: nhánh `main`, W2-BE + W2-FE đã land trên đĩa.)

```
Passed!  - Failed: 0, Passed:  45, Skipped: 0, Total:  45  — SolutionErp.Domain.Tests.dll
Passed!  - Failed: 0, Passed: 521, Skipped: 0, Total: 521  — SolutionErp.Infrastructure.Tests.dll
```

**TRƯỚC = 45 + 521 = 566, 0 fail** — khớp con số đề bài giao (566 = 45D + 521I). Không có drift.

---

## §0.1 — ĐỌC ĐĨA TRƯỚC KHI VIẾT (test theo CODE, không theo prompt — luật S34)

| Thứ cần biết | Đo được trên đĩa | file:line |
|---|---|---|
| Rào (i) phase PE | `pe.Phase != DaDuyet` → `ConflictException` | `ContractSigningPlanFeatures.cs:312-313` |
| Rào (ii) winner | `winners.Count == 0` → `ConflictException` | `:317-318` |
| Rào (iii) 1-PE-1-KH sống | `AnyAsync(PurchaseEvaluationId == pe.Id && Phase != TuChoi)` → Conflict | `:323-328` |
| Rào (iv) workflow-type | `wfType is null` → **NotFound**; `!= 10` → **Conflict** | `:197-200` (helper `EnsureWorkflowTypeAsync`) |
| Phép tính tiền | `SUM(Quote.ThanhTien WHERE q.PurchaseEvaluationSupplierId == w.Id && q.IsSelected)` | `:366-368` |
| Mồi D4 | `ProposedAmount = peReference` | `:379` |
| D2 drafter | `DrafterUserId = actorId` (currentUser, KHÔNG `pe.DrafterUserId`) | `:353` |
| Mã phiếu | gen TRƯỚC `Add` (codegen tự-commit, bài S88) | `:339` |
| Delete allow-list | `{DangSoanThao, TuChoi}`, owner ∥ Admin, `Remove()` TRƯỚC `Add(changelog)` | `:1115-1126` |
| Codegen format | `KHKK/{yyyy}/{LastSeq:D3}`, bảng dùng chung `WorkflowAppCodeSequences` | `ContractSigningPlanCodeGenerator.cs:18-42` |
| FK cứng duy nhất | `ApprovalWorkflowId` → `ApprovalWorkflows` (Restrict) ⇒ **buộc seed workflow thật** | `ContractSigningPlanConfiguration.cs:39-42` |
| loose-Guid (KHÔNG FK) | `PurchaseEvaluationId / ProjectId / DepartmentId / DrafterUserId` | `ContractSigningPlanConfiguration.cs:30-33` |

**Hệ quả chọn fixture:** không handler nào của W2 cần `UserManager` ⇒ **không cần `IdentityFixture`**.
NHƯNG `TestApplicationDbContext` KHÔNG wire `AuditingInterceptor` ⇒ `Remove()` = xoá **CỨNG** (bài S155).
Đo "xoá xong tạo lại được" trên xoá-cứng là **đo nhầm cơ chế** (rào iii dựa vào *global query filter* che row soft-deleted, chứ không phải row biến mất).
⇒ dựng fixture riêng có interceptor (xem §2).

---
