# sub-test-specialist-2 — ĐỢT-1 lane TEST W1 (14 test T1-T14)

> ⚠️ TASK header ghi artifact = `sub-test-specialist-0.md`, nhưng khối RUN-TRACE (ISOLATION B6) ra lệnh
> **CHỈ ĐƯỢC GHI** `sub-test-specialist-2.md`. Ưu tiên luật isolation ⇒ ghi file NÀY, KHÔNG tạo `-0`.

- ts-mở: 2026-08-08 · lane: test-specialist (WRITE `tests/**` ONLY, 0 chạm `src/**`)
- SPEC: `spec-khkk-awf-mirror-ncc-08-08-2026.md` MỤC 2 W1 khối Test (:37-39)
- Homes: `tests/SolutionErp.Infrastructure.Tests/Application/UpdateAwDefinitionTests.cs` (T1-T3, T8-T14)
  · `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs` (T4-T7)
- 🔴 implementer-backend land 5 fix SONG SONG ⇒ test viết theo **HÀNH VI ĐÍCH**; ca đỏ-vì-BE-chưa-land = RED hợp lệ.

## §0 — Ghi liên tục (chống-#53). Mục CHƯA CHẠY chỉ có tiêu-đề + "CHƯA ĐO".

## §1 — Đọc nền (prod code sites) — ĐÃ ĐỌC

| Vế | Site trên đĩa (HEAD hôm nay) | Hành vi HÔM NAY | ⇒ test |
|---|---|---|---|
| W1a deactivate | `ApprovalWorkflowV2AdminFeatures.cs:339-343` `Where(w.ApplicableType == typeEnum && w.IsActive)` + comment `:339` "only ONE active per type" | tắt **MỌI** active cùng type ⇒ POST 1 nhóm = tắt cả 8 | T1 ĐỎ · T3 ĐỎ · T2 XANH (type-1 giữ per-type) |
| nextVersion | `:334-337` `Where(w.Code == request.Code).Max(Version)` | ĐÃ per-Code sẵn (bất đối xứng với `:340`) | nền của T1/T3 |
| Group-match Create | `ContractSigningPlanFeatures.cs:479-486` regex `^KHKK-N([1-8])$`, code ngoài họ CHO QUA (`:477-478` chủ đích) | CÓ ở Create | (đã có test cũ, xem §4) |
| Group-match UpdateDraft | `:603-607` — **chỉ** `EnsureWorkflowTypeAsync` | KHÔNG check nhóm | T4 ĐỎ |
| Group-match Submit | `ContractSigningPlanWorkflowService.cs:176-187` (rào hạng mục) → `:189 LoadStepsAsync` | KHÔNG check nhóm | T5 ĐỎ |
| Pin-check IsActive/Selectable | `:603-607` không đọc 2 cờ | pin bản archived OK | T7 ĐỎ |
| Changelog PUT | `:863-865` gate `ApplicableType is DuyetNcc or DuyetNccPhuongAn` | type-10 KHÔNG ghi vết | T8 ĐỎ · T9 (vế 0-row) XANH-mù ⇒ phải kèm control-dương |
| DELETE | `:1193-1205` `KeyNotFoundException` + `Remove` trần, 0 usage-check | KHKK/PE/Contract = **FK Restrict** ⇒ `DbUpdateException` 500 · Leave/Ot/… = **loose 0-FK** ⇒ **200 xoá êm** | T10-T14 ĐỎ |

Sự-thật nền dùng để dựng lưới:
- 8 module có cột `ApprovalWorkflowId`: Contract · ContractSigningPlan · LeaveRequest · OtRequest · Proposal · TravelRequest · VehicleBooking · PurchaseEvaluation (`grep --include=*.cs src/Backend/SolutionErp.Domain/`).
- `ContractSigningPlanConfiguration.cs:50-53` FK **Restrict** + `:55` `HasQueryFilter(!IsDeleted)` ⇒ T11 phân biệt được "guard đọc soft-deleted" (409) vs "guard mù" (DbUpdateException).
- `WorkflowAppsConfiguration.cs:12-26` LeaveRequest **KHÔNG khai FK** cho `ApprovalWorkflowId`/`LeaveTypeId` ⇒ T12 seed Guid tự do; và guard là RÀO DUY NHẤT (chứng-nhân trong test).
- `NotFoundException : Exception` (`Common/Exceptions/ApplicationException.cs:3`) — KHÔNG họ hàng `KeyNotFoundException` ⇒ T13 đỏ thật hôm nay.
- `ApprovalWorkflowConfiguration.cs:26` UNIQUE (Code, Version) ⇒ T14 seed được 2 version cùng Code.

## §2 — Baseline đo tươi

- `git status` lúc mở lane: **`src/` SẠCH** (implementer-backend chưa land) ⇒ số đo dưới là nền TRƯỚC cả 5 fix.
- Lệnh: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1` (chạy nền).
- Kết quả: xem §7 (điền khi job xong).

## §3 — T1-T3 (deactivate-per-Code, home UpdateAwDefinitionTests)

CHƯA ĐO

## §4 — T4-T7 (group-match guard + pin-check, home ContractSigningPlanApprovalTests)

CHƯA ĐO

## §5 — T8-T9 (changelog PUT type-10)

CHƯA ĐO

## §6 — T10-T14 (DELETE usage-check 409/404 + D5 last-version)

CHƯA ĐO

## §7 — Bảng kết {test → RED | GREEN | đợi-BE}

CHƯA ĐO

## §8 — Bug prod phát hiện / spec drift

CHƯA ĐO

## §9 — VÁ MAJOR-3 (reviewer pre-commit) — D6 ở đường TẠO · ĐÃ ĐO

- ts: 2026-08-08 (lượt resume riêng, sau khi reviewer-precommit ra finding). Scope: **đúng 1 `[Fact]`**, 0 chạm `src/**`, 0 sửa 14 ca cũ, 0 commit.
- **Lỗ:** `EnsureWorkflowPinnableAsync` cắm 2 site (Create `ContractSigningPlanFeatures.cs:507` · UpdateDraft `:642`) mà T7 (`ContractSigningPlanApprovalTests.cs:1402`) gọi `UpdateDraftAsync` ở CẢ 4 chân ⇒ đường TẠO 0 răng đo.
- **Nhà:** `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanGroupAndCatalogTests.cs`
  — 1 `[Fact]` `Create_PinQuyTrinhDaNgungHoacChuaGhim_ThiBiChan_VaKhongDotMa` (đặt cạnh ca rào (v) để dùng chung `SeedPeForCreateAsync` + `CreateAsync` + `ReadPlanScopesAsync`).
  — `SeedWorkflowAsync` +3 tham số **ĐUÔI** optional (`version` / `isActive` / `isUserSelectable`, default giữ hành vi cũ) ⇒ **15 call-site cũ 0 đổi chữ**; cùng khuôn `ContractSigningPlanApprovalTests.cs:135-140`.

**Thiết kế phép đo (vì sao 3 chân này phân biệt được 2 thế giới):**

| Chân | Seed (khác nhau ĐÚNG Version + 1 cờ) | Kỳ vọng | Bịt cái "xanh giả" nào |
|---|---|---|---|
| (1) | `KHKK-N1` v2 `IsActive=false` | 409 chứa "ngừng hoạt động" | — |
| (2) | `KHKK-N1` v3 `IsUserSelectable=false`, **`IsActive` VẪN true** | 409 chứa "ghim" **∧ message ≠ chân (1)** | 2 cờ cùng false ⇒ `!IsActive` nổ trước ⇒ chân (2) đo lại chân (1) = phép đo RỖNG |
| (3) | `KHKK-N1` v1 active+ghim | tạo được, mã **`KHKK/2026/001`** | rào chặn-tuốt (chân 3 đỏ) ⟂ rào bị gỡ khỏi Create (chân 1-2 xanh giả) |

- **Code cùng họ `KHKK-N1` + phiếu nhóm 1** ⇒ rào (v) `:515` KHỚP nên im lặng; mọi 409 quan sát được chỉ có thể từ D6. Ba message của các rào khác trong đường tạo đều khác chữ ("không thuộc loại…" / "đã có kế hoạch ký kết cho nhóm duyệt N…" / "Phiếu nhóm N{g} đang gắn quy trình N{k}") ⇒ 2 phép `Contain` định danh D6 duy nhất.
- **Ordering ĐO TRỰC TIẾP, không suy luận:** sau 2 lần 409, assert `WorkflowAppCodeSequences.Any(Prefix=="KHKK/2026") == false` — nếu ai dời D6 xuống SAU `codeGen.GenerateAsync` (`:524`) thì hàng sequence đã tồn tại với `LastSeq=2` và chân (3) ra `003`. Codegen **tự commit** transaction riêng (`ContractSigningPlanCodeGenerator.cs:23-43`) ⇒ số mất vĩnh viễn, không rollback theo request.
- **Không để lại phiếu nửa vời:** `ReadPlanScopesAsync` rỗng sau 2 lần 409 (D6 ném trước `db.ContractSigningPlans.Add` `:577`), rồi `BeEquivalentTo` đúng 1 `PlanScope("KHKK/2026/001", 1, dangDung)` sau chân (3).
- 🔴 **CỐ Ý KHÔNG pin chuỗi `wf.Code` trong message** dù prod có chèn (`:342`/`:345`): bất-biến mà spec tuyên bố là "**2 câu TÁCH**", pin thêm chữ Code là biến chi-tiết trình bày thành yêu cầu (bài S164 không-pin-config-lie). Vế `NotBe` giữa 2 message mới là thứ khoá đúng tuyên bố đó.

**Đo (lệnh chuẩn `-p:BuildInParallel=false -maxcpucount:1`):**
- Ca mới chạy CÔ LẬP (`--filter`): `Passed: 1, Failed: 0`.
- Full-suite `dotnet test SolutionErp.slnx`: **Domain 45 + Infra 615 = 660 PASS, 0 FAIL** (nền 659 → **Δ+1**, 0 hồi quy).
- `git status`: đổi DUY NHẤT `…/ContractSigningPlanGroupAndCatalogTests.cs` (+106/−7). `git diff --stat -- src/` = 3 file của lane implementer-backend (có TRƯỚC lượt này), **0 dòng của tôi**.

**Bug prod / spec drift lượt này:** KHÔNG có. Guard hành xử đúng như spec W1b2 mô tả ở cả 3 chân.
