---
title: "Coverage 100% mà vẫn không chạy"
description: "Một tính năng chết hẳn trong sản phẩm, trong khi 178 test xanh và coverage vẫn 100%. Coverage đo dòng đã chạy, chứ không đo đường mà sản phẩm thật sự đi."
pubDate: 2026-09-29
author: tusgino
tags: [ai-engineering, architecture, infrastructure, testing]
series:
  id: tu-llm-den-harness
  title: "Từ LLM đến Harness"
  order: 11
cover: /images/covers/coverage-100-ma-van-khong-chay.svg
coverAlt: "Một điểm rẽ thành hai đường: đường trên qua năm ô đã đánh dấu, đường dưới đứt đoạn tới một ô rỗng"
faq:
  - q: "Coverage 100% thì còn sai được ở đâu?"
    a: "Ở chỗ đường đi. Coverage đo dòng nào đã chạy, không đo dòng đó có được chạy qua đúng đường mà sản phẩm dùng hay không."
  - q: "Vì sao test xanh mà CI vẫn cho qua?"
    a: "Vì bài e2e duy nhất chạm tới đường thật bị khoá sau API key, nên CI không có key thì bỏ qua nó."
  - q: "Làm sao biết một test có thật sự canh được lỗi?"
    a: "Gieo lại lỗi cũ rồi xem test có đỏ không. Một test chưa từng đỏ là một test chưa chứng minh được gì."
  - q: "Có nên bắt chước cổng coverage 100% mỗi file không?"
    a: "Không, trừ khi bạn đủ người. Nó đắt, và nó vẫn không thay được một cổng chạy đúng đường load thật."
---

<TLDR>Một tính năng chết hẳn trong sản phẩm trong khi 178 test xanh và coverage 100%. Test đo dòng đã chạy, không đo đường đã ship.</TLDR>

## Chuyện có thật, và nó có số

Tôi để dành câu chuyện này tới cuối series, vì nó là thứ tôi muốn bạn nhớ nhất.

Một harness tôi đọc có thư mục hậu kiểm. Bài đầu tiên kể chuyện: server ACP crash ngay khi một editor thật kết nối vào. Không phải hỏng một phần — hỏng hẳn. Hai RPC đầu tiên mà editor gọi đều trả lỗi.

Nguyên văn:

<Quote cite="dsh, docs/postmortem/0001-acp-default-export-drops-inject.md:13">The bridge was completely non-functional in production despite 178 green unit tests and 100% line coverage.</Quote>

**178 test xanh. Coverage 100%. Tính năng không chạy một giây nào.**

Đọc câu đó lần đầu, tôi nghĩ chắc có gì gian lận — test giả, hoặc coverage đo sai. Không phải. Mọi thứ đều thật. Test thật, chạy thật, xanh thật.

Vấn đề nằm ở chỗ khác, và tài liệu nói thẳng:

<Quote cite="dsh, docs/postmortem/0001-acp-default-export-drops-inject.md">Every test mounted the plugin through a path that did not exercise how it actually loads or how its services actually resolve.</Quote>

Mọi test đều gắn plugin bằng một **đường khác** với đường nó thực sự được nạp.

## Hai đường, một cái tên

Đây là cơ chế, và nó đáng đọc chậm.

Ứng dụng nạp plugin qua một bộ nạp thật. Bộ nạp đó gọi một hàm xử lý export trước khi dùng. Bộ test trong bộ nhớ thì không. Nó tự dựng đối tượng plugin bằng tay, và tự điền sẵn trường mà bộ nạp thật sẽ điền.

```ts title="cách bộ test gắn plugin — khác với cách sản phẩm gắn"
ctx.plugin({ name, inject, apply })
// `inject` được điền tay ở đây.
// Hàm xử lý export chỉ được bộ nạp gọi, không bao giờ được ctx.plugin gọi.
```

Lỗi trong sản phẩm nằm ở chỗ **bộ nạp** xử lý export. Bộ test đi vòng qua đúng chỗ đó. Nên nó không bao giờ chạm tới lỗi — dù nó chạy qua mọi dòng code xung quanh.

Tài liệu còn nói thêm một chi tiết làm tôi dừng lại: kể cả gọi `ctx.plugin` với namespace import cũng không bắt được lỗi. Vì đường đi vẫn khác.

Và lỗi thứ hai cũng cùng một kiểu. Một lần tra cứu service đi qua một ranh giới bóng. Bộ test gắn mọi thứ phẳng trên một context gốc, nên không bao giờ đi qua ranh giới đó.

Hai lỗi độc lập. Cùng một lý do lọt.

## Bài e2e duy nhất, và cái khoá

Đến đây bạn sẽ hỏi: thế không có bài test đầu-cuối nào à?

Có. Hai bài, và cả hai đều không cứu được.

Bài thứ nhất gửi một lệnh khởi tạo rồi kiểm tra stdout sạch. Nghe hợp lý. Nhưng lệnh đó **không bao giờ chạm tới nhà máy** nơi lỗi nằm. Nó đi qua một nhánh khác.

Bài thứ hai mới là bài chạm đúng chỗ. Và nó bị **khoá sau API key** — nên CI, vốn không có key, bỏ qua nó.

Còn khi chạy ở máy local thì nó "xanh". Tài liệu giải thích vì sao, và đây là chi tiết tôi thấy rùng mình:

<Quote cite="dsh, docs/postmortem/0001-acp-default-export-drops-inject.md">The only test that drove session/new/session/load was key-gated, so CI (no key) skipped it — and locally it "passed" only because a stale built lib/ (with the old code) happened to satisfy module resolution.</Quote>

Một thư mục `lib/` cũ còn sót lại. Nó thoả mãn việc phân giải module. Nên bài test chạy trên **code cũ**, và code cũ thì không có lỗi.

Bạn thấy cái bẫy chưa: test xanh vì nó đang test một phiên bản khác với phiên bản bạn sắp ship.

## Câu chốt

Rồi tài liệu nói câu mà tôi muốn đóng khung:

<Quote cite="dsh, docs/postmortem/0001-acp-default-export-drops-inject.md">100% line coverage was satisfied the whole time. Coverage proves lines ran; it says nothing about whether the feature works the way it ships.</Quote>

Coverage chứng minh các dòng **đã chạy**. Nó không nói gì về việc tính năng có hoạt động **theo đúng cách nó được ship** hay không.

Suốt thời gian đó, con số 100% vẫn đúng. Không có gì gian lận. Chỉ là con số đó trả lời một câu hỏi khác với câu hỏi bạn tưởng nó trả lời.

```diagram
type: architecture
title: Hai đường nạp plugin song song
desc: Đường test tự điền trường tạo ảo tưởng xanh; đường ship thật qua bộ nạp dẫn tới lỗi runtime
nodes:
  - id: testpath
    label: Đường test (ctx.plugin)
    sub: Tự điền inject bằng tay
  - id: shippath
    label: Đường ship thật
    sub: Bộ nạp xử lý export mặc định
    kind: security
    focal: true
  - id: green
    label: 178 test xanh (100%)
    sub: Ảo tưởng an toàn
  - id: dead
    label: Crash trong sản phẩm
    sub: Không tìm thấy inject lúc chạy
    focal: true
edges:
  - [testpath, green]
  - [shippath, dead]
```

## Họ sửa bằng gì

Phần này mới là phần đáng bắt chước, không phải phần kể lỗi.

Họ không viết thêm test cho hàm bị lỗi. Họ sửa **đường đi của bài test**:

- Một bài e2e mới chạy `session/new` qua **stdio thật**, khởi động cả profile như một tiến trình con qua bộ nạp thật.
- Bài đó **không cần API key**, nên CI chạy nó thay vì bỏ qua.
- Và họ chỉ cho tiến trình con tới đúng file cấu hình ở gốc repo, để việc phân giải module không phụ thuộc thư mục làm việc — nhờ đó bài test chạy trên **mã nguồn**, không phải trên một bản build có thể đã cũ.

Rồi bước cuối, và đây là bước tôi muốn bạn nhớ:

<Quote cite="dsh, docs/postmortem/0001-acp-default-export-drops-inject.md">This fails loudly on Bug #1 with no API key. Verified it fails when export default apply is restored.</Quote>

Họ **gieo lại lỗi cũ** và xác nhận bài test mới đỏ lên.

Một test chưa từng đỏ là một test chưa chứng minh được gì. Nó chỉ là một dòng code xanh.

## Cái giá của kỷ luật

Harness đó giữ kỷ luật ở mức tôi chưa thấy ở đâu khác.

Cổng coverage là **100% mỗi file** — không phải 100% toàn repo. Nghĩa là một file lớn được test kỹ không bù được cho một file nhỏ không ai test. Comment trong file cấu hình nói gọn: *"100% or it doesn't merge."*

Và họ giữ lại cả những quyết định đã bị loại. Đếm ở đúng commit tôi đọc:

| Loại ghi chép | Số lượng |
|---|---|
| Đã triển khai | 522 |
| Đang đề xuất | 40 |
| **Bị từ chối** | **14** |
| Đã lưu trữ | 642 |
| **Tổng** | **1.218** |

Mười bốn quyết định bị từ chối vẫn được viết ra và lưu lại. Cộng thêm 4 bài hậu kiểm và 69 script kiểm tra.

Đó là một mức kỷ luật mà repo nhỏ không kham nổi. Và đây là chỗ tôi phải nói thẳng: **bạn không nên bắt chước nó.**

Một mình bạn viết blog và dựng một harness nhỏ thì 100% mỗi file là bất khả thi. Thứ đáng bắt chước không phải con số, mà là **hướng của cổng**: cổng phải chạy đúng đường mà sản phẩm chạy.

## Nối lại cả series

Mười bài trước đều là về cơ chế. Bài này là về chỗ cơ chế đó có thể đúng mà vẫn vô dụng.

Nhớ lại bài 1: model là một hàm không nhớ gì. Mọi thứ nó biết đến từ ngữ cảnh bạn dựng. Nếu đường dựng ngữ cảnh có lỗi mà test không chạm tới, model sẽ trả lời sai một cách tự tin.

Nhớ lại bài 4: vòng lặp chạy được cho tới lúc không. Bốn nhánh vỡ. Một cổng test chỉ đi qua nhánh thành công sẽ không bao giờ thấy ba nhánh còn lại.

Và đây là điều nối chúng lại. Mọi mảnh trong series này đều có một **đường sản phẩm** và một **đường test**. Sổ ghi, đường ống tool, cắt ngữ cảnh, quyền — mảnh nào cũng vậy.

Hai đường đó giống nhau tới mức bạn tưởng chúng là một.

Chúng không phải một. Và khoảng cách giữa chúng là chỗ lỗi sống.

<Callout type="warn" title="Test xanh là điều kiện cần, chưa bao giờ là điều kiện đủ">
Một bộ test xanh nói rằng code bạn viết đã chạy đúng như bạn mô tả. Nó không nói rằng code đó được nạp theo cách sản phẩm nạp, hay được gọi theo cách sản phẩm gọi. Muốn biết điều thứ hai, bạn phải chạy đúng đường đó.
</Callout>

## Làm được ngay

<Steps>
<Step title="Hỏi mỗi test đang đi đường nào">Với từng bài test, hỏi: nó dựng đối tượng bằng tay hay để hệ thống thật dựng? Nếu là tay, nó không test đường nạp.</Step>
<Step title="Đừng để bài e2e quan trọng nhất bị khoá sau bí mật">Một bài test cần API key là một bài CI sẽ bỏ qua. Nếu nó là bài duy nhất chạm đường thật, bạn đang không có cổng nào.</Step>
<Step title="Ghim việc phân giải module vào gốc repo">Để nó không phụ thuộc thư mục làm việc. Nếu không, bài test có thể chạy trên một bản build cũ và xanh vì lý do sai.</Step>
<Step title="Gieo lại lỗi để chứng minh test bắt được nó">Sửa xong, khôi phục lỗi cũ và xem test có đỏ. Một test chưa từng đỏ chưa chứng minh được gì.</Step>
</Steps>

## Nguyên tắc

Cả series này nói về việc dựng đúng cơ chế. Bài cuối nói về việc chứng minh cơ chế đó chạy — và đó là việc khó hơn.

<Quote cite="tusgino">Test xanh là điều kiện cần. Nó chưa bao giờ là điều kiện đủ. Một test chưa từng đỏ là một test bạn chưa kiểm chứng được gì.</Quote>

<KeyTakeaways>
- 178 test xanh và coverage 100% vẫn không cứu được một tính năng chết hẳn trong sản phẩm.
- Mọi test gắn plugin bằng tay, đi vòng qua đúng chỗ bộ nạp thật xử lý — nên không bao giờ chạm tới lỗi.
- Bài e2e duy nhất chạm đường thật bị khoá sau API key; CI bỏ qua, còn local "xanh" nhờ một thư mục build cũ.
- Cách sửa đúng là sửa đường đi của bài test, rồi gieo lại lỗi để chứng minh nó đỏ lên.
- Cổng 100% mỗi file cùng 1.218 ghi chép quyết định là kỷ luật đáng nể — và không bắt chước được nếu bạn làm một mình.
</KeyTakeaways>
