Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/cicd-verify-dot2.md
2026-07-27 15:51:05 +07:00

248 lines
16 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.

# CI/CD verify — S155 đợt 2 (`4464a46`)
> Ghi-đĩa-trong-lúc-làm (#53). File này là SẢN PHẨM CHÍNH. Mỗi mục ghi ngay khi có bằng chứng.
> Bắt đầu: 2026-07-27.
## 0. Ngữ cảnh
- Commit: `4464a464c58b062d1d5a0fbeaf558753799baf28``[CLAUDE] PurchaseEvaluation: nut XOA phieu o man duyet + menu 3 muc + man "Da xoa" (dot 2)`
- Author date: 2026-07-27 14:06:14 +0700
- Push: `b1bae77..4464a46 → main`, `git log origin/main..HEAD` = RỖNG (đã push thật).
- 15 file đổi: 8 FE (fe-admin ×4 + fe-user ×4), 6 BE, 1 test.
- BE: `PurchaseEvaluationsController.cs`, `PurchaseEvaluationDtos.cs`, `PeSoftDeleteFeatures.cs`, `PurchaseEvaluationFeatures.cs`, `MenuKeys.cs`, `DbInitializer.cs`
- Test: `tests/SolutionErp.Infrastructure.Tests/Application/PeDeleteByApproverTests.cs`
- 🔴 **KHÔNG có file nào trong `*Migrations*`** ⇒ đợt 2 NO-MIG (đúng như lead nói).
- Không file nào khớp `paths-ignore` 7-glob ⇒ CI PHẢI chạy.
## 0bis. PRE-DEPLOY BASELINE (chụp 14:0714:08, khi run #417 còn `running`)
Chụp TRƯỚC khi deploy xong ⇒ mọi thay đổi thấy sau này là DO ĐỢT NÀY, không phải "sẵn có".
**Bundle (14:07:54):** admin js `CiUBEEJr` css `DX5ew0wg` · eoffice js `DFh7GK0t` css `BHsBUA8e`
(= đúng baseline đợt 1 run #416 ⇒ chưa ship đợt 2 tại thời điểm chụp)
**Menu `Pe_*` (10 hàng, KHÔNG có `_Approved`/`_Deleted`):**
```
Pe_DuyetNcc | Duyệt NCC | vis=1 Pe_DuyetNccPhuongAn | Duyệt NCC và Giải pháp | vis=0
Pe_DuyetNcc_Create | Thao tác Pe_DuyetNccPhuongAn_Create | Thao tác
Pe_DuyetNcc_List | Danh sách Pe_DuyetNccPhuongAn_List | Danh sách
Pe_DuyetNcc_Pending | Duyệt ← NHÃN CŨ Pe_DuyetNccPhuongAn_Pending | Duyệt ← NHÃN CŨ
Pe_DuyetNcc_WfView | Luồng duyệt Pe_DuyetNccPhuongAn_WfView | Luồng duyệt
```
🔑 **Kỹ thuật đọc nhãn tiếng Việt qua ssh→sqlcmd:** output mặc định mangle (`Duy?t`) không phân
biệt được "Duyệt" vs "Đang duyệt" đủ chắc ⇒ dùng
`CONVERT(varchar(400), CAST(Label AS varbinary(400)), 2)` → hex, decode `utf-16-le` phía client.
Pre-deploy hex-decode CHÍNH XÁC: `Pe_DuyetNcc_Pending -> 'Duyệt'` · `Pe_DuyetNccPhuongAn_Pending -> 'Duyệt'`.
**Mig top:** `20260727033522_AddPeAllowApproverDelete` (Mig 68) · `sys.tables`(is_ms_shipped=0) = **89**
## 1. Gitea Actions run — ĐANG ĐO
**PASS.** Run **#417** (task id=**530**), `head_sha=4464a464`.
- created `14:06:29``status=success` @ `14:12:16+07:00`**5m47s** (bình thường, ~5m30 gần đây).
- `conclusion=None` — đúng khuôn Gitea `tasks` (trust `status`, đã tái-xác-nhận từ S126).
- Path-filter: 15 file đổi, KHÔNG file nào khớp `paths-ignore` 7-glob ⇒ CI trigger ĐÚNG.
- URL: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/417
**Test gate — số THẬT trích từ log CI** (`…/actions/runs/417/jobs/0/logs`, 17.864 B):
```
07:06:53Z Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 — SolutionErp.Domain.Tests.dll
07:09:25Z Passed! - Failed: 0, Passed: 517, Skipped: 0, Total: 517 — SolutionErp.Infrastructure.Tests.dll
```
**562 PASS / 0 FAIL / 0 SKIP** (45 + 517). **Khớp CHÍNH XÁC số lead đo local (562)** = cross-check
2 nguồn độc lập. Tăng **+11 so với run #416 đợt 1 (551)** — khớp file test mới `PeDeleteByApproverTests.cs`.
Gate chạy TRƯỚC build/deploy ⇒ `success` ⟹ gate đã qua.
## 2. Menu prod — ✅ PASS (TRỌNG-TÂM, gồm cả chỗ dễ trượt nhất)
Đo lúc 14:12 (sau `success`), cùng câu lệnh hex-decode như baseline 14:08 ⇒ so được 1:1.
```
Pe_DuyetNcc 'Duyệt NCC' vis=1 ord=1
Pe_DuyetNcc_WfView 'Luồng duyệt' vis=1 ord=2
Pe_DuyetNcc_List 'Danh sách' vis=1 ord=3
Pe_DuyetNcc_Create 'Thao tác' vis=1 ord=4
Pe_DuyetNcc_Pending 'Đang duyệt' 🔴ĐÃ ĐỔI vis=1 ord=5
Pe_DuyetNcc_Approved 'Đã duyệt' 🆕MỚI vis=1 ord=6
Pe_DuyetNcc_Deleted 'Đã xóa' 🆕MỚI vis=1 ord=7
Pe_DuyetNccPhuongAn 'Duyệt NCC và Giải pháp' vis=0 ord=8
Pe_DuyetNccPhuongAn_WfView 'Luồng duyệt' vis=1 ord=9
Pe_DuyetNccPhuongAn_List 'Danh sách' vis=1 ord=10
Pe_DuyetNccPhuongAn_Create 'Thao tác' vis=1 ord=11
Pe_DuyetNccPhuongAn_Pending 'Đang duyệt' 🔴ĐÃ ĐỔI vis=1 ord=12
Pe_DuyetNccPhuongAn_Approved 'Đã duyệt' 🆕MỚI vis=1 ord=13
Pe_DuyetNccPhuongAn_Deleted 'Đã xóa' 🆕MỚI vis=1 ord=14
```
| Điểm kiểm | Kỳ vọng | Thật | |
|---|---|---|---|
| `Pe_{code}_Approved` nhãn "Đã duyệt" | 2/2 type | 2/2 có, nhãn ĐÚNG | ✅ |
| `Pe_{code}_Deleted` nhãn "Đã xóa" | 2/2 type | 2/2 có, nhãn ĐÚNG | ✅ |
| 🔴 `Pe_{code}_Pending` "Duyệt"→"Đang duyệt" | 2/2 đổi | 2/2 đổi (**pre-deploy 14:08 còn là `'Duyệt'`**) | ✅ |
| `Pe_*` tổng | 10 → 14 | 10 → **14** (+4) | ✅ |
🔑 **3 chứng cứ cùng lúc, khoá chặt trọng-tâm "API phải restart thì seed mới ăn":**
(i) 4 key mới KHÔNG có ở baseline 14:08 và CÓ ở 14:12 ⇒ seeder chạy TRONG cửa-sổ deploy, không phải sẵn có;
(ii) nhãn `Pending` đổi được ⇒ **`labelBackfill` (`DbInitializer.cs:1918-1924`) ĐÃ CHẠY THẬT** — đây là
đường RIÊNG, không đi qua upsert `:1893-1905` (upsert gặp key cũ chỉ update `Order` rồi `continue`,
không đụng `Label`). Nhãn vẫn "Duyệt" sẽ là FAIL đáng báo động — **KHÔNG xảy ra**;
(iii) `Order` cả cụm được ghi lại liền mạch 1→14 (Approved=6/13, Deleted=7/14 chèn sau Pending) ⇒
nhánh upsert cũng chạy trọn, không nửa vời.
## 3. Permission seed — ✅ PASS (cả 2 danh sách đều ăn)
`Permissions` prod (cột thật: `RoleId` + **`MenuKey` nvarchar**, KHÔNG phải FK `MenuItemId`).
Prod có **13 role**. Gom nhóm theo `MenuKey LIKE 'Pe[_]%'`:
```
Pe_DuyetNcc rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_WfView rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_List rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_Create rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_Pending rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_Approved 🆕 rows=13 read=13 cre=13 upd=7 del=1
Pe_DuyetNcc_Deleted 🆕 rows=13 read=13 cre=13 upd=7 del=1
… (khối `DuyetNccPhuongAn` 7 dòng GIỐNG HỆT, kể cả 2 key mới)
```
**4 key mới đều có đủ 13 hàng/13 role**, và profile quyền (13/13/7/1) **trùng khít** key `Pe_*`
⇒ CẢ HAI danh sách được vá (`DbInitializer.cs:2110-2111` `UpgradeReviewModulePermissions…` **và**
`:2514-2515` `SeedPurchaseEvaluationPermissionDefaults…`) — không lệch bên nào.
Tổng: `Permissions` 718 hàng / **68 MenuKey distinct** · `MenuItems` 105 hàng.
**Số canonical KHÔNG đổi (đúng chủ đích):** đếm từ mã `MenuKeys.All` = **54** phần tử ·
`Actions` = 4 ⇒ **Policies = 216**. Trong `All` chỉ có root `PurchaseEvaluations`, KHÔNG có
`Pe_*` leaf. Diff `MenuKeys.cs` đợt này chỉ 1 hunk `@@ -135,6 +135,15 @@` (2 factory + chú thích),
**không chạm mảng `All`** (dòng 156). ✅
**NO-MIG confirm:** `git diff HEAD~1 HEAD -- '*Migrations*'` = RỖNG · `__EFMigrationsHistory` top
vẫn `20260727033522_AddPeAllowApproverDelete` (Mig 68) · `sys.tables`(is_ms_shipped=0) = **89**.
Đúng kỳ vọng "đợt 2 không có migration".
---
> 🔻 **Mục 4-7 đo ở `/tiep` @S156 (2026-07-27 ~15:35), do LEAD tự đo bằng curl/sqlcmd** — không giao lại
> `cicd-monitor` vì chính vai đó đã dừng giữa chừng ở file này lúc 14:14 (VERIFY-lane, khuôn `#53`).
> 🔴 **Đổi ngữ cảnh so mục 0-3:** sau `4464a46` còn một deploy nữa — **`f4494cf`** (fix menu sáng nhầm),
> run **531** `14:36:54 → 14:42:35 = 5m41s status=success`. Nên **bundle hiện tại là của `f4494cf`**,
> đã ĐÈ bundle của `4464a46`. Xem caveat B ở mục 7.
## 4. Endpoint mới — ✅ PASS (401, KHÔNG phải 404)
Route thật (`PurchaseEvaluationsController.cs:177,187`): `DELETE {id:guid}/by-approver` · `GET deleted`.
```
DELETE /api/purchase-evaluations/{guid}/by-approver -> 401 ← endpoint MỚI #1
GET /api/purchase-evaluations/deleted -> 401 ← endpoint MỚI #2
DELETE /api/purchase-evaluations/{guid} -> 401 (control: xóa nháp, có từ trước)
--- control: route chắc chắn KHÔNG tồn tại ---
DELETE /api/purchase-evaluations/{guid}/by-approver-nonexistent -> 404
GET /api/purchase-evaluations/deleted-nonexistent -> 404
```
→ 2 route mới **401** = đã lên prod + `[Authorize]` chặn đúng. Route bịa trả **404** ⟹ 404 đúng là tín-hiệu
"route chưa lên", nên **401 ở route thật là CÓ NGHĨA** — discriminator sạch, không phải may.
## 5. Smoke chống hồi quy — ✅ PASS 8/8 + D3 anti-regression PASS
Bearer admin (`POST /api/auth/login` → 200, `accessToken` 468 ký tự).
```
PE /api/purchase-evaluations?page=1&pageSize=5 HTTP 200 bytes=5103 InvalidCol=False
HopDong /api/contracts?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
DeXuat /api/proposals?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
NghiPhep /api/leave-requests?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
TangCa /api/ot-requests?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
CongTac /api/travel-requests?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
DatXe /api/vehicle-bookings?page=1&pageSize=5 HTTP 200 bytes=91 InvalidCol=False
AwV2-list /api/approval-workflows-v2 HTTP 200 bytes=41230 InvalidCol=False
```
**8/8 HTTP 200, 0/8 có `Invalid column name`**. (Caveat giữ nguyên như đợt 1: 6 module Văn-phòng-số trả
91 B = envelope rỗng ⇒ chỉ chứng minh query **không ném**, chưa chứng minh join sâu.)
**Model ⟷ DB đồng bộ:** `AwV2` materialize `allowApproverDelete` = **54** · `allowApproverFinalize` = **54**
(khớp đúng số đợt 1) ⇒ cột mới round-trip qua EF từ DB thật.
### 🔑 D3 anti-regression — phiếu xóa mềm KHÔNG rò rỉ sang list sống
🔴 **2 phép đo ĐẦU TIÊN của lead là RỖNG NGHĨA, phải vứt** — đo `item.isDeleted` trên cả 2 endpoint,
ra "0 rò rỉ" + "9 item không-deleted lọt vào". **Cả hai vô nghĩa**: `isDeleted` **KHÔNG có trong DTO**
(đã kiểm: key-set 29 field, `'isDeleted' in item` = `False` ở CẢ HAI). Một cái trông như PASS, một cái
trông như FAIL — **đều là hư không**. Đúng lớp lỗi *phép-đo-rỗng-nghĩa* mà chính phiên này vừa ghi bài học.
Phép ĐÚNG = **giao tập ID**, độc lập hoàn toàn với field DTO:
```
list thường = 37 phiếu (totalCount=37)
/deleted = 9 phiếu (totalCount=9)
GIAO TẬP = 0 ← phiếu đã xóa KHÔNG hiện ở list sống
```
**8a PASS** (`IgnoreQueryFilters()` có kèm `.Where(x => x.IsDeleted)`, không "gỡ filter rồi liệt kê tất").
**8c(a) PASS** (list "Đã xóa" không chứa phiếu sống). Hai tập phân hoạch sạch.
**KHÔNG phải lỗi (đã soi, kết luận là cố ý):** `/deleted` dùng **CHUNG DTO** với list sống ⇒ không có
`deletedAt`/`deletedBy`. FE khai thẳng ở `PurchaseEvaluationsListPage.tsx:557-561` và hiển thị `updatedAt`
với nhãn **`🗑 xóa ≈`**, **cố ý bỏ trống tên người xóa thay vì đoán**. Spec mục 6 chỉ đòi "CHỈ XEM",
không đòi cột ai/khi-nào ⇒ **không phải lớp "DTO thiếu field FE đang đọc"** (H1 đợt 2), vì FE biết và đã
xử lý trung thực. Ghi lại để lần sau khỏi báo động nhầm.
## 6. Bundle FE ×2 — ✅ PASS
```
== admin ==
JS pre(14:07)=CiUBEEJr now=BK8w3QxU ROTATED
CSS pre(14:07)=DX5ew0wg now=DiNJjaoo ROTATED
real-js index-BK8w3QxU.js HTTP 200 size=1674598 ct=application/javascript LM=Mon, 27 Jul 2026 07:41:15 GMT
FAKE-ctl HTTP 200 size=900 ct=text/html
== eoffice ==
JS pre(14:07)=DFh7GK0t now=DYnxSI0T ROTATED
CSS pre(14:07)=BHsBUA8e now=BRCvEHvC ROTATED
real-js index-DYnxSI0T.js HTTP 200 size=1574829 ct=application/javascript LM=Mon, 27 Jul 2026 07:42:10 GMT
FAKE-ctl HTTP 200 size=876 ct=text/html
```
- **JS + CSS ROTATE cả 2 app** so baseline pre-đợt-2 (14:07:54).
- `Last-Modified` **07:41:15 / 07:42:10 GMT = 14:41:15 / 14:42:10 +07** → nằm **TRONG cửa-sổ deploy run 531
(`f4494cf`, 14:36:5414:42:35)** ⇒ ship-proof thật, không phải hash-delta suông.
- Bẫy SPA-fallback loại sạch: hash bịa cũng trả 200 nhưng **`text/html` + 900/876 B** ⟹ file thật là JS thật.
### Byte-verify #77 — mã tính-năng CÓ THẬT trong bundle prod (cộng dồn 3 commit)
| chuỗi | nguồn | admin | eoffice |
|---|---|---|---|
| `allowApproverDelete` | `b1bae77` đợt 1 (cờ F6) | 8 | 2 |
| `by-approver` | `4464a46` đợt 2 (endpoint xóa) | 1 | 1 |
| `deleted=1` | `4464a46` đợt 2 (màn "Đã xóa") | 1 | 1 |
| `view=approved` | `f4494cf` (fix menu sáng nhầm) | 1 | 1 |
**4/4 marker có mặt trong CẢ HAI bundle** ⟹ mã của **cả ba** commit đang sống trên prod.
## 7. Verdict — ✅ **CICD PASS 6/6**
| # | Hạng mục | Kết quả |
|---|---|---|
| 1 | Gitea run + test gate | ✅ run 530 `4464a46` success 5m47s · **562 PASS/0 FAIL** trích từ log CI |
| 2 | Menu prod (trọng-tâm) | ✅ 4 key mới + `Pending` "Duyệt"→"Đang duyệt" 2/2, có mốc pre-deploy làm chứng |
| 3 | Permission seed | ✅ 4 key mới × 13/13 role, cả 2 danh sách seed đều ăn · NO-MIG confirm |
| 4 | Endpoint mới | ✅ 2/2 route → 401; control bịa → 404 |
| 5 | Smoke + D3 anti-regression | ✅ 8/8 · giao tập ID = 0 · AwV2 cột mới ×54 |
| 6 | Bundle FE ×2 | ✅ rotate ×2 app, LM trong cửa-sổ deploy, 4/4 byte-marker |
**Kèm: cicd cho `f4494cf` cũng ĐẠT** — run 531 success 5m41s; bundle hiện tại chính là của commit này
(LM 14:4114:42) và chứa `view=approved`. Hai mục nợ trong WAL (`[!]` đợt 2 + `[ ]` `f4494cf`) đóng **cùng
một lượt đo** vì dùng chung phép bundle.
### ⚠️ 3 caveat khai thẳng (đừng đọc thành "đã kín")
- **A. 6 module Văn-phòng-số trả envelope rỗng 91 B** ⇒ chỉ chứng minh query không ném, **chưa** chứng minh
join sâu. Hai phép mạnh thật là PE (5.103 B có dữ-liệu) và AwV2 (41.230 B).
- **B. 🔴 Không thể chứng minh HỒI-TỐ rằng bundle của riêng `4464a46` đã rotate lúc 14:12** — không ai chụp
snapshot tại thời điểm đó, và `f4494cf` đã đè lên 30 phút sau. Cái **chứng minh được** là: mã FE của đợt 2
(`by-approver`, `deleted=1`) **đang sống trong bundle prod hiện tại**. Về vận-hành đó mới là điều cần biết,
nhưng phát biểu phải đúng phạm vi — không được nói "bundle đợt 2 đã verify rotate".
- **C. Lead tự bắt 2 lỗi đo của chính mình trong lượt này**: (i) `item.isDeleted` — field không tồn tại,
cả PASS-giả lẫn FAIL-giả; (ii) `item.code` — tên thật là `maPhieu`, nên `/deleted` trông như trả rỗng.
Cả hai lộ ra vì **hỏi lại "phép đo này có đo gì không"** trước khi tin con số.
<!-- END · TOTAL=6 HANG-MUC · VERDICT=PASS -->