Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k7.md
2026-08-01 13:36:33 +07:00

362 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

CICD-K7: PARTIAL — K7 ĐÃ LIVE & verify sạch (gate 639=45+594 · Mig GIỮ 71 · bundle rotate ×2 marker 8/8 · seeder `0 added + 2 upgraded` + COUNT 1→3 · endpoint mới 400-không-403). 2 lệch: run `aaed699` bị **CANCELLED** (ship qua #442, đề-xuất gotcha #86) + restart×2 không quan-sát-được trong cửa-sổ.
# 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).
---
## M1 — Run Gitea 🔴 PHÁT HIỆN LỚN: run của `aaed699` bị **CANCELLED**, KHÔNG success
Đo lúc 13:09-13:10 qua `actions/tasks?limit=3` (khớp theo `head_sha`, KHÔNG theo run_number):
| run | head_sha | status | started | updated | ghi chú |
|---|---|---|---|---|---|
| **#441** (id 554) | **`aaed699`** | 🔴 **cancelled** | 13:04:57 | 13:07:59 | sống ~3m02s rồi bị **huỷ giữa chừng** |
| **#442** (id 555) | `628132c` | running → (đo tiếp) | 13:08:26 | … | run-2 lead báo |
| #440 (id 553) | `1c6d77a` | success | 11:54:38 | 12:01:07 | mốc PRE (K6) |
**Cơ chế:** push `628132c` lúc 13:08:26 ⇒ Gitea **cancel-in-progress** giết run #441 đang chạy.
**Đề bài mục 1 ("run `aaed699` success") KHÔNG THOẢ THEO NGHĨA ĐEN**`aaed699` chưa bao giờ tự deploy xong.
**Nhưng K7 VẪN ship, chứng minh bằng ancestry (không phải suy đoán):**
- `git merge-base --is-ancestor aaed699 628132c` = **YES** ⇒ cây của run #442 CHỨA trọn 13 file K7.
- `git diff --name-only aaed699..628132c` = 6 file (`docs/STATUS.md` · `fe-{admin,user}/…/KhkkWorkflowPanel.tsx` · 3 `scripts/*.ps1`) — không đụng file K7 nào.
**run #442 = phương-tiện ship DUY NHẤT của K7.** Mọi phép đo POST vì thế quy về #442.
### 🔴 GOTCHA MỚI đề-xuất (#86) — "push-đè nuốt deploy im lặng"
Push code A rồi push B ngay sau (<run-time của A) run A **cancelled**. Nếu B **rơi trọn vào `paths-ignore`**
(docs/memory-only) thì run B **skip** **code A KHÔNG BAO GIỜ deploy**, bảng Actions trông vẫn "lành"
(1 cancelled + 0 run) `git log` thì đã push. Lần này thoát **NHỜ MAY**: `628132c` chạm `fe-*/src/**`
nên mới đẻ run. **Luật rút ra: sau 2 push liên tiếp, PHẢI xác minh run cuối cùng head_sha TIP
tip commit code, chứ không được đọc " run xanh" xong.**
**Hệ quả cho mục 5 (restart-proof):** giả định "run-1 + run-2 = restart ×2 miễn phí" của lead **KHÔNG còn
đúng** run #441 bị giết TRƯỚC bước deploy nên **không hề restart pool**. Trong cửa sổ này chỉ **1 lần
restart thật** (từ #442). Idempotent-qua-2-restart do đó phải hạ cấp thành "chưa chứng-minh-được trong
cửa-sổ" (xem M5) tao KHÔNG tự restart pool prod (READ-only, cấm #6).
---
## M4-PRE — mốc bundle + tập marker (ĐO TRƯỚC khi deploy đè, 13:06-13:07)
**Bundle PRE (live, khớp đúng baseline K6 post-#440):**
| app | js | css | Last-Modified (GMT) | size js |
|---|---|---|---|---|
| admin | `index-Ig1R5tAh.js` | `index-DqXm5Hpn.css` | Sat 01 Aug 2026 **04:59:40** (=11:59:40 +07) | 1.758.853 B |
| eoffice | `index-DFOCQ2ks.js` | `index-DlN3yZ5L.css` | Sat 01 Aug 2026 **05:00:41** (=12:00:41 +07) | 1.676.752 B |
**Kiểm-tra-tiền-đề cho marker (bài K6 nâng cấp — kiểm ĐƠN-VỊ-MÃ-HOÁ + ĐỊA-CHỈ trước khi tin 0-hit):**
1. `file` báo bundle "ASCII text" **nghi non-ASCII bị `\u`-escape**. Bác bỏ bằng đo: `grep -c -F 'Hợp đồng'`
= **4**, `'Kế hoạch'` = **3** trên bundle PRE tiếng Việt nằm **thô (UTF-8)**, `grep -F` đọc được.
2. **Nghi code-split** (nếu `KhkkDetailPage` nằm chunk lazy riêng thì marker 0-hit nghĩa): index.html chỉ
trỏ **đúng 2 asset** (`index-*.js` + `index-*.css`), bundle **0 ref** dạng `"./xxx-<hash>.js"` **single-bundle**, không lazy-chunk.
3. **CONTROL DƯƠNG** (chứng `KhkkDetailPage` THẬT nằm trong `index-*.js` + grep răng) 4 chuỗi bản **tiền-K7**
của chính file đó, đo trên **cả 2** bundle PRE: `Phiếu nguồn chưa có NCC trúng thầu.`=1/1 ·
`Chưa khai căn cứ nào.`=1/1 · `Giá tham chiếu (phiếu NCC)`=1/1 · `Tên TVGS xử lý`=1/1 **4/4 hiện diện**.
**CONTROL ÂM / tập marker K7 — 0-hit PRE trên CẢ 2 bundle (8/8, `grep -c -F`):**
| marker (chuỗi UI nút bridge) | adm PRE | eof PRE |
|---|---|---|
| `Đưa dòng kế hoạch vào Hợp đồng` | 0 | 0 |
| `Chọn dòng đưa vào hợp đồng` | 0 | 0 |
| `Loại hợp đồng` | 0 | 0 |
| `Quy trình duyệt hợp đồng` | 0 | 0 |
| `Phiếu chưa có dòng nhà cung cấp nào.` | 0 | 0 |
| `khkk-bridge-line` | 0 | 0 |
| `Tạo hợp đồng` | 0 | 0 |
| `Chưa có giá chốt` | 0 | 0 |
marker **tín hiệu thật** (0→≥1 mới bằng chứng ship). Diff FE đối xứng: `fe-admin` `fe-user`
mỗi bên +308 dòng `KhkkDetailPage.tsx`, +61 `usePipelineStages.ts`, +49 `types/khkk.ts` (796 insert/40 delete tổng)
kỳ vọng **element-wise 8/8 ở CẢ 2 app**.
---
## M3 — Migration (phần git, ĐÃ CHỐT) + prod PRE
- git: `1c6d77a..aaed699` **0 file** dưới `*Migrations*` **0-mig**. (Range mở rộng tới `628132c` cũng 0 6 file kia không Migrations.)
- prod PRE (sqlcmd 13:06): `MIG_TOP` = `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` = **Mig 71** ·
`sys.tables(is_ms_shipped=0)` = **97** · `MenuItems`=**200** · `Permissions`=**2221** (khớp anchor #438/#440).
---
## M5-PRE — trạng thái quyền `Contracts` TRƯỚC deploy (mấu chốt K7)
`SELECT COUNT(*) FROM Permissions WHERE MenuKey='Contracts' AND CanCreate=1` **PRE = 1** (đúng dự-đăng-ký của gate-K7)
`SELECT COUNT(*) … WHERE MenuKey='Contracts'` (mọi cờ) **PRE = 13 row** (13 role, mỗi role 1 row)
Bảng đầy đủ 13 role PRE `CanRead=1` **toàn bộ**, `CanCreate=1` **duy nhất Admin**:
| role | create | read | | role | create | read |
|---|---|---|---|---|---|---|
| Accounting | 0 | 1 | | Equipment | 0 | 1 |
| **Admin** | **1** | 1 | | Finance | 0 | 1 |
| AuthorizedSigner | 0 | 1 | | HrAdmin | 0 | 1 |
| CatalogManager | 0 | 1 | | **Procurement** | **0** | 1 |
| CostControl | 0 | 1 | | ProjectManager | 0 | 1 |
| DeptManager | 0 | 1 | | | | |
| Director | 0 | 1 | | | | |
| **Drafter** | **0** | 1 | | | | |
**Drafter + Procurement ĐÃ CÓ row sẵn** (read=1, create=0) seeder phải **UPGRADE 2 row**, **KHÔNG add row nào**
acceptance `0 added + 2 upgraded` **nhất-quán với trạng thái prod đo được**, kỳ vọng POST:
**CanCreate=1 → 3** đồng thời **tổng row `Contracts` GIỮ NGUYÊN 13** (nếu thành 15 seeder add trùng FAIL).
Đọc code seeder xác nhận khuôn UPGRADE-ONLY: `SeedContractCreateAccessAsync` gọi **sau** revoker, chỉ nâng cờ, không hạ.
---
## M1-bis — run #442 (`628132c`) = phương-tiện ship K7: **SUCCESS**
- start **13:08:26** settle **13:14:51** = **6m25s** (nằm dải bình thường 5m3x-6m3x, không escalate).
- status `success` (field `conclusion` = None như thường lệ với `tasks` tin `status`, đã biết từ trước).
- URL: `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/442`
---
## M2 — Test gate: **639 = 45 Domain + 594 Infra** ✅ KHỚP TUYỆT ĐỐI
Đo LIVE web-UI log (`/actions/runs/442/jobs/0/logs`, 18.695 B) **tách phần**, không chỉ khớp tổng:
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Duration: 169 ms - SolutionErp.Domain.Tests.dll (net10.0)
Passed! - Failed: 0, Passed: 594, Skipped: 0, Total: 594, Duration: 1 m 47 s - SolutionErp.Infrastructure.Tests.dll (net10.0)
```
| | PRE (#440) | POST (#442) | Δ |
|---|---|---|---|
| Domain | 45 | **45** | 0 |
| Infrastructure | 577 | **594** | **+17** |
| **tổng** | 622 | **639** | **+17** |
| Failed / Skipped | 0 / 0 | **0 / 0** | |
**Δ+17 phân rã khớp TỪNG ĐỒNG với nguồn** (đếm attr trong repo an toàn **0 `[Theory]`/`[InlineData]`**, hết rủi ro undercount):
- `CreateContractFromSigningPlanTests.cs` = **12** `[Fact]`
- `KhkkBridgeAuthzTests.cs` = **5** `[Fact]`
- `AuthorizePolicyRegressionTests.cs` (file **sửa**, không thêm test) = **0** attr test mới trong diff
- 12 + 5 + 0 = **17** = đúng Δ Infra đo được.
**Flaky đã khai:** `DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds`
(`ContractSigningPlanCrudTests.cs:482`) gate báo **Skipped 0 / Failed 0** **XANH lần thứ 4** liên tiếp
(#439, #440, local, #442). Chưa cớ hồi-quy.
---
## M3 — Migration GIỮ 71 + `sys.tables` 97 ✅ (2 nguồn ⟂)
| nguồn | kết quả |
|---|---|
| **git** `1c6d77a..aaed699 -- '*Migrations*'` | **0 file** 0-mig |
| **git** mở rộng tới `628132c` | **0 file** (6 file của run-2 không Migrations) |
| **sqlcmd** `__EFMigrationsHistory` TOP | `20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` = **Mig 71** (PRE == POST) |
| **sqlcmd** `COUNT(*) __EFMigrationsHistory` | **71** (khớp tên-đỉnh 2 cách đọc độc lập cùng chốt) |
| **sqlcmd** `sys.tables (is_ms_shipped=0)` | **97** (PRE == POST) |
NO-Mig proof đủ 2 chân: history KHÔNG tiến + range git rỗng file Migrations.
Anchor phụ giữ nguyên: `MenuItems` **200** · `Permissions` **2221** · control revoker `Hrm*/Off*` CanRead=1 = **29**.
---
## M4 — Bundle rotate ×2 app ✅ + marker 8/8 element-wise ✅
**Hash + LM + size (curl LIVE sau `status=success`):**
| app | PRE js | POST js | LM POST (GMT / +07) | size PREPOST | Δ |
|---|---|---|---|---|---|
| admin | `Ig1R5tAh` | **`RgvHZkPH`** | 06:13:22 / **13:13:22** | 1.758.853 **1.765.720** | **+6.867 B** |
| eoffice | `DFOCQ2ks` | **`D65FJPGd`** | 06:14:22 / **13:14:22** | 1.676.752 **1.683.623** | **+6.871 B** |
- LM cả 2 **nằm trong cửa-sổ deploy** (13:08:26 13:14:51) ship THẬT, không phải FROZEN-kẹt.
- Δ size 2 app **gần bằng nhau** (+6.867 / +6.871) nhất quán với diff FE **mirror đối xứng** (+308/+61/+49 dòng mỗi app). Nhân chứng grep.
- CSS **giữ nguyên** cả 2 (`DqXm5Hpn` / `DlN3yZ5L`) hợp lý: class mới đều utility Tailwind đã tồn tại, không sinh CSS mới. Không phải cờ đỏ.
- **NHÂN CHỨNG THỨ 4 log CI in thẳng hash+size, khớp curl tới từng byte:**
`dist/assets/index-RgvHZkPH.js 1,765.72 kB` curl `Content-Length: 1765720`
`dist/assets/index-D65FJPGd.js 1,683.62 kB` curl `Content-Length: 1683623`
- **Chống SPA-fallback trap (#S72):** `Content-Type: application/javascript` cho cả 2 hash mới;
hash **BỊA** `index-ZZZZfake9.js` `text/html` 900 B discriminator còn răng;
hash **CŨ** `Ig1R5tAh` `text/html` 900 B **bundle cũ đã biến khỏi đĩa = chứng ghi-đè** (control âm tầng-2)
**Marker (đo bằng `grep -c -F` — KHÔNG regex-neo-quote, đúng recipe K6):**
| marker | ADM prepost | EOF prepost |
|---|---|---|
| `Đưa dòng kế hoạch vào Hợp đồng` | 0 **1** | 0 **1** |
| `Chọn dòng đưa vào hợp đồng` | 0 **1** | 0 **1** |
| `Loại hợp đồng` | 0 **1** | 0 **1** |
| `Quy trình duyệt hợp đồng` | 0 **1** | 0 **1** |
| `Phiếu chưa có dòng nhà cung cấp nào.` | 0 **1** | 0 **1** |
| `khkk-bridge-line` | 0 **1** | 0 **1** |
| `Tạo hợp đồng` | 0 **1** | 0 **1** |
| `Chưa có giá chốt` | 0 **1** | 0 **1** |
| **CONTROL DƯƠNG** `Phiếu nguồn chưa có NCC trúng thầu.` | 1 **1** | 1 **1** |
| **CONTROL DƯƠNG** `Chưa khai căn cứ nào.` | 1 **1** | 1 **1** |
| **CONTROL ÂM ĐỘC-QUYỀN** `…vào Hợp đồng XYZ` (bịa) | 0 **0** | 0 **0** |
| **CONTROL ÂM ĐỘC-QUYỀN** `khkk-bridge-line-nonexistent` | 0 **0** | 0 **0** |
**8/8 × 2 app**, control dương giữ, control âm câm tín hiệu sạch cả 2 chiều.
**TRÍCH BYTE THẬT** (bắt buộc trước khi kết luận bài K6):
```
ADMIN idx=1667835 : …title:`Đưa dòng kế hoạch vào Hợp đồng`,footer:(0,N.jsxs)(N.Fragment,…
ADMIN idx=1669865 : …jsx)(`input`,{type:`radio`,name:`khkk-bridge-line`,checked:a===e.id,…
EOFFICE idx=1501164 : …title:`Đưa dòng kế hoạch vào Hợp đồng`,footer:(0,F.jsxs)(F.Fragment,…
EOFFICE idx=1503194 : …jsx)(`input`,{type:`radio`,name:`khkk-bridge-line`,checked:a===e.id,…
```
🔑 **Xác nhận lại bài K6:** rolldown/Vite-8 minify literal thành **BACKTICK** (`` title:`` ``) — regex neo `["']`
sẽ 0-hit GIẢ. `grep -F` là đúng công cụ. 2 app chỉ khác tên biến module (`N` vs `F`) = minifier, code y hệt.
**Kiểm-tra-tiền-đề đã làm TRƯỚC khi tin marker** (ghi lại vì suýt sập bẫy mới):
`file` báo bundle "ASCII text" ⇒ nghi `\u`-escape ⇒ bác bỏ bằng đo (`grep -c -F 'Hợp đồng'`=4 trên bundle PRE);
và nghi code-split ⇒ bác bỏ bằng đo (index.html chỉ 2 asset, 0 ref `"./x-<hash>.js"`). Cả 2 nghi ngờ đều SAI,
nhưng nếu không đo mà kết luận "0-hit = chưa ship" thì đã FAIL-OAN như K6.
---
## M5 — 🎯 K7-đặc-thù: seeder grant restart-proof (#75/#76)
### (a) Log API — dòng acceptance CHỐT bởi gate-K7: **CÓ, verbatim** ✅
```
2026-08-01 13:14:40.592 +07:00 [INF] Seeded Contracts.Create access: 0 added + 2 upgraded
(K7 cầu KHKK→HĐ - Drafter/Procurement/Admin; câu III default chờ ratify)
```
(`C:\inetpub\solution-erp\api\logs\solution-erp-20260801.log`; ký tự Việt hiển thị lỗi do console codepage, không phải log hỏng.)
**Chứng restart THẬT (3 chân kiềng, không đọc số lúc `running`):**
1. Dòng **ĐẦU TIÊN** của log = `13:14:33.867 [INF] Applying migrations...` ⇒ tiến trình khởi động mới.
2. `w3wp` **PID 7008**, `StartTime = 13:14:27` — và **map PID→pool bằng `appcmd list wp`** xác nhận
`WP "7008" (applicationPool:SolutionErp-Api)`. ⚠️ Map này KHÔNG thừa: PID 7680 cũng start 13:13:09 nhưng là
`vietreport-admin` (**dự án khác**) — đọc vội sẽ tưởng pool ta restart 2 lần.
3. Cả 3 mốc (13:14:27 / :33 / :40) nằm gọn trong cửa-sổ run #442 (13:08:26 → 13:14:51). ✅
### (b) sqlcmd đếm row — PRE **1** → POST **3** ✅
| phép đo | PRE (13:06) | POST (13:16) | phán |
|---|---|---|---|
| `COUNT(*) Permissions WHERE MenuKey='Contracts' AND CanCreate=1` | **1** | **3** | ✅ đúng kỳ vọng |
| `COUNT(*) Permissions WHERE MenuKey='Contracts'` (mọi cờ) | 13 | **13** | ✅ **KHÔNG đẻ row** ⇒ chứng "0 added" |
| `COUNT(*) Permissions` (toàn hệ) | 2221 | **2221** | ✅ hệ quả tất yếu của 0-added |
3 role có `CanCreate=1` POST + **hình-dạng cờ** (không chỉ đếm):
```
Admin | create=1 read=1 upd=1 del=1 ← vốn đã đủ từ trước (không bị đụng)
Drafter | create=1 read=1 upd=0 del=0 ← NÂNG, và KHÔNG tràn sang Update/Delete
Procurement | create=1 read=1 upd=0 del=0 ← NÂNG, và KHÔNG tràn sang Update/Delete
```
⇒ vừa đúng số, vừa đúng **hình dạng** (không over-grant) — thứ mà đếm-tổng đơn thuần không thấy được.
### (c) Idempotent — ⚠️ **PARTIAL: chứng ở tầng-row (ĐO ĐƯỢC), chưa chứng ở tầng restart×2 (không quan-sát-được)**
**Đo được (mạnh):** `STARTUP_COUNT('Applying migrations...')` trong log = **1** ⇒ trong cửa-sổ này pool
`SolutionErp-Api` restart **đúng 1 lần**. Giả định của lead "run-1 + run-2 = restart ×2 miễn phí" **KHÔNG thành**
vì run #441 bị **giết trước bước deploy** (M1) ⇒ **không có lần restart thứ hai để đo**.
Tao **KHÔNG tự recycle pool prod** (READ-only, anti-pattern #6/#8).
**Nhưng nhánh no-op ĐÃ CHẠY THẬT và đo được — nhờ Admin đóng vai control sẵn có:**
code đếm (`DbInitializer.cs`, diff K7):
```csharp
if (existing.TryGetValue(role.Id, out var row)) {
var changed = false;
if (!row.CanRead) { row.CanRead = true; changed = true; }
if (!row.CanCreate) { row.CanCreate = true; changed = true; }
if (changed) upgraded++; // ← CHỈ tăng khi cờ THỰC SỰ lật
continue; // ← row có sẵn ⇒ KHÔNG BAO GIỜ Add
}
```
Seeder chạy trên **3** role nhưng báo **2 upgraded** (không phải 3) ⇒ **Admin bị bỏ qua vì đã đúng sẵn**
⇒ **đó chính là nhánh idempotent, đã thực thi trên prod và đo được**, chứ không phải suy từ code.
**Suy (chưa đo — khai rõ):** restart lần 2, cả 3 row đã ở trạng thái đích ⇒ `added=0, upgraded=0` ⇒ do guard
`if (added > 0 || upgraded > 0)` bọc câu log, **sẽ KHÔNG in gì cả (IM LẶNG)** — chứ không in `0 added + 0 upgraded`.
🔎 Ai verify lần sau: **đừng đọc "log không có dòng seeder" thành hồi-quy** — đó là hình-dạng ĐÚNG của lần 2.
Phép đo rẻ nhất cho (c) ở deploy kế: đếm lại COUNT (phải vẫn **3**) + `STARTUP_COUNT` (phải **≥1** ở log mới).
---
## M6 — Smoke ✅ (kể cả endpoint MỚI)
Bearer admin OK (`accessToken` len=468, roles=`['Admin']`).
| phép thử | kỳ vọng | thực đo | |
|---|---|---|---|
| `POST /api/auth/login` (admin) | 200 + token | 200, token 468 ký tự | ✅ |
| `GET /api/contracts` | 200 | **200** | ✅ |
| `GET /api/purchase-evaluations` | 200 | **200** | ✅ |
| `GET /api/menus` | 200 | **200** | ✅ |
| `GET /api/contract-signing-plans` | 200 | **200** | ✅ |
| **control âm** `GET /api/contracts` KHÔNG token | 401 | **401** | ✅ |
| **control âm** `GET /api/bogus-route-xyz` + admin | 404 | **404** | ✅ |
| 🎯 **MỚI** `POST /api/contract-signing-plans/{0-guid}/create-contract` + **admin** | 404/400, **KHÔNG 403** | **400** | ✅ |
| 🎯 **MỚI** cùng endpoint, **không token** | 401 | **401** | ✅ |
**Wire-proof mạnh hơn cả mã số** — thân 400 là **lỗi bind/validate của chính command K7**, tức request đã đi
xuyên authz vào tới FluentValidation:
```json
{"status":400,"errors":{"PlanId":["'Plan Id' must not be empty."],
"ApprovalWorkflowId":["'Approval …"]}}
```
⇒ endpoint **đã wire thật**, policy **không chặn nhầm** Admin. (403 mới là FAIL — không xảy ra.)
### ⚠️ Hạn chế trung thực — tầng-2 (#82) của Drafter CHƯA đo LIVE được
`nv.test@solutions.com.vn / TestUser@123456` → **401 "Email hoặc mật khẩu không đúng"** (tài khoản UAT không
đăng nhập được ở thời điểm đo). Vì vậy:
- **Đã đo:** row `Contracts` của Drafter/Procurement có `CanCreate=1` (tầng dữ-liệu/hiển-thị).
- **CHƯA đo:** một principal **Drafter thật** gọi endpoint và KHÔNG bị 403 (tầng API-authz).
- Bằng chứng thay thế (CI, không phải prod): `KhkkBridgeAuthzTests` **5/5 PASS** trong gate #442.
- ⚠️ Admin qua được **không** tách bạch được công của row vs admin-bypass (chính comment seeder cũng nói vậy)
⇒ **không** dùng kết quả Admin để tuyên bố grant Drafter đã ăn ở tầng API. Đề nghị: UAT đăng nhập 1 tài khoản
Drafter thật bấm nút, hoặc cấp lại mật khẩu `nv.test` rồi đo lại 1 lệnh curl.
---
## 📌 UAT — NÓI TRƯỚC (ghi theo yêu cầu lead)
> **Nút "Đưa vào Hợp đồng" chỉ sáng cho Drafter / Procurement SAU KHI USER ĐĂNG XUẤT & ĐĂNG NHẬP LẠI.**
> Lý do: menu/permission được client cache sau lần login trước; quyền `Contracts` vừa được nâng ở phía server
> lúc 13:14:40 hôm nay nên phiên đang mở vẫn mang bộ quyền CŨ. Không phải lỗi deploy, không cần báo bug —
> chỉ cần **re-login**. (Admin không thấy khác biệt vì vốn đã có quyền.)
---
## Tổng kết 6 mục
| # | Mục | Phán | Bằng chứng gọn |
|---|---|---|---|
| 1 | Run `aaed699` success | 🔴 **LỆCH** | #441 **cancelled** 13:07:59 (bị push `628132c` giết); K7 ship qua **#442 success** 6m25s, ancestry `aaed699 ⊂ 628132c` đã verify |
| 2 | Gate **639 = 45+594** | ✅ **PASS** | log web-UI tách phần; Δ+17 = 12+5 `[Fact]` khớp từng đồng; 0 fail/0 skip; flaky xanh lần 4 |
| 3 | Mig **71** + tables **97** | ✅ **PASS** | git 0-file Migrations ⟂ sqlcmd TOP + COUNT=71 + sys.tables=97 |
| 4 | Bundle rotate ×2 + marker | ✅ **PASS** | `Ig1R5tAh→RgvHZkPH`, `DFOCQ2ks→D65FJPGd`; LM in-window; hash+size khớp log CI; marker **8/8 × 2 app** + 2 control dương + 2 control âm |
| 5 | Seeder restart-proof | 🟡 **PARTIAL** | (a) log verbatim `0 added + 2 upgraded` ✅ (b) COUNT 1→3, row 13→13 ✅ (c) restart×2 **không quan-sát-được** (chỉ 1 startup) — nhánh no-op vẫn chứng được qua Admin |
| 6 | Smoke + endpoint mới | ✅ **PASS** | 4 GET 200 · 401/404 control · endpoint mới **400 (không 403)** + 401 khi thiếu token; ⚠️ Drafter-live chưa đo (login UAT hỏng) |
**Việc cần lead xử:**
1. **Gotcha #86 (đề xuất mới)** — push-đè nuốt deploy: nếu commit thứ 2 rơi trọn `paths-ignore` thì code commit
thứ 1 **im lặng không bao giờ lên prod**. Lần này thoát nhờ may. Luật: sau 2 push liên tiếp phải kiểm
"run cuối có head_sha = TIP" **và** "TIP ⊇ commit code".
2. Mở lại tài khoản UAT `nv.test` (hoặc cấp 1 Drafter khác) để đóng nốt tầng-2 #82 cho K7.
3. Deploy kế: đếm lại `Contracts/CanCreate=1` (phải vẫn **3**) để khép mục 5(c).
---
END cicd-k7 — VERDICT=PARTIAL (K7 ĐÃ LIVE, 4/6 PASS sạch + 1 LỆCH-cơ-chế run bị huỷ + 1 PARTIAL restart×2 không quan-sát-được; 0 defect trong code K7)