wal: flush 20260801T1310
This commit is contained in:
@ -0,0 +1,25 @@
|
||||
CICD-K7: IN-PROGRESS (đang đo — verdict ở dòng cuối)
|
||||
|
||||
# sub-cicd-verify-k7 — deploy verify K7 (bridge KHKK→HĐ), S167 2026-08-01
|
||||
|
||||
> Artifact chống-#53: ghi TỪNG MỤC ngay khi có bằng chứng. Return chỉ là tóm tắt.
|
||||
> Baseline PRE đọc từ `sub-cicd-verify-k6.md` (post-#440): admin js `Ig1R5tAh` · eoffice js `DFOCQ2ks` · gate 622=45+577 · Mig 71 · sys.tables 97.
|
||||
|
||||
---
|
||||
|
||||
## M0 — Push + path-filter (ĐÃ ĐO)
|
||||
|
||||
- HEAD = `aaed699228f418fa31ecba07414709de465502fc`
|
||||
subject: `[CLAUDE] Contract: K7 SP-002 bridge KHKK->HD hoi-2-khoa + Line.ContractId + grant seeder (gate PASS-WITH-FLAGS 10, F-8 fixed)`
|
||||
committed: 2026-08-01 13:04:49 +0700
|
||||
- `git log origin/main..HEAD` = **RỖNG** ⇒ đã push thật.
|
||||
- Range `1c6d77a..aaed699` = 7 commit (6 × `wal: flush` + 1 code).
|
||||
- **File code trong range (13/13 khớp đề bài):**
|
||||
- BE (4): `Api/Controllers/ContractSigningPlansController.cs` · `Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` · `Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` · `Infrastructure/Persistence/DbInitializer.cs`
|
||||
- FE mirror ×2 app (6): `fe-{admin,user}/src/hooks/usePipelineStages.ts` · `.../pages/khkk/KhkkDetailPage.tsx` · `.../types/khkk.ts`
|
||||
- Test (3): `Api/AuthorizePolicyRegressionTests.cs` · `Api/KhkkBridgeAuthzTests.cs` · `Application/CreateContractFromSigningPlanTests.cs`
|
||||
- (còn lại 14 file đều `.claude/**` = paths-ignore, KHÔNG trigger)
|
||||
- **M3-phần-1 (git):** `git diff --name-only 1c6d77a..aaed699 -- '*Migrations*'` = **0 file** ⇒ 0-mig chứng-minh-được từ git.
|
||||
- Path-filter: có file `src/**` + `fe-*/**` ⇒ CI PHẢI chạy (không skip).
|
||||
|
||||
---
|
||||
@ -0,0 +1,469 @@
|
||||
GATE-K7: PASS-WITH-FLAGS 10
|
||||
|
||||
# Gate K7 — adversarial review cầu KHKK → HĐ (S167, 2026-08-01)
|
||||
|
||||
Vai: reviewer (READ-only). Base đo: HEAD = `f5ee146` (`wal: flush 20260801T1242`), diff UNCOMMITTED.
|
||||
Nguyên tắc: mọi mục dưới đây là phép đo TỰ CHẠY của reviewer, không chép claim của lane.
|
||||
|
||||
---
|
||||
|
||||
## §0 — Mốc cây + residual scope
|
||||
|
||||
Đo bằng `git status --porcelain` + `git diff --stat` (2026-08-01, đầu lượt review).
|
||||
|
||||
13 file thuộc phạm vi K7 — CÓ ĐỦ, đúng như spec giao:
|
||||
|
||||
| Nhóm | File | Trạng thái |
|
||||
|---|---|---|
|
||||
| BE | `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` | M (+33) |
|
||||
| BE | `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` | M (+23/-2) |
|
||||
| BE | `src/Backend/SolutionErp.Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` | **?? MỚI** (252 dòng) |
|
||||
| BE | `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` | M (+86) |
|
||||
| FE | `fe-{admin,user}/src/hooks/usePipelineStages.ts` | M (+61/-?) ×2 |
|
||||
| FE | `fe-{admin,user}/src/pages/khkk/KhkkDetailPage.tsx` | M (+308) ×2 |
|
||||
| FE | `fe-{admin,user}/src/types/khkk.ts` | M (+49) ×2 |
|
||||
| Test | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | M (+48/-?) |
|
||||
| Test | `tests/SolutionErp.Infrastructure.Tests/Api/KhkkBridgeAuthzTests.cs` | **?? MỚI** |
|
||||
| Test | `tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromSigningPlanTests.cs` | **?? MỚI** |
|
||||
|
||||
**FLAG RESIDUAL (F-R1, MINOR)** — 3 file dirty NGOÀI cả 13-file-set LẪN lead-set đã khai
|
||||
(`.claude/*` + `docs/STATUS.md`):
|
||||
|
||||
- `scripts/distill-shard-probe.ps1` (+1/-1)
|
||||
- `scripts/measure-agent-memory.ps1` (+1/-1)
|
||||
- `scripts/memory-archive-gate.ps1` (+11/-2)
|
||||
|
||||
Đọc diff: cả 3 là việc **governance memory-index rotation `_INDEX-<period>.md` @S167**, KHÔNG
|
||||
liên quan K7. Không phải lỗi nội dung; rủi ro là **`git add -A` sẽ nuốt chúng vào commit K7**.
|
||||
Đề nghị: hoặc khai chúng vào lead-set, hoặc commit tách. Untracked trong `src`/`tests` = đúng
|
||||
3 file MỚI đã khai, **0 file lạ**.
|
||||
|
||||
---
|
||||
|
||||
## §1 — TRỌNG TÂM 1: hội-2-khoá 2 tầng (BE ∧ FE)
|
||||
|
||||
### 1a. Tầng server — ĐO ĐƯỢC, ĐẠT
|
||||
|
||||
- Class-level: `ContractSigningPlansController.cs:29` → `[Authorize(Policy = "KeHoachKyKet.Read")]`.
|
||||
- Action mới: `ContractSigningPlansController.cs:227-228` →
|
||||
`[HttpPost("{id:guid}/create-contract")]` + `[Authorize(Policy = "Contracts.Create")]`.
|
||||
- ASP.NET Core cộng dồn class + action theo AND ⇒ hội 2 khoá là THẬT, không phải claim.
|
||||
- Policy `Contracts.Create` có tồn tại: `MenuKeys.cs:12` khai const `Contracts`, và
|
||||
`MenuKeys.cs:228` đưa `Contracts` VÀO mảng `All` ⇒ `Program.cs` sinh đủ 4 policy.
|
||||
(Nếu key ngoài `All` thì runtime 500 chứ không 403 — đã kiểm, không rơi vào ca đó.)
|
||||
|
||||
### 1b. Tầng FE — ĐỌC DIFF TỪNG DÒNG (lead ghi §3-§4 on-behalf; đây là lưới THẬT đầu tiên)
|
||||
|
||||
`fe-user/src/pages/khkk/KhkkDetailPage.tsx` (bản admin byte-identical, xem §3c):
|
||||
|
||||
- `:138-140` — `const { can } = usePermission()`; `canReadPlan = can('KeHoachKyKet','Read')`;
|
||||
`canCreateContract = can('Contracts','Create')`. Đúng 2 key của server, KHÔNG OR `Khkk_*`
|
||||
(gotcha #85 giữ được).
|
||||
- `:299` — `showBridge = phase === KhkkPhase.DaDuyet && canReadPlan`.
|
||||
- `:303-312` — `bridgeBlockReason`: nhánh ĐẦU TIÊN là `!canCreateContract` ⇒ nút render nhưng
|
||||
`disabled`.
|
||||
- `:352-363` — `<Button disabled={bridgeBlockReason != null} onClick={() => setBridgeOpen(true)}>`.
|
||||
|
||||
**Truy đường lách:** grep toàn file cho `setBridgeOpen(true)` → xuất hiện ĐÚNG 1 lần, tại
|
||||
`onClick` của nút đã disabled. Dialog `CreateContractFromKhkkDialog` render có điều kiện
|
||||
`{bridgeOpen && ...}` (`:781`) ⇒ **không có đường nào tới `create.mutate()` khi thiếu
|
||||
`Contracts.Create`**. React không bắn `onClick` trên `<button disabled>`.
|
||||
⇒ FE **KHÔNG lỏng hơn server**. ĐẠT.
|
||||
|
||||
**Verify hook đọc gì:** `fe-user/src/hooks/usePermission.ts` — `findNode(menu, key)` đệ quy trên
|
||||
cây THÔ từ `AuthContext`, trả `false` khi không thấy node; `Create` → `node.canCreate`. Nghĩa là
|
||||
gate FE phụ thuộc **row Permission của key `Contracts`**, đúng thứ seeder mới cấp.
|
||||
|
||||
**CLARIFY-1 (không phải lỗi mã, là điều kiện nghiệm thu):** cây menu FE nạp lúc login và cache
|
||||
`localStorage` (`AuthContext.loadMenu`). Comment seeder khai "grant ăn @restart, KHÔNG phải
|
||||
deploy FE" — đúng cho BE, nhưng **nút vẫn xám cho tới khi user logout/login lại** vì `can()` đọc
|
||||
menu cache cũ. UAT phải nói rõ bước đăng-xuất/đăng-nhập, nếu không sẽ bị đọc thành "seeder không
|
||||
ăn".
|
||||
|
||||
---
|
||||
|
||||
## §2 — TRỌNG TÂM 2: seeder grant, gotcha #84 nghiêm
|
||||
|
||||
`DbInitializer.cs:2738-2814` = `SeedContractCreateAccessAsync`; call-site `:2361`.
|
||||
|
||||
### 2c. THỨ TỰ (vế nặng nhất) — ĐO BẰNG SỐ DÒNG THẬT, ĐẠT
|
||||
|
||||
`awk` liệt kê mọi `await Seed*/Revoke*` trong khối 2250-2420:
|
||||
|
||||
```
|
||||
2331: await SeedAllRolesReviewReadPermissionsAsync(...)
|
||||
2341: await RevokeTemporarilyHiddenModulesAsync(...) ← revoker
|
||||
2347: await SeedProcurementMasterAccessAsync(...)
|
||||
2354: await SeedKeHoachKyKetAccessAsync(...)
|
||||
2361: await SeedContractCreateAccessAsync(...) ← MỚI, SAU revoker ✔
|
||||
```
|
||||
|
||||
Và trong cửa sổ 2361→2520 **không còn call nào khác** ⇒ seeder mới là seeder-quyền CHẠY CUỐI,
|
||||
không ai lật lại nó. ĐẠT.
|
||||
|
||||
Đọc thêm thân revoker `:2550-2589` để không chỉ tin thứ tự: filter là
|
||||
`MenuKey.StartsWith("Hrm") || StartsWith("Off") || == Personal` — **nhánh `Contracts`/`Ct_*` đã
|
||||
bị GỠ từ S159**. Vậy claim "revoker hiện không còn nhánh Contracts" là ĐÚNG, và việc giữ thứ tự
|
||||
sau revoker là phòng-thủ dư (đúng chiều, không hại).
|
||||
|
||||
### 2b. UPGRADE-ONLY + IDEMPOTENT — ĐẠT
|
||||
|
||||
- Nhánh row-đã-có (`:2790-2797`): chỉ `if (!row.CanRead) row.CanRead = true` và
|
||||
`if (!row.CanCreate) row.CanCreate = true`. **Không chạm `CanUpdate`/`CanDelete`, không hạ cờ
|
||||
nào.** Không có `= false` ở bất kỳ đâu trong hàm.
|
||||
- Nhánh row-chưa-có (`:2800-2808`): tạo `CanRead=true, CanCreate=true, CanUpdate=false,
|
||||
CanDelete=false`.
|
||||
- Idempotent: lượt 2 trở đi `added=0, upgraded=0` ⇒ **không gọi `SaveChangesAsync`** (`:2811`),
|
||||
không log rác. ĐẠT.
|
||||
- Guard role-chưa-seed (`:2779-2784`): `roles.Count == 0` → log warning + `return`, không ném.
|
||||
|
||||
### 2a/2d. TIỀN ĐỀ "chưa role nào có Contracts.Create" — ĐO ĐƯỢC TỪ MÃ
|
||||
|
||||
`DbInitializer.cs:2474` `reviewKeys = MenuKeys.All.Where(InReviewScope)…` ⇒ `Contracts` nằm
|
||||
trong tập. `IsPeKey("Contracts") == false` ⇒ nhánh insert dùng `CanCreate = isPe` (`:2523`) =
|
||||
**false**, và nhánh row-đã-có là **skip-existing** cho key non-Pe (`:2515`) ⇒ không ai nâng
|
||||
`CanCreate` lên. Tiền đề của lane ĐÚNG.
|
||||
|
||||
Bộ vai: `roleNames = { Drafter, Procurement, Admin }` (`:2775`) — đúng 3 vai spec nêu, kèm comment
|
||||
"câu III default, chờ anh ratify" và chỉ-dẫn sửa 1 chỗ. ĐẠT.
|
||||
|
||||
**MINOR F-2** — Admin thừa: `MenuPermissionHandler.cs:26-31` có Admin-bypass (`Succeed` ngay khi
|
||||
role chứa `Admin`) ⇒ cấp row cho Admin **vô hại nhưng vô tác dụng**. Không đề nghị sửa; nêu để
|
||||
người sau đừng đọc row Admin thành "bằng chứng Admin cần row".
|
||||
|
||||
**MINOR F-3** — comment `:2814` viết "Thu hồi khi cần: xoá call ở SeedAsync", nhưng call thật
|
||||
nằm trong `SeedAdminPermissionsAsync` (thân 2288-2381), không phải `SeedAsync`. Chỉ-dẫn trỏ sai
|
||||
tên hàm.
|
||||
|
||||
**MINOR F-4 (mở rộng ngoài Create)** — hàm nâng cả `CanRead`, trong khi tên hàm + comment chỉ hứa
|
||||
`Contracts.Create`. Với 3 vai này `CanRead` gần như đã true từ S159 nên thực tế không đổi gì;
|
||||
nhưng `reviewKeys` có ghi rõ "trên prod row false KHÔNG được nâng ở đây" ⇒ nếu prod có role nào
|
||||
đang `Contracts.CanRead=false` thì hàm này **âm thầm mở lại quyền XEM**, rộng hơn nhãn.
|
||||
|
||||
---
|
||||
|
||||
## §3 — TRỌNG TÂM 3: chuỗi dữ liệu cây, khớp từng khớp nối
|
||||
|
||||
### 3a. Write-site → DTO (BE)
|
||||
|
||||
- Write-site: `CreateContractFromSigningPlanFeatures.cs:238` → `foreach (var l in lines) l.ContractId = contract.Id;`
|
||||
Đây là gán THẬT trên entity đã tracked (`plan.Lines` nạp qua `Include` `:93`), nằm TRƯỚC
|
||||
`SaveChangesAsync` `:248`. ĐẠT.
|
||||
- Fallback: `:246` `if (pe is not null) pe.ContractId ??= contract.Id;` — `??=` đúng chiều
|
||||
(HĐ đầu thắng, không cướp ô của cầu PE cũ).
|
||||
- DTO list: `ContractSigningPlanFeatures.cs:75` `List<Guid> ContractIds` (append CUỐI record ⇒
|
||||
positional-arg của các call-site cũ không lệch).
|
||||
- Projection: `ContractSigningPlanFeatures.cs:836-842` — subquery
|
||||
`Where(l => l.ContractSigningPlanId == p.Id && l.ContractId != null).OrderBy(CreatedAt)
|
||||
.Select(l => l.ContractId!.Value).ToList()`. Filter `!= null` đứng TRƯỚC `.Value` ⇒ không NRE,
|
||||
và line chưa bridge thì mảng RỖNG (không phải null-element). ĐẠT.
|
||||
|
||||
### 3b. DTO → FE (tên + null-safety)
|
||||
|
||||
- `types/khkk.ts:200` → `contractIds?: string[] | null`. JSON camelCase khớp `ContractIds`.
|
||||
Khai optional dù BE non-null: đúng, vì cache React Query cũ / API chưa deploy không có field.
|
||||
- Helper `types/khkk.ts:304-313` `khkkPlanContractIds(plan)`:
|
||||
`for (const id of plan.contractIds ?? []) if (id) out.push(id)` +
|
||||
`for (const line of plan.lines ?? []) if (line.contractId) out.push(line.contractId)`.
|
||||
**Ca "line không chọn ⇒ ContractId null ⇒ mảng rỗng, không crash" = ĐẠT** — cả 2 vòng đều
|
||||
`?? []` và đều lọc falsy trước khi push.
|
||||
- Chữ ký `plan: KhkkListItemDto & Partial<Pick<KhkkDetailDto,'lines'>>` ⇒ truyền
|
||||
`KhkkListItemDto` thuần vẫn hợp lệ (lines optional). ĐẠT.
|
||||
- Union + dedupe: `usePipelineStages.ts:295-300` — `contractIdsOfPackage` là `Set<string>`, đổ
|
||||
nguồn (1) `p.contractId` rồi nguồn (2) `khkkPlanContractIds(k)`. Chồng nhau ở HĐ đầu là CỐ Ý và
|
||||
`Set` khử. ĐẠT.
|
||||
- Dòng phụ: `usePipelineStages.ts:321-326` — `fallbackName` nay tra CẢ 2 nguồn
|
||||
(`peItems.find(...)?.tenGoiThau ?? khkkItems.find(k => khkkPlanContractIds(k).includes(id))?.peTenGoiThau ?? null`).
|
||||
Đo field tồn tại: `types/khkk.ts:171 peTenGoiThau: string | null`, BE `…Features.cs:39
|
||||
string? PeTenGoiThau`. Khớp. ĐẠT.
|
||||
|
||||
### 3c. Mirror 2 app — TỰ ĐO SHA, KHÔNG TIN CLAIM
|
||||
|
||||
```
|
||||
IDENTICAL src/types/khkk.ts 60ec8fa3a5aea94456e2180b1b80f63c1994257c55a424f4e1f7adf376cbbc07
|
||||
IDENTICAL src/hooks/usePipelineStages.ts 76e1823345d4e78b87e802bc10d22ea0a42307bbb426b8787eb2d25b02e8c52f
|
||||
IDENTICAL src/pages/khkk/KhkkDetailPage.tsx cf0a2272061f4bd8cc19c54b1a4992fdac331e9093665ec4ce708e119ef38123
|
||||
```
|
||||
|
||||
3/3 byte-identical — claim của lane TÁI LẬP ĐƯỢC.
|
||||
|
||||
**Rủi ro riêng của mirror byte-identical (đã kiểm, KHÔNG thủng):** file dùng đường dẫn cứng
|
||||
`/contracts/${id}` ở 3 chỗ (`:527` neo "✓ HĐ", `:975` neo "Đã có HĐ", `:855` `navigate`). Vì 2
|
||||
app có bảng route KHÁC nhau, đây là chỗ dễ 404 một bên. Đo:
|
||||
`fe-user/src/App.tsx:65` và `fe-admin/src/App.tsx:84` đều có `<Route path="/contracts/:id">`.
|
||||
⇒ ĐẠT ở cả 2 app.
|
||||
|
||||
**MINOR F-5** — 2 neo dùng `<a href>` thay `<Link>` ⇒ full reload (mất SPA state); và neo
|
||||
"Đã có HĐ" (`:975`) nằm LỒNG trong `<label>` (nested-interactive, cùng lớp a11y với finding K5 về
|
||||
`<button>` trong `<summary>`). Radio của dòng đó `disabled` nên không sinh double-activate, nhưng
|
||||
khuôn thì vẫn là interactive-lồng-interactive.
|
||||
|
||||
**MINOR F-6 (hiệu năng, không sai)** — `usePipelineStages.ts:324` gọi `khkkPlanContractIds(k)`
|
||||
BÊN TRONG `find` mà `find` lại nằm trong vòng lặp theo `contractIdsOfPackage` ⇒ O(n·m) và cấp phát
|
||||
mảng mới mỗi lượt. Quy mô hiện tại (list kẹp 200) vô hại.
|
||||
|
||||
### 3d. Chuông DEV — đọc kỹ vì dễ thành chuông-câm hoặc chuông-loạn
|
||||
|
||||
`usePipelineStages.ts:303-315`: chỉ chạy khi `import.meta.env.DEV`, có cờ module-level
|
||||
`warnedMissingLineContractIds` (`:62`) chặn lặp; điều kiện
|
||||
`approved.length > 0 && approved.every(k => k.contractIds == null)`.
|
||||
`== null` bắt cả `null` lẫn `undefined` — ĐÚNG chủ đích "vắng-field ⟂ mảng-rỗng", vì sau khi BE
|
||||
deploy field luôn là `[]` (non-null) ⇒ chuông tự im. Logic ĐẠT.
|
||||
Lưu ý (không phải lỗi): prod strip `DEV` ⇒ đây **không phải lưới an toàn**, chỉ là đèn báo lúc dev.
|
||||
|
||||
---
|
||||
|
||||
## §4 — TRỌNG TÂM 4: guard 4-vế + idempotency
|
||||
|
||||
Đọc `CreateContractFromSigningPlanFeatures.cs` toàn file (252 dòng).
|
||||
|
||||
Rào theo thứ tự thực thi:
|
||||
|
||||
| # | Dòng | Nội dung | Nhận xét |
|
||||
|---|---|---|---|
|
||||
| 0 | :87-89 | `IsAuthenticated` + `UserId` | ném `UnauthorizedException` |
|
||||
| 1 | :92-99 | phiếu tồn tại + `Phase == DaDuyet` | ĐẠT |
|
||||
| 2 | :101-109 | dòng ∈ phiếu (đếm sau `Distinct`) | id lạ/của phiếu khác đều rơi vào đây — ĐẠT |
|
||||
| 3 | :111-115 | 1 HĐ = 1 NCC | ĐẠT |
|
||||
| 4 | :125-127 | `ApprovedAmount is null` → 409 | KHÔNG fallback `ProposedAmount` — ĐÚNG |
|
||||
| 5 | :128-129 | `ContractId is not null` → "đã có Hợp đồng" | chống bridge-lặp |
|
||||
| 6 | :134-148 | workflow pin **4 vế** | xem dưới |
|
||||
|
||||
**vá-3 (4 vế workflow) — ĐẠT, và ĐÚNG "throw tách":**
|
||||
`:136` NotFound nếu không tồn tại · `:137-140` sai `ApplicableType` · `:143-145` `!IsActive` ném
|
||||
riêng · `:146-148` `!IsUserSelectable` ném riêng. Bốn thông điệp bốn nguyên nhân — đúng yêu cầu
|
||||
spec, và **rộng hơn khuôn tiền lệ** `ContractFeatures.cs:79-87` (khuôn cũ CHỈ có 2 vế đầu — tôi đã
|
||||
đọc lại `ContractFeatures.cs:74-88` để xác nhận, claim của lane đúng).
|
||||
|
||||
**Idempotency / double-click — ĐÂY LÀ FINDING NẶNG NHẤT.** Rào (5) là kiểm tra
|
||||
**đọc-rồi-ghi trong bộ nhớ**, không có khoá nào ở tầng CSDL:
|
||||
|
||||
- không `unique index` trên `ContractSigningPlanLines.ContractId`;
|
||||
- không `RowVersion`/optimistic concurrency trên `Line`;
|
||||
- không transaction bao ngoài (`SaveChangesAsync` trần `:248`; transaction duy nhất trong luồng
|
||||
là transaction NỘI BỘ của `ContractCodeGenerator`, đã commit xong trước đó).
|
||||
|
||||
⇒ Hai request song song (double-click thật, hoặc 2 tab) đều đọc `l.ContractId == null` ở
|
||||
`:128`, cùng đi tiếp, **tạo 2 Contract**, và lượt ghi sau đè `Line.ContractId`. Kết quả: 1 HĐ mồ
|
||||
côi (đã tiêu 1 số sequence, đã có changelog) mà không dòng nào trỏ tới. Đây là **TOCTOU thật**,
|
||||
không phải lo xa: nút "Tạo hợp đồng" chỉ chặn bằng `disabled={create.isPending}` (`:892`) — chặn
|
||||
được double-click trong CÙNG tab, **không** chặn 2 tab / retry mạng.
|
||||
|
||||
Xếp hạng: **MAJOR F-1**, không phải blocker của gate (dữ liệu không mất, HĐ mồ côi xoá được, và
|
||||
xác suất thấp vì thao tác có dialog), nhưng phải được KHAI, vì lane trình mục này như "đã đóng".
|
||||
Acceptance đề nghị (không phải chỉ-dẫn sửa mã): một phép kiểm chứng minh 2 lệnh gọi đồng thời chỉ
|
||||
sinh 1 HĐ — hoặc bằng ràng buộc CSDL, hoặc bằng cập-nhật-có-điều-kiện, hoặc khai công khai
|
||||
"chấp nhận rủi ro, chặn ở UI".
|
||||
|
||||
---
|
||||
|
||||
## §5 — TRỌNG TÂM 5: vá-6
|
||||
|
||||
- **Project/Supplier NotFound guard:** `:153-154` (`Project`) và `:156-157` (`Supplier`), cả 2 ném
|
||||
`NotFoundException` trước khi dùng `.Code`. ĐẠT — loose-Guid không có FK đỡ lưng nên guard này
|
||||
là bắt buộc.
|
||||
- **Codegen TRƯỚC `Add`:** `:212` `contract.MaHopDong = await codeGenerator.GenerateAsync(contract,
|
||||
project.Code, supplier.Code, ct);` rồi `:214` `db.Contracts.Add(contract);`. Đúng thứ tự bài S88
|
||||
(codegen tự-commit ⇒ không được để HĐ dở trong context). Chữ ký khớp
|
||||
`IContractWorkflowService.cs:29 Task<string> GenerateAsync(Contract, string, string, CancellationToken)`.
|
||||
ĐẠT.
|
||||
- **SlaDeadline:** `:205-206` `DateTime.UtcNow.Add(workflow.GetPhaseSla(DangSoanThao) ?? TimeSpan.FromDays(7))`
|
||||
— trùng từng chữ với 2 tiền lệ (`ContractFeatures.cs:110`, bridge PE `:109-110`). ĐẠT.
|
||||
- **`WorkflowDefinitionId` pin:** `:163-166` truy `WorkflowDefinitions` active theo
|
||||
`request.ContractType` → `:201` gán. Ngang bridge PE cũ (đã đọc lại
|
||||
`CreateContractFromEvaluationFeatures.cs:67-71` + field-map `:105-113` để đối chiếu). Set CẢ
|
||||
V1 + V2 (`:202` `ApprovalWorkflowId`) là ĐÚNG — service dispatch V2 trước. ĐẠT.
|
||||
|
||||
**KHAI-1 của test-lane (invest T1 ghi null nhưng vá-6 thắng): XÁC NHẬN ĐÚNG — ĐÓNG, không mở lại.**
|
||||
Lý do đo được: vá-6 trong spec ②:16 yêu cầu pin `WorkflowDefinitionId` theo 2 tiền lệ; mã hiện
|
||||
tại làm đúng thế; "để null" là hành vi mà chính spec loại bỏ. Không có mâu thuẫn còn lại.
|
||||
|
||||
**MINOR F-7 (field-map thiếu so với tiền lệ, có thể CỐ Ý):** bridge PE cũ carry
|
||||
`DraftData = pe.PaymentTerms` (`CreateContractFromEvaluationFeatures.cs:104`); cầu K7 **không**
|
||||
carry `DraftData`, dù đã nạp `pe` sẵn ở `:170`. Cũng không có `BypassProcurementAndCCM` (cái này
|
||||
hợp lý vì command K7 không nhận tham số đó). Nếu bỏ `DraftData` là chủ đích thì nên có một dòng
|
||||
comment, vì mọi khoản lệch khác so với tiền lệ đều đã được chú thích.
|
||||
|
||||
---
|
||||
|
||||
## §6 — TRỌNG TÂM 6+7 (test có răng · comment sạch số-đếm)
|
||||
|
||||
### 6b. Comment mang số-đếm / mỏ-neo — CÓ LỖI, ĐO ĐƯỢC BẰNG 1 LỆNH
|
||||
|
||||
**MAJOR F-8 — mỏ-neo TỰ VÔ HIỆU ngay lúc land (đúng lớp S166-K4b, tái phát):**
|
||||
2 comment MỚI trỏ `DbInitializer.cs:2516` cho `CanCreate = isPe`:
|
||||
- `ContractSigningPlansController.cs:224` ("seeder `DbInitializer.cs:2516`")
|
||||
- `DbInitializer.cs:2743` ("`SeedAllRolesReviewReadPermissionsAsync:2516`")
|
||||
|
||||
Phép đo (2 lệnh, cho 2 thế giới):
|
||||
|
||||
```
|
||||
git show HEAD:…/DbInitializer.cs | grep -n "CanCreate = isPe" → 2516 (TRƯỚC diff)
|
||||
grep -n "CanCreate = isPe" …/DbInitializer.cs → 2523 (SAU diff)
|
||||
```
|
||||
|
||||
Chính diff này chèn 7 dòng ở `:2353-2359`, đẩy dòng đích xuống 2523. Nghĩa là con trỏ ĐÚNG lúc
|
||||
viết và **SAI ngay khoảnh khắc commit** — không ai làm gì sai sau đó cả. Đây là lỗi loại "đo
|
||||
trước khi chèn". Acceptance: hoặc cập nhật thành 2523, hoặc bỏ số dòng và trỏ bằng TÊN
|
||||
(`SeedAllRolesReviewReadPermissionsAsync` + token `CanCreate = isPe` là duy nhất, grep ra ngay).
|
||||
|
||||
**MINOR F-9 — số-đếm trong comment tự mâu thuẫn:** `DbInitializer.cs:2743-2744` viết "nút bridge
|
||||
sẽ 403 với **13/13 role** (kể cả Drafter), chỉ Admin qua được nhờ bypass". Đếm thật:
|
||||
`AppRoles.cs:23-28` liệt **13** vai và Admin nằm trong đó; `MenuPermissionHandler.cs:26-31` cho
|
||||
Admin bypass ⇒ con số đúng phải là **12/13**. Một câu chứa cả con số lẫn phản-ví-dụ của chính nó.
|
||||
|
||||
Ngoài 2 mục trên, đã quét các con số khác trong comment MỚI và chúng kiểm được:
|
||||
`ContractConfiguration TenHopDong = 500` khớp `MaximumLength(500)` (`:73`) và clamp `:184`;
|
||||
`ContractTypeLabel` 1..7 (`fe-user/src/types/forms.ts:13-21`) khớp ĐÚNG
|
||||
`ContractType.cs:5-11` 1..7 ⇒ `IsInEnum()` không bao giờ đá dropdown.
|
||||
|
||||
### 6a. Ba file test — ĐỌC HẾT, CÓ RĂNG
|
||||
|
||||
**Đếm:** `KhkkBridgeAuthzTests` 5 `[Fact]` + `CreateContractFromSigningPlanTests` 12 `[Fact]`
|
||||
= **17** — khớp đúng con số "17 test mới" spec nêu (đếm bằng `grep -c '^\s*\[Fact\]'`, không chép).
|
||||
|
||||
**`KhkkBridgeAuthzTests.cs` — răng thật, và đúng loại răng:**
|
||||
- `:33-48` `BridgePolicies()` **đọc 2 tên policy TỪ CHÍNH CONTROLLER** bằng reflection, không
|
||||
hardcode ⇒ ai sửa attribute thì test đo theo cái mới mà vẫn hỏi đúng câu. Kèm
|
||||
`actionPolicy.Should().NotBe(classPolicy)` (`:45`) — chặn ca "2 vế trùng nhau thì hội-2-khoá
|
||||
còn 1 khoá mà nhìn vẫn như cũ".
|
||||
- Chạy **máy quyết định THẬT** `MenuPermissionHandler` (`:57-68`), không mô phỏng.
|
||||
- (2) `:170-171` và (3) `:195-196` mỗi test đều assert **vế còn lại VẪN QUA** trước khi assert vế
|
||||
thiếu bị chặn ⇒ không thể xanh nhờ "chặn tất cả". Đây đúng là thứ tôi định đòi.
|
||||
- (5) `:235-243` phép **2 thế giới 1 biến**: cùng bộ quyền, chỉ lật `IsActive`, kết quả phải đảo.
|
||||
- (4) `:214` assert `Permissions.Count == 0` trước khi kết luận Admin qua bằng bypass.
|
||||
|
||||
**`CreateContractFromSigningPlanTests.cs` — 12 ca, assert phân biệt được thế giới hỏng:**
|
||||
- T1 `:243-245` — 3 con số CỐ Ý khác nhau (750 chốt / 999 đề xuất / 900 NS) ⇒ lấy nhầm nguồn nào
|
||||
cũng ra số khác. T1 `:266` soi **RUỘT mã HĐ** `{ProjectCode}/HĐTP/SOL&{SupplierCode}/01` —
|
||||
chính là bằng chứng vá-6 đã nạp 2 bảng Project/Supplier, mạnh hơn `NotBeNull`.
|
||||
- T1 `:255-257` — assert pin **đúng bản V1 active của đúng loại**, và seed sẵn 1 bản NHIỄU loại
|
||||
khác (`:197-201`) để `!= null` không đủ để xanh. Đây là cách đúng để đóng KHAI-1.
|
||||
- T3 `:360` + T7 `:491` + T11 `:661-662` — assert **`ContractCodeSequences` không cháy**, tức đo
|
||||
được thứ tự "rào đứng TRƯỚC codegen" bằng side-effect, không bằng đọc code.
|
||||
- T6 `:436-440` và T7 `:496-498` — **đối chứng dương cùng lượt**, đổi đúng 1 biến thì QUA.
|
||||
- T10 `:625-627` — đo **CHIỀU của `??=`**: viết `=` thì mọi assert khác vẫn xanh, chỉ vế này đỏ.
|
||||
- T8 `:549-551` + `:555-562` — mối nối cầu → đường ống V2, đo bằng **hộp thư của đúng người**
|
||||
(Cấp 1 thấy, Cấp 2 chưa thấy), không phải đọc cột.
|
||||
|
||||
**T12 vs vá-7 — ĐÚNG, đủ 3 vế spec đòi:** (a) phiếu TỰ đi tới `DaDuyet` qua service THẬT
|
||||
(`:724-730`), không seed thẳng; (b) `ApprovedAmount` đông cứng `{250, 111}` với tương phản chứng
|
||||
minh `??=` (dòng chốt tay giữ 111, **không bị 999 đè**) `:735-736`, và `GiaTri = 361` với lập luận
|
||||
"Σ Proposed = 1249 ⇒ mọi cách sai đều ra số khác" `:744-745`; (c) sau bắc cầu, bấm duyệt lại →
|
||||
`Conflict`, rồi đọc lại **cả `ContractId` lẫn `ApprovedAmount` đều không đổi** `:753-758`. ĐẠT.
|
||||
|
||||
**T11 (double-click) — CÓ RĂNG NHƯNG CHỈ CHO NỬA TUẦN TỰ.** Assert thật, 4 vế: Conflict + đúng
|
||||
thông điệp + `Contracts.Count == 1` + `LastSeq == 1`. Nhưng `:652-655` là `await` lần 1 **rồi mới**
|
||||
lần 2 (`ChangeTracker.Clear()` mô phỏng request thứ 2). Test này **không đo được và không thể đo**
|
||||
ca ĐỒNG THỜI — tức F-1 vẫn hở. Bản thân comment test khai đúng phạm vi ("bấm 2 lần liên tiếp"),
|
||||
nên đây KHÔNG phải lane nói quá; rủi ro là **người đọc sau tưởng "idempotency đã đóng"**.
|
||||
|
||||
**Ngoại lệ `CreateContract` trong `AuthorizePolicyRegressionTests` — SIẾT, không mở lỗ:**
|
||||
- `:370-373` là `Dictionary<string,string>` map **1 action → 1 chuỗi policy CHÍNH XÁC**, dùng
|
||||
`authz.Policy.Should().Be(expectedPolicy)` (`:419`) — so khớp tuyệt đối, **không** `StartsWith`
|
||||
/ `Contains` / danh sách bỏ-qua.
|
||||
- Action nào không có tên trong map vẫn rơi vào `else` đòi prefix `KeHoachKyKet` (`:428`) ⇒
|
||||
**action tương lai KHÔNG được miễn trừ tự động**. Đúng yêu cầu "chỉ miễn đúng 1 action".
|
||||
- `MenuKeys.All.Should().Contain(parts[0])` (`:434`) nằm NGOÀI `if/else` ⇒ vẫn chạy cho cả nhánh
|
||||
ngoại lệ (chặn ca policy không tồn tại → 500).
|
||||
- `:477-479` `KhkkCrossModuleGates.Keys.Should().BeSubsetOf(byName.Keys)` — chống **cấu-hình-chết**
|
||||
(đổi tên/xoá action mà quên dọn map thì mục đó im lặng không ai đo). Đây là vế "vắng-mặt trông
|
||||
giống ổn" mà tôi hay phải tự đòi; lane tự đặt sẵn.
|
||||
|
||||
---
|
||||
|
||||
## §7 — Trọng tài tươi (reviewer tự chạy, không nhận số của lane)
|
||||
|
||||
| Phép đo | Lệnh | Kết quả |
|
||||
|---|---|---|
|
||||
| Build BE | `dotnet build SolutionErp.slnx -v q` | **Build succeeded — 0 Warning, 0 Error** (62s) |
|
||||
| 3 lớp test K7 | `dotnet test --filter …KhkkBridgeAuthzTests\|…CreateContractFromSigningPlanTests\|…AuthorizePolicyRegressionTests` | **39/39 passed, 0 failed** |
|
||||
| Toàn suite | `dotnet test SolutionErp.slnx --no-build` | Domain **45/45** + Infrastructure **594/594** = **639 pass / 0 fail** |
|
||||
| tsc fe-user | `npx tsc --noEmit` | exit 0, 0 dòng lỗi |
|
||||
| tsc fe-admin | `npx tsc --noEmit` | exit 0, 0 dòng lỗi |
|
||||
|
||||
🔴 **Phép cộng khớp:** baseline trước đợt này là **622** (đo ở gate K4c cùng ngày) và diff thêm
|
||||
**17** `[Fact]` ⇒ kỳ vọng 639. Đo được đúng **639**. Nghĩa là 17 test mới **có chạy thật** (không
|
||||
bị filter/skip câm) **và** 0 test cũ bị vỡ. Đây là 1 con số đóng cả 2 câu hỏi.
|
||||
|
||||
### Prod probe — TIỀN ĐỀ SEEDER, ĐO ĐƯỢC, XÁC NHẬN
|
||||
|
||||
`GET /api/permissions/by-role/{id}` cho **cả 13 vai** trên `api.solutions.com.vn` (token admin;
|
||||
lưu ý field token là `accessToken`, không phải `token` — sai tên là "login failed" giả):
|
||||
|
||||
```
|
||||
roles on prod: 13
|
||||
Accounting/AuthorizedSigner/CatalogManager/CostControl/DeptManager/Director/
|
||||
Drafter/Equipment/Finance/HrAdmin/Procurement/ProjectManager → Contracts R=True C=False
|
||||
Admin → Contracts R=True C=True
|
||||
--- roles with Contracts.CanCreate = true: 1 / 13
|
||||
```
|
||||
|
||||
Kết luận đo được (không suy từ code):
|
||||
- Tiền đề của lane **ĐÚNG**: hôm nay **Drafter = False, Procurement = False** ⇒ nút bridge sẽ 403
|
||||
cho họ nếu không có seeder. Lỗ là THẬT.
|
||||
- **Sửa lại F-9 bằng số sống:** không phải "13/13 role", mà **12/13**; và Admin qua **không chỉ**
|
||||
nhờ bypass — Admin **đã có sẵn row `CanCreate=True`** trên prod.
|
||||
- **Củng cố F-2 bằng số sống:** vì Admin đã True, nhánh Admin của seeder mới là **no-op**.
|
||||
- **Dự đoán kiểm chứng được sau deploy+restart:** seeder phải log đúng **`0 added + 2 upgraded`**
|
||||
(Drafter, Procurement). Lệch con số này = có thứ khác đang lật quyền. Đây là acceptance rẻ nhất
|
||||
cho nghiệm thu #75/#76 "restart THẬT rồi đếm row".
|
||||
|
||||
Giới hạn khai thật: prod **chưa deploy** diff này ⇒ phép đo trên chứng minh TIỀN ĐỀ, **không**
|
||||
chứng minh seeder chạy đúng. Không đo được bằng con mắt người dùng thường vì
|
||||
`nv.test@solutions.com.vn` đăng nhập thất bại (tài khoản không còn/đổi mật khẩu) — đã dùng đường
|
||||
admin thay thế.
|
||||
|
||||
---
|
||||
|
||||
## §8 — Tổng hợp finding (CHỐT)
|
||||
|
||||
| # | Sev | Vị trí | Một câu |
|
||||
|---|---|---|---|
|
||||
| F-1 | MAJOR | `CreateContractFromSigningPlanFeatures.cs:128-129` + `:248` | Chống-trùng chỉ là kiểm-tra-trong-bộ-nhớ, không ràng buộc CSDL / không transaction bao ngoài ⇒ 2 request ĐỒNG THỜI tạo 2 HĐ, 1 cái mồ côi; T11 chỉ phủ ca tuần tự. |
|
||||
| F-8 | MAJOR | `ContractSigningPlansController.cs:224` · `DbInitializer.cs:2743` · `KhkkBridgeAuthzTests.cs:153` | Mỏ-neo `DbInitializer.cs:2516` đúng lúc viết, **sai ngay lúc land** (dòng thật 2523 — chính diff này chèn 7 dòng ở `:2353-2359` đẩy nó xuống); **3 nơi**, không phải 2. |
|
||||
| F-R1 | MINOR | `scripts/{distill-shard-probe,measure-agent-memory,memory-archive-gate}.ps1` | Dirty ngoài cả 13-file-set lẫn lead-set (việc rotate `_INDEX-<period>.md`); `git add -A` sẽ nuốt vào commit K7. |
|
||||
| F-9 | MINOR | `DbInitializer.cs:2743-2744` | "403 với **13/13** role… chỉ Admin qua nhờ bypass" tự mâu thuẫn; đo prod: **12/13** có `CanCreate=False`, và Admin **đã có sẵn row True** (không chỉ bypass). |
|
||||
| F-2 | MINOR | `DbInitializer.cs:2775` | Cấp row cho Admin vô hại nhưng **vô tác dụng** — có Admin-bypass (`MenuPermissionHandler.cs:26-31`) và prod đo được Admin đã `CanCreate=True`. |
|
||||
| F-3 | MINOR | `DbInitializer.cs:2814` | "Thu hồi khi cần: xoá call ở SeedAsync" — call thật nằm trong `SeedAdminPermissionsAsync` (thân 2288-2381). |
|
||||
| F-4 | MINOR | `DbInitializer.cs:2792` | Hàm nâng cả `CanRead` trong khi tên + comment chỉ hứa `Contracts.Create` (rộng hơn nhãn). |
|
||||
| F-5 | MINOR | `KhkkDetailPage.tsx:527` · `:975` | `<a href>` thay `<Link>` (full reload, mất state SPA) + neo "Đã có HĐ" lồng trong `<label>` (interactive-lồng-interactive, cùng lớp a11y với finding K5). |
|
||||
| F-6 | MINOR | `usePipelineStages.ts:324` | `khkkPlanContractIds` gọi trong `find` trong vòng lặp — O(n·m) + cấp phát mảng mỗi lượt; vô hại ở list kẹp 200. |
|
||||
| F-7 | MINOR | `CreateContractFromSigningPlanFeatures.cs:186-207` | Không carry `DraftData` như bridge PE tiền lệ (`CreateContractFromEvaluationFeatures.cs:104`) dù `pe` đã nạp sẵn, và không có dòng nào nói đó là chủ đích. |
|
||||
| CLARIFY-1 | — | `usePermission.ts` + `AuthContext.loadMenu` | Sau khi seeder chạy, nút vẫn xám cho tới khi user **đăng-xuất/đăng-nhập lại** (menu cache `localStorage`) — UAT phải nêu, không thì bị đọc thành "seeder không ăn". |
|
||||
|
||||
**Không có blocker.** F-1 và F-8 phải xử lý nhưng không chặn commit: F-1 xác suất thấp + không mất
|
||||
dữ liệu (HĐ mồ côi xoá được), F-8 là con trỏ trong chú thích.
|
||||
|
||||
**ĐẠT (đo xong, không có finding):** hội-2-khoá đủ 2 tầng và FE không lỏng hơn server (0 đường
|
||||
gọi API lách gate) · thứ tự seeder SAU revoker (2341 → 2361) và là seeder-quyền chạy CUỐI ·
|
||||
upgrade-only + idempotent (0 chỗ hạ cờ, lượt 2 không `SaveChanges`) · chuỗi
|
||||
`Line.ContractId` → `ContractIds` → `khkkPlanContractIds` → `Set` union null-safe từng khớp ·
|
||||
mirror 3/3 byte-identical (SHA tự đo) · route `/contracts/:id` tồn tại ở CẢ 2 app · guard 4 vế
|
||||
workflow ném tách từng nguyên nhân · vá-6 đủ 4 khoản (2 NotFound guard · codegen trước `Add` ·
|
||||
`SlaDeadline` · pin `WorkflowDefinitionId`) · 17 test mới có răng thật · ngoại lệ regression siết
|
||||
đúng 1 action + chống cấu-hình-chết · build 0/0 · 639/0 test · tsc sạch 2 app.
|
||||
|
||||
**KHAI-1 (test-lane: invest T1 ghi `WorkflowDefinitionId` null nhưng vá-6 thắng): XÁC NHẬN ĐÚNG —
|
||||
ĐÓNG, không mở lại.** Spec `②:4` chốt "16 vá THẮNG khi xung đột với nền"; mã theo vá-6 (`:163-166`
|
||||
+ `:201`); và T1 `:255-257` assert đúng bản active + có bản nhiễu để chống xanh-giả.
|
||||
|
||||
---
|
||||
|
||||
## §9 — Đề nghị hành động (mô tả tiêu chí, KHÔNG chỉ-dẫn sửa mã)
|
||||
|
||||
1. **F-8** — bỏ số dòng, trỏ bằng TÊN (`SeedAllRolesReviewReadPermissionsAsync` + token
|
||||
`CanCreate = isPe` là duy nhất trong file, grep ra ngay), hoặc cập nhật cả **3** nơi thành 2523.
|
||||
Tiêu chí nghiệm thu: sau khi vá, `grep -n "CanCreate = isPe"` trên cây ĐÃ COMMIT phải khớp con
|
||||
số ghi trong chú thích.
|
||||
2. **F-1** — hoặc dựng một phép kiểm chứng minh 2 lệnh gọi đồng thời chỉ sinh 1 HĐ, hoặc khai công
|
||||
khai "chấp nhận rủi ro, chỉ chặn ở UI" ngay tại `:128-129` để người sau không đọc T11 thành
|
||||
"đã đóng".
|
||||
3. **F-9 / F-2** — sửa "13/13" thành "12/13" và bỏ vế "chỉ Admin qua nhờ bypass" (prod đo được
|
||||
Admin có row thật).
|
||||
4. **F-R1** — tách 3 file `scripts/*.ps1` khỏi commit K7, hoặc khai chúng vào lead-set trước khi
|
||||
`git add`.
|
||||
5. **Nghiệm thu sau deploy** — restart THẬT rồi soi log seeder: kỳ vọng **`0 added + 2 upgraded`**;
|
||||
và UAT phải có bước đăng-xuất/đăng-nhập lại (CLARIFY-1).
|
||||
|
||||
END gate-k7 — VERDICT=PASS-WITH-FLAGS 10
|
||||
Reference in New Issue
Block a user