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

16 KiB
Raw Blame History

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.

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… :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ố.