# 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)