CICD-K6: PASS — 5/5

# cicd-monitor — verify deploy K6 (S167, 2026-08-01)

Push range `e202393..1c6d77a` (2 commit: `d82c5a8` wal-flush + `1c6d77a` K6). ĐÚNG 1 file code `fe-admin/src/components/Layout.tsx`. 0-mig · fe-user 0-diff · BE 0-diff (đo: `git diff --name-only e202393..1c6d77a -- fe-user/ src/Backend/ tests/` = **0 file**).
Run **#440** (task id 553) — **success**, 11:54:38 → 12:01:07 (**6m29s**, trong dải quen 5m3x–6m3x).
Artifact ghi TỪNG MỤC ngay khi có bằng chứng (luật #53: ghi-đĩa-TRONG-lúc-làm; return chỉ là tóm tắt).

---

## M0 — Mốc PRE (tự đo LIVE lúc run CÒN `running`, KHÔNG tin số chép)

Cửa sổ vàng: bắt được #440 ở `running` lúc **11:55:43** ⇒ bundle LIVE lúc đó = PRE thật.

| App | index-*.js | index-*.css | size js (B) | Last-Modified js | Content-Type |
|---|---|---|---|---|---|
| admin | `index-C2FhHCNT.js` | `index-DqXm5Hpn.css` (86.495) | **1.758.685** | 01 Aug 04:13:05 GMT = **11:13:05 +07** | application/javascript |
| eoffice | `index-DFOCQ2ks.js` | `index-DlN3yZ5L.css` (92.233) | **1.676.752** | 01 Aug 04:14:05 GMT = **11:14:05 +07** | application/javascript |

🔑 **PRE tự chứng là bundle #439 bằng LM, không chỉ bằng hash**: LM 11:13:05 / 11:14:05 nằm TRỌN trong cửa-sổ deploy #439 (11:08:17 → 11:14:33). Trùng baseline đề bài — nhưng trùng đó là KẾT QUẢ ĐO, không phải giả-định đầu vào (bài "mốc PRE lão hoá sau MỖI deploy").
Content-Type `application/javascript` + ~1,7MB ⇒ không dính bẫy SPA-fallback (`text/html` ~900B). File tải về khớp byte-for-byte `Content-Length`.

## M0-bis — Chọn byte-marker CÓ RĂNG (verify 0-hit PRE TRƯỚC khi phong)

Đề bài cảnh báo đúng: K6 = 12 key literal **đã có từ K4a** + comment sẽ bị Vite strip ⇒ marker ngây thơ = 0 bit. Đo PRE xác nhận từng cái:

❌ **LOẠI "key đơn lẻ"** — PRE admin đã có sẵn: `PurchaseEvaluations` **4** · `KeHoachKyKet` **1** · `HopDongCung` **1** · `PeWorkflows` **3** ⇒ POST có hit cũng **0 bit**.
❌ **LOẠI tên biến `ADMIN_HIDDEN_KEYS`** — minifier đổi tên; đo POST: source-name **0 hit**, thực tế thành `var IW=new Set([...])`. (Bài K3 `earlyFinalizeLocked` tái xác nhận.)
❌ **LOẠI comment lineage** — `NƠI-CONFIG` / `thắng mốc trước`: PRE **0** POST **0** ⇒ Vite strip đúng như dự đoán.

✅ **CHỌN marker HỢP-CHẤT: TRẬT TỰ KỀ NHAU của 12 literal trong `new Set([...])`.** Từng literal thì cũ, nhưng *thứ tự kề nhau* chỉ sinh ra khi Set mới được minify. Lineage source: `git grep ADMIN_HIDDEN_KEYS` @`e202393` = **0 hit** (exit 1) · @`1c6d77a` = **2 hit** ⇒ marker chưa tồn tại ở mốc PRE.
✅ **Marker ÂM (K6 là DISPLAY-ONLY, không gỡ route):** `/purchase-evaluations`, `/khkk/list`, `/suppliers` PHẢI CÒN trong bundle admin POST.
✅ **Differential:** eoffice kỳ vọng FROZEN (fe-user 0-diff) ⇒ marker phải **0** ở eoffice cả PRE lẫn POST.

---

## Mục 1 — Run Gitea cho `1c6d77a` ✅ PASS

| | |
|---|---|
| head_sha | `1c6d77a6ccd95b2e287a3602f23780558c9d1ecc` (khớp HEAD; `git log origin/main..HEAD` **rỗng**) |
| run | **#440** / task id **553** / `status=success` |
| thời gian | created 11:54:38 → updated 12:01:07 = **6m29s** |
| tail log | `Deploy done. App pool started.` → `API /health/live -> 200` → `🏁 Job succeeded` |

`paths-ignore` (7-glob): 8/10 file trong range là `.claude/**` (bị bỏ qua), **nhưng** `fe-admin/**` KHÔNG ⇒ CI đúng ra phải chạy, và nó đã chạy.

## Mục 2 — Gate CI **622 = 45 D + 577 I** ✅ PASS (tách-phần, khớp từng vế)

Đo ở web-UI log `…/actions/runs/440/jobs/0/logs` (18.697 B):

| Project | dòng log | Failed | Passed | Skipped |
|---|---|---|---|---|
| `SolutionErp.Domain.Tests.dll` | L29 | **0** | **45** | 0 |
| `SolutionErp.Infrastructure.Tests.dll` | L47 | **0** | **577** | 0 |
| **Tổng** | | **0** | **622** | 0 |

- Khớp **dự đoán TRƯỚC khi đo**: giữ nguyên 622, delta **0** — đúng: K6 chỉ thêm `Set` + comment, 0 file test trong range.
- **control ÂM:** `grep -c 'Failed!'` = **0**; pattern lỗi = **1 hit duy nhất** là dòng runner vô hại `Failed to setup Pty Unsupported` (L12, có ở mọi run).
- **Flaky đã khai** `DeleteContractSigningPlan_SoftDeletes`: run này **KHÔNG đỏ** (Failed 0). Mốc #439 xanh, #440 xanh ⇒ 2 run liên tiếp sạch, chưa cần re-run.

## Mục 3 — Mig GIỮ **71** + `sys.tables` **97** ✅ PASS (2 nguồn)

- **Nguồn 1 — git:** `git diff e202393..1c6d77a -- '*Migrations*'` = **rỗng** ⇒ 0-mig phía code. Đếm file repo (filter neo-đuôi `Designer\.cs$|ModelSnapshot\.cs$` — bản vá từ K5) = **71**.
- **Nguồn 2 — sqlcmd prod (đo SAU `status=success`, tức sau recycle):**
  `MIGTOP=20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime` (= Mig 71, GIỮ) · `MIGCOUNT=71` · `SYSTABLES=97` (is_ms_shipped=0).
- Anchor DB không nhích (đúng cho FE-only): `MENUITEMS=200` · `PERMS=2221` — y hệt mốc #438.

## Mục 4 — Bundle: admin ROTATE / eoffice FROZEN-hợp-lệ ✅ PASS

| App | asset | PRE | POST | size | LM POST (+07) | trong cửa-sổ deploy? |
|---|---|---|---|---|---|---|
| admin | js | `C2FhHCNT` | **`Ig1R5tAh`** ROTATE | 1.758.685 → **1.758.853** (**+168**) | 11:59:40 | ✅ (11:54:38–12:01:07) |
| admin | css | `DqXm5Hpn` | `DqXm5Hpn` FROZEN | 86.495 (giữ) | 11:59:40 | ✅ |
| eoffice | js | `DFOCQ2ks` | `DFOCQ2ks` FROZEN | 1.676.752 (giữ) | **12:00:41** (PRE là 11:14:05) | ✅ |
| eoffice | css | `DlN3yZ5L` | `DlN3yZ5L` FROZEN | 92.233 (giữ) | 12:00:41 | ✅ |

**Admin rotate = ĐẠT điều kiện FAIL-nếu-không** của đề bài. Δ **+168 B** — dương, nhỏ, đúng cỡ 1 `new Set` 12 key backtick (~170 ký tự); comment bị strip nên không cộng thêm.

🔑 **eoffice FROZEN là LÀNH TÍNH — chứng bằng 3 nhân chứng độc lập, KHÔNG phải "đoán vì fe-user 0-diff":**
1. **LM ĐÃ ĐỔI** 11:14:05 → **12:00:41**, nằm trong cửa-sổ #440 ⇒ file **có bị ghi lại** (deploy.yml copy `dist\*` vô điều kiện = #69 mode-A), không phải bundle cũ kẹt lại. *Đây là discriminator quyết định: bundle-cũ-kẹt sẽ giữ LM 11:14:05.*
2. **Log CI in thẳng asset phát ra** — L128 `dist/assets/index-DFOCQ2ks.js 1,676.75 kB` lúc `05:00:41.56` GMT ⇒ runner **ĐÃ BUILD LẠI** fe-user trong CHÍNH run này và Vite phát ra **cùng hash**. LM trên prod = mốc build này **khớp tới từng giây**. Frozen = build-determinism, không phải skip.
3. `cmp` PRE vs POST eoffice = **byte-identical** ⇒ nhất quán với source 0-diff.
   (Đối chiếu admin: L93 `dist/assets/index-Ig1R5tAh.js 1,758.85 kB` @ `04:59:40.39` — khớp cả hash lẫn size lẫn LM prod. Log-CI là nhân chứng **thứ 4**, độc lập với curl prod.)

🔑 **Control-ÂM tầng-2 (chứng ghi-đè thật):** GET hash CŨ admin `index-C2FhHCNT.js` → **200 `text/html` 900B** ⇒ rơi về SPA-fallback ⇒ bundle cũ đã biến mất khỏi đĩa. (200 ở đây KHÔNG phải "còn sống" — discriminator là Content-Type.)

### 4-bis — Byte-marker: 🔴 SUÝT FAIL-OAN 2 LẦN vì CƠ-CHẾ REGEX, không phải vì code

Marker hợp-chất đo lần đầu ra **PRE 0 / POST 0** — đọc vội thì thành "K6 không ship" ⇒ FAIL. Không phong vội, đi **soi byte thật** (`grep -oE '.{90}HopDongCung.{90}'`) thì thấy dạng minify THẬT:

```
var IW=new Set([`PurchaseEvaluations`,`KeHoachKyKet`,`Contracts`,`HopDongCung`,`Forms`,
`Reports`,`Master`,`Hrm`,`Off`,`Personal`,`Workflows`,`PeWorkflows`]);
```

- **Sai lần 1:** regex của tao chỉ chấp `["']` — rolldown/Vite 8 phát **backtick** (template literal). 0-hit là do regex không thể khớp, KHÔNG phải do vắng mặt.
- **Sai lần 2:** vá xong, truyền pattern qua biến trong `"..."` → backtick bị shell nuốt, `grep -cE` gãy (`grep: Unmatched (`) và trả **0** gần như im lặng trong vòng lặp. Lại một 0-hit giả.
- **Chốt bằng `grep -F` fixed-string** (không còn regex để sai):

| Marker | PRE admin | POST admin | POST eoffice | kỳ vọng | |
|---|---|---|---|---|---|
| Trọn chuỗi 12-key `new Set([...])` | **0** | **1** | **0** | 0→1, eoffice 0 | ✅ |
| Adjacency 3-key `` `PurchaseEvaluations`,`KeHoachKyKet`,`Contracts` `` | **0** | **1** | 0 | 0→1 | ✅ |
| control-ÂM đảo trật tự `` `KeHoachKyKet`,`PurchaseEvaluations` `` | 0 | **0** | 0 | 0 | ✅ |
| control-ÂM chuỗi bịa `ZZZ_NOT_EXIST_MARKER` | 0 | **0** | 0 | 0 | ✅ |

🔑 **COUNT-DELTA — nhân chứng thứ 2, độc lập với adjacency.** Vì literal đã tồn tại từ K4a nên *sự có mặt* = 0 bit, nhưng *số lần xuất hiện tăng* thì đo được. PRE→POST admin, cả **12/12** key đều **+1**:

`PurchaseEvaluations` 4→5 · `KeHoachKyKet` 1→2 · `Contracts` 7→8 · `HopDongCung` 1→2 · `Forms` 3→4 · `Reports` 3→4 · `Master` 4→5 · `Hrm` 28→29 · `Off` 400→401 · `Personal` 2→3 · `PeWorkflows` 3→4 · **`Workflows` 9→11 (+2)**.

Con **+2** duy nhất KHÔNG phải bất thường mà là **kiểm-tra-nhất-quán tự thân**: `Workflows` là substring của `PeWorkflows`, nên Set thêm 1 `Workflows` + 1 `PeWorkflows` ⇒ đếm substring ra +2. Nếu K6 chỉ ship một phần thì hình dạng 12×(+1) với đúng 1 ngoại lệ giải-thích-được này sẽ không dựng lại được.

🔑 **Marker ÂM (display-only, KHÔNG phải gỡ route) — ĐẠT:** `/purchase-evaluations` **68→68** · `/khkk/list` **13→13** · `/suppliers` **16→16**. Route còn nguyên trong bundle admin ⇒ K6 cắt đúng lớp *hiển thị*, không lỡ tay cắt lớp routing.

## Mục 5 — Smoke + phép K6-đặc-thù ✅ PASS

Bearer admin: `POST /api/auth/login` → `accessToken` len **468** ✅

| Endpoint | code |
|---|---|
| `GET /api/contracts` | **200** |
| `GET /api/purchase-evaluations` | **200** |
| `GET /api/menus` | **200** |
| `GET /api/contract-signing-plans` | **200** |
| control-ÂM `GET /api/khong-ton-tai-zzz` (có bearer) | **404** ✅ |
| control-ÂM `GET /api/contracts` KHÔNG token | **401** ✅ |

🔑 **Phép K6-đặc-thù — 2 tầng độc lập (gotcha #82) đo được bằng số:** `GET /api/menus` bearer admin trả **200 item** (khớp `MENUITEMS=200` sqlcmd ⇒ không mất row nào). Trong đó **10/10 root bị K6 ẩn VẪN được BE trả về**: `Contracts`, `Forms`, `HopDongCung`, `Hrm`, `KeHoachKyKet`, `Master`, `Off`, `Personal`, `PurchaseEvaluations`, `Reports` — cộng cả 2 con của `System` là `Workflows` + `PeWorkflows` ⇒ **12/12**.
⇒ Đúng như comment K6 tự khai: **ẩn ở client, KHÔNG thu quyền**. Deep-link + API vẫn 200. Muốn CHẶN thật phải làm ở BE authz.

---

## Verdict

**PASS 5/5.** Run #440 `1c6d77a` success 6m29s · gate **622 = 45 D + 577 I** (Failed 0, delta 0 đúng dự đoán) · Mig GIỮ **71** + `sys.tables` **97**, MenuItems/Perms đứng im 200/2221 · admin ROTATE `Ig1R5tAh` (+168 B) & eoffice FROZEN-**lành-tính** (LM đổi + log CI in cùng hash + byte-identical) · marker 12-key `Set` PRE 0→POST 1 + count-delta 12/12 + marker-ÂM route 68/13/16 nguyên vẹn · smoke 4/4 + 2 control-âm + menu-tree 12/12 key vẫn được BE phục vụ.

**Treo / khuyến nghị (không chặn):**
1. 🔴 **Bài học to nhất run này: 0-hit marker bị SAI 2 lần liên tiếp do cơ-chế regex, cả 2 lần đều trông y hệt "code không ship".** (a) rolldown/Vite 8 minify string → **backtick**, regex neo `["']` không bao giờ khớp; (b) truyền pattern chứa backtick qua biến trong `"..."` làm grep gãy và trả 0 gần như im lặng. ⇒ Từ nay: marker chuỗi trên bundle **dùng `grep -F` fixed-string**, và **luôn trích byte thật quanh literal (`.{90}X.{90}`) trước khi kết luận vắng mặt**.
2. **Kỹ thuật mới dùng được lại — COUNT-DELTA:** khi literal đã tồn tại từ đợt trước (marker "có/không" = 0 bit), đo **số lần xuất hiện PRE→POST**. Ngoại lệ substring (`Workflows` ⊂ `PeWorkflows` ⇒ +2) lại thành kiểm-tra-nhất-quán miễn phí.
3. **Log CI in thẳng `dist/assets/index-<hash>.js <size>`** — nhân chứng thứ 4 cho ship-proof, độc lập hoàn toàn với curl prod, và là cách rẻ nhất phân biệt FROZEN-lành-tính với bundle-kẹt (so LM prod với mốc build in trong log).

**Bundle baseline MỚI (post-#440):** admin `Ig1R5tAh`/`DqXm5Hpn` · eoffice `DFOCQ2ks`/`DlN3yZ5L`. *(Snapshot-per-run — lão hoá ngay ở deploy kế.)*

END cicd-k6 — VERDICT=PASS 5/5
