# sub-test-specialist-2 — S161 W3 ĐỢT-2 TEST-BEFORE (6 test sàn, kỳ vọng ĐỎ)

> Ghi TỪNG MỤC trong lúc làm (bài #53: chỉ thứ trên đĩa mới sống).
> Scope MINE: `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs` (file MỚI).
> KHÔNG chạm: `src/Backend/**` · `fe-*/**` · slnx · commit/push · MEMORY.md.

⚠️ **Tên file sub-md:** lệnh lead (khối LUẬT) ghi `sub-test-specialist-2.md`; khối RUN-TRACE
boilerplate ghi `sub-test-specialist-0.md`. Chọn **-2** vì khớp đánh số của chính run-folder này
(implementer-backend-**0**, implementer-frontend-**1**) và tránh đè file (bài S133: 2 invocation
cùng role + cùng index ghi đè nhau, mất trọn nội dung). Khai ở đây để lead không phải đoán.

---

## §0 — ĐO TRƯỚC KHI GÕ (baseline TƯƠI, không tin số đề bài)

| Mục đo | Lệnh | Kết quả |
|---|---|---|
| Baseline | `dotnet test SolutionErp.slnx --nologo -v minimal -p:BuildInParallel=false -maxcpucount:1` | Domain **45/45** · Infra **529/529** · Failed **0** ⇒ **574** — KHỚP số bàn giao |
| Chữ ký service | `IContractSigningPlanWorkflowService.cs:25-29` + `ContractSigningPlanWorkflowService.cs:78-82` | `new ContractSigningPlanWorkflowService(db, currentUser, dateTime, notifications)` · `TransitionAsync(planId, action, comment, ct)` |
| Record trả về | `IContractSigningPlanWorkflowService.cs:38-41` | `ContractSigningPlanTransitionResult(int Phase, int? CurrentWorkflowStepIndex, int? CurrentApprovalLevelOrder)` — `Phase` là **int** (QUYẾT-1 lane BE) |
| Skeleton ném gì | `ContractSigningPlanWorkflowService.cs:108/113/117/121` | `NotImplementedException("W3 đợt-3: " + action)` cho cả 4 nhánh ⇒ ĐỎ THẬT |
| Inbox W2 (không chạm service) | `ContractSigningPlanFeatures.cs:725-752` + `:229-264` | `GetMyContractSigningPlanInboxQueryHandler(db, currentUser)`; non-admin qua `ResolveV2InboxIdsAsync` ⇒ **CHẠY ĐƯỢC HÔM NAY** |
| Con-trỏ là INDEX hay Order? | `ContractSigningPlanFeatures.cs:256-260` | `steps[idx]` sau `OrderBy(s => s.Order)` ⇒ `CurrentWorkflowStepIndex` = **INDEX 0-based**, KHÔNG phải `Step.Order`. Mã W2 đã chốt ⇒ test được phép ép Step.Order ≠ index (xem §2 T2) |
| FK bắt buộc row thật | `ApprovalWorkflowConfiguration.cs:72-75` (Level.ApproverUserId → Users **Restrict**) · `ContractSigningPlanConfiguration.cs:36-39` (Plan.ApprovalWorkflowId → ApprovalWorkflows **Restrict**) | ⇒ seed PHẢI có User thật + ApprovalWorkflow thật |
| loose-Guid (khỏi seed) | `ContractSigningPlanConfiguration.cs:10-12` + `ContractSigningPlan.cs:24-29` | `PurchaseEvaluationId / ProjectId / DepartmentId / DrafterUserId` KHÔNG FK ⇒ **không cần seed PE/Project/Department** (khác hẳn W2) |
| `MaKeHoach` UNIQUE | `ContractSigningPlanConfiguration.cs:28` | filtered unique ⇒ mỗi phiếu trong 1 fixture phải mã KHÁC nhau |
| Nguồn copy đợt-3 (để biết đo cái gì) | `ContractWorkflowService.cs:246` GroupBy · `:255-266` OR-of-N + **admin bypass** · `:289-316` UPSERT · `:355-393` advance/terminal | terminal set **CẢ HAI** con-trỏ = null (`:380-381`) ⇒ test pin null tại terminal |
| Stub notify có sẵn | `CreateContractCommandApplicableTypeTests.cs:95` `NoOpNotificationServiceApp` — **cùng namespace** `...Tests.Application` | dùng thẳng, KHÔNG đẻ stub thứ 2 |

**Kết luận §0:** seed W3 NHẸ hơn W2 nhiều (không PE, không Supplier, không quote) — chỉ
`Users` + `ApprovalWorkflow(+Steps+Levels)` + `ContractSigningPlan(+Lines)`.

---

## §1 — 2 quyết định hạ tầng của file test

**(1) Fixture RIÊNG `KhkkWorkflowFixture` có `AuditingInterceptor`** (mirror `ContractSigningPlanCrudTests.cs:49-82` W2).
Lý do KHÔNG dùng `IdentityFixture`: guard trình đợt-3 = `CreatedBy == actor ∨ DeptManager ∨ Admin`
⇒ `CreatedBy` của phiếu phải là NGƯỜI THẬT. `TestApplicationDbContext` trần KHÔNG wire interceptor
⇒ `CreatedBy` sẽ là `null` cho mọi phiếu ⇒ nhánh `CreatedBy == actor` KHÔNG BAO GIỜ đúng, test
`submit` sẽ đỏ vì lý do sai. Seed phiếu bằng context của chính người soạn ⇒ `CreatedBy` khớp
`DrafterUserId` (2 đường đọc, cùng 1 giá trị — đợt-3 chọn đường nào cũng đúng).

**(2) User seed THẲNG `db.Users.Add(...)`, KHÔNG qua `UserManager`.**
Service đợt-3 lấy role từ `ICurrentUser.Roles` (lane BE khai QUYẾT-2: cố ý KHÔNG inject
`UserManager`) ⇒ kéo cả Identity stack vào chỉ để có row FK là phí. Repo chưa có tiền lệ
`db.Users.Add` trong tests (grep 0 hit) ⇒ mục này là **cái mới**, và nó chỉ hợp lệ vì
`Level.ApproverUserId` cần *row tồn tại*, không cần *đăng nhập được*.

---

## §2 — 6 test: đo gì, bẫy cài ở đâu

(điền tiếp bên dưới trong lúc viết)
