# 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:07–14: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ũ ⇒ 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:54–14: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:41–14: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ố.