16 KiB
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
- BE:
- 🔴 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-ignore7-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 Giteatasks(truststatus, đã tái-xác-nhận từ S126).- Path-filter: 15 file đổi, KHÔNG file nào khớp
paths-ignore7-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ạicicd-monitorvì 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: sau4464a46còn một deploy nữa —f4494cf(fix menu sáng nhầm), run 53114:36:54 → 14:42:35 = 5m41s status=success. Nên bundle hiện tại là củaf4494cf, đã ĐÈ bundle của4464a46. 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-Modified07: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/deletedtrô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ố.