Skip to content

Commit 9cfbebf

Browse files
committed
Record the WebRTC decision, the new floor, and where the last five points are
Progress.md's open decision — install the extra in CI, or write those 2,090 statements off as dead weight in the denominator — is closed with what it was waiting on measured, and replaced by the roadmap the decision unblocks: which files are still dark on *every* square, which of those can move the floor (only the portable ones do), and which two must not be touched until CI installs their extra, because tests written there would skip and move nothing. It also corrects a trap of its own making. The entry said the floor "had to be dug out of the XML artifact"; `coverage report` is what enforces `fail_under` and it includes branches, while Cobertura's `line-rate` does not — about 1.8 points apart on the same square. A floor set from the artifact is one the suite cannot clear.
1 parent b0d6d5f commit 9cfbebf

2 files changed

Lines changed: 198 additions & 24 deletions

File tree

‎Progress.md‎

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,12 @@ capability enum 值與 variadic `ei_seat_bind_capabilities`、event-type enum
228228
2026-08-21 把**機制**補上了(做法見 [WHATS_NEW.md](WHATS_NEW.md)):
229229
型別契約的豁免清單 2026-08-22 清空,平台縫最後兩個名稱(`keyboard`/`mouse`)
230230
2026-08-23 拿到合約;覆蓋率那半發現不是爬得不夠,是量測起點錯了,修正後地板
231-
從 50 提到 69。
231+
從 50 提到 69,同日再提到 74。
232232

233233
**2026-08-23 維護者拍板:下一個覆蓋率目標是 80。** 這一條留到那時候。
234234
型別那一節留著是因為它記的那幾個坑之後還會踩到。
235235

236-
### 覆蓋率:地板 69,下一站 80
236+
### 覆蓋率:地板 74,下一站 80
237237

238238
`TODO` — 目標 **80**(2026-08-23 拍板)。地板本身照舊只跟著實測的最低那一格走。
239239

@@ -270,10 +270,17 @@ import 期程式碼(`def` 行、類別本體、常數、兩張大分派表)
270270

271271
地板因此設成 **69**——取最低那一格往下取整,與當初 50 取自 50.26% 是同一個慣例。
272272
`[tool.coverage.report]` 的 `precision` 也從預設的 0 提到 2:預設精度下九格全部印
273-
「70%」,而它們其實是 69.67 到 70.97,害得這次的地板得去 XML artifact 裡撈。
273+
「70%」,而它們其實是 69.67 到 70.97,分不出最低的是哪一格。
274274
順帶把 `fail_under` 的容差從一整個百分點縮到 0.01。
275275
地板只有一個家(`pyproject.toml` 的 `fail_under`),`quality.yml` 不再另外抄一份。
276276

277+
**這一段原本寫「地板得去 XML artifact 裡撈」,那是錯的,2026-08-23 實測更正。**
278+
上面那九個數字是 `coverage report` 印的,也就是 `fail_under` 真正比對的那個數字,
279+
而它**含分支**(`[tool.coverage.run]` 的 `branch = true`);`coverage.xml` 的
280+
`line-rate` 屬性**不含分支**,所以同一格會高出約 1.8 點——a585e65 那一格
281+
report 印 69.67%,artifact 是 71.07%。照 artifact 設地板,設出來的會是這套測試
282+
過不了的地板。要看單一子系統的缺口用 artifact,要設地板只能用 report。
283+
277284
Windows 是高的那一角,因為門面 import 進來的是**它自己那個平台的後端**;
278285
換句話說剩下的那 30 點裡,有一部分是任何單一平台都拿不到的。
279286

@@ -307,32 +314,97 @@ MCP adapter 與 372 個執行器 adapter 可以在沒有滑鼠、沒有螢幕、
307314
機器)。三個 MCP adapter 需要的比合約承諾的更多,用名字列在測試檔裡,各附一行
308315
理由;**其中任何一個哪天開始通過,測試會要求把它刪掉**,清單不會爛在那裡。
309316

310-
#### 剩下的在哪裡(2026-08-23 實測,本機 Windows/3.14)
317+
#### 2026-08-23 拍板:`quality.yml` 裝 `[webrtc]` extra
318+
319+
原本這裡是一條 `DECIDE`,寫的是「要嘛讓 CI 裝那個 extra,要嘛承認那 2,900 個
320+
statement 是分母裡的死重」。**維護者選了裝**,理由不是那一點覆蓋率:
321+
322+
`utils/remote_desktop` 底下有 **11 個模組在沒有 `aiortc`/`av` 時於模組層拋
323+
ImportError**,共 2,090 個 statement——在 CI 上是硬性的 0%,寫什麼測試都動不了。
324+
比數字嚴重的是另一件事:**涵蓋 WebRTC host 的 auth、TLS、resume token、檔案傳輸的
325+
測試早就寫好了**,只是每一格都 `importorskip` 略過,等於只在開發機上跑過。
326+
327+
只換這一個變數實測(同一台機器、同一套測試):那 2,090 個裡有 **513 個**是現有測試
328+
就會蓋到的,端到端 +1.23 點(windows-2022/3.14)。相依在九宮格上都解得開
329+
(`aiortc` 1.15.0 + `av` 17.1.0 對 win_amd64/manylinux x86_64/macos-14 arm64 的
330+
3.10 與 3.14 都有 wheel)。`typing-stable-api` **刻意不裝**——那個閘門的判定不能隨
331+
環境浮動,理由在下一節。
332+
333+
`dev_requirements.txt` 與 `CLAUDE.md` 的開發指令也跟著加了那個 extra:少裝它的人
334+
量到的數字會比 CI 執行的地板低約 4 點。
335+
336+
#### 三個註冊表都掃過了,替身再往合約深一層
337+
338+
`test_adapter_registry_sweep.py` 之後又走了三步(做法見 [WHATS_NEW.md](WHATS_NEW.md)):
339+
340+
| 這次多掃到的 | 為什麼原本掃不到 |
341+
| --- | --- |
342+
| 92 個 adapter:被呼叫者回傳專案 dataclass | `_value_for` 只認純量與容器,dataclass 直接被判成「模不出來」。現在從 dataclass **自己的欄位標注**造實例,於是 adapter 的 `.to_dict()` 也一起跑起來 |
343+
| 101 個 adapter:被呼叫者是模組層單例的方法 | 匯入的名字不是 function 而是 `default_observer`/`default_scheduler`/`registry` 這種物件。呼叫**單一**方法就是同一個轉接形狀往下一層,方法的標注就是合約;呼叫兩個以上算編排,仍然排除 |
344+
| 31 條 REST 路由 | `rest_handlers` 是同一形狀的第三個註冊表,而現有的 REST 測試走 HTTP 層,在無頭 runner 上多半只是看著 handler 掉進自己的 `except` 回 500 |
345+
346+
REST 那一支的參數來自 `rest_openapi.build_openapi_spec()`——與 handler 不同檔,
347+
所以掃不出「拿被測程式當答案」的循環;順帶白拿一條契約測試:**路由表與 OpenAPI
348+
文件必須描述同一組 API**,多一條少一條都當場紅。
311349

312-
兩支 sweep 之後的數字(`utils/executor` 與 `utils/mcp_server` 已經含進去了):
350+
三支共用的機器搬進 `test/unit_test/headless/_contract_sweep.py`,各自只留自己的
351+
參數來源。
352+
353+
#### 現在的九宮格(2026-08-23 實測,本 PR 的 run)
354+
355+
| | 最低 | 最高 |
356+
| --- | --- | --- |
357+
| 上一版(PR #486 首輪) | 69.67%(ubuntu-22.04/3.14) | 70.97%(windows-2022/3.12) |
358+
| 這一版 | **74.99%**(ubuntu-22.04/3.14) | 76.19%(windows-2022,3.10–3.13) |
359+
360+
地板隨之從 69 提到 **74**。
361+
362+
#### 剩下的 5 點在哪裡(2026-08-23,ubuntu-22.04/3.14 的 artifact)
363+
364+
以下是 statement 數(artifact 的口徑,不含分支),拿來看缺口分佈:
313365

314366
| 子系統 | 沒蓋到 / 總 statement | 覆蓋率 |
315367
| --- | ---: | ---: |
316-
| `utils/remote_desktop` | 2,923 / 6,622 | 55.9% |
317-
| `utils/accessibility` | 822 / 1,397 | 41.2% |
318-
| `utils/executor` | 797 / 3,906 | 79.6% |
319-
| `utils/mcp_server` | 761 / 4,618 | 83.5% |
320-
| `utils/usb` | 455 / 2,137 | 78.7% |
321-
| `wrapper/window_backends` | 397 / 477 | 16.8% |
368+
| `utils/remote_desktop` | 2,653 / 6,622 | 59.9% |
369+
| `utils/accessibility` | 824 / 1,397 | 41.0% |
370+
| `utils/executor` | 653 / 3,906 | 83.3% |
371+
| `utils/usb` | 573 / 2,137 | 73.2% |
372+
| `utils/mcp_server` | 573 / 4,618 | 87.6% |
373+
| `wrapper/window_backends` | 361 / 477 | 24.3% |
322374
| `utils/hotkey` | 221 / 426 | 48.1% |
323-
| `utils/rest_api` | 220 / 808 | 72.8% |
324-
325-
**一件之前沒量過、但決定 80 到底可不可能的事**:其他平台的程式碼
326-
(X11/Wayland/macOS/Android/iOS)在這一格是 3,291 個 statement,
327-
其中 1,854 個沒被蓋到。就算那些永遠蓋不到,這一格的上限仍有 96%,
328-
所以**80 不是被「單一平台拿不到」卡住的**——純粹是量的問題:
329-
可攜程式碼還有 9,539 個 statement 沒被執行過。
330-
331-
**最大的那一塊有個前提要先決定**:`utils/remote_desktop` 佔了將近三分之一,
332-
但 `quality.yml` 不裝 `[webrtc]` extra,所以 webrtc 那一族在 CI 上**永遠是 0%**,
333-
在那裡補的測試會整批 skip,對地板一個點也不會動。要嘛先讓 CI 裝那個 extra,
334-
要嘛承認那 2,900 個 statement 是分母裡的死重、把 80 算在其餘部分上。
335-
**兩條路都可以,但得先選一條再動手。**
375+
| `utils/rest_api` | 146 / 808 | 81.9% |
376+
377+
**要動地板,只能補在每一格都跑得到的程式碼上。** 地板取的是最低那一格,
378+
所以只在 Windows 跑得到的東西補再多也不會動它。把九格的未覆蓋行取交集,
379+
2026-08-23 量到 **9,697 個 statement 在每一格都沒被執行過**——那就是可攜的缺口,
380+
也是唯一會抬地板的地方。最大的幾塊:
381+
382+
| 檔案 | 每一格都沒蓋到 | 備註 |
383+
| --- | ---: | --- |
384+
| `utils/executor/action_executor.py` | 589 | 掃不到的那批 adapter:被呼叫者是 class(沒有回傳標注可讀)、或 adapter 伸手進兩個模組 |
385+
| `utils/accessibility/backends/windows_backend.py` | 446 | UIA COM,要一套夠像的替身 |
386+
| `utils/remote_desktop/webrtc_viewer.py` | 354 | 現在 import 得到了(17.9%),但沒有測試驅動 |
387+
| `utils/remote_desktop/webrtc_host.py` | 351 | 同上(24.8%) |
388+
| `utils/mcp_server/tools/_handlers.py` | 348 | 同 `action_executor.py` |
389+
| `utils/remote_desktop/signaling_server.py` | 155 | **CI 動不了**:要 `[signaling]` extra(fastapi/uvicorn),沒裝 |
390+
| `utils/remote_desktop/multi_viewer.py` | 146 | |
391+
| `wrapper/window_backends/x11_backend.py` | 133 | 只有 Linux 那兩格跑得到,抬得動地板 |
392+
| `utils/accessibility/backends/linux_backend.py` | 132 | 同上 |
393+
394+
**下一步的順序**(都還沒做):
395+
396+
1. `webrtc_host`/`webrtc_viewer`/`multi_viewer`/`webrtc_files`:`[webrtc]` 裝了
397+
之後這一族才第一次可測,合計 960 個 statement 在每一格都沒跑過。`webrtc_host_auth`
398+
與 `webrtc_stats` 已經照這個做法補完(56 個測試),其餘照抄。
399+
2. 掃不到的那批 adapter:目前卡在「被呼叫者是 class」。要嘛從 class 自己的方法標注
400+
長出一個替身物件,要嘛承認那批不掃——**得先決定,因為前者會讓「跑起來了」和
401+
「驗到了東西」分家**。
402+
3. `utils/accessibility` 與 `wrapper/window_backends` 的 Linux 後端:抬得動地板,
403+
但需要一套 Xlib/AT-SPI 的替身。
404+
405+
`utils/office`(77)與 `signaling_server`(155)**不要碰**:`quality.yml` 沒裝
406+
`[office]`/`[signaling]`,補的測試會整批 skip,對地板一個點都不動。要補之前
407+
先照 `[webrtc]` 的先例把 extra 加進 CI。
336408

337409
### mypy:整包把關,**豁免清單已經清空**
338410

‎WHATS_NEW.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,107 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-08-24)
4+
5+
### A Whole Subsystem Was Being Measured At Zero, And Its Tests Were Skipping
6+
7+
`quality.yml` now installs the `[webrtc]` extra. The coverage number was the
8+
smaller half of the reason. Eleven modules under `utils/remote_desktop` raise
9+
`ImportError` at module level without `aiortc`/`av` — 2,090 statements that were
10+
a hard 0% on every square no matter what anyone wrote — but the part that
11+
mattered is that the tests covering the WebRTC host's auth, TLS, resume tokens
12+
and file transfer *were already written*. They `importorskip`ped straight past on
13+
all nine squares, so they ran on developer machines and nowhere else.
14+
15+
Measured with one variable changed, same machine and same suite: 513 of those
16+
statements are covered by tests that exist today, worth +1.23 points end to end.
17+
The extra resolves on every square (`aiortc` 1.15.0 and `av` 17.1.0 have wheels
18+
for win_amd64, manylinux x86_64 and macos-14 arm64 on both ends of the supported
19+
Python range). `typing-stable-api` deliberately does not get it: that gate's
20+
verdict must not depend on what happens to be installed.
21+
22+
`dev_requirements.txt` and the `CLAUDE.md` setup line carry it too, because a
23+
developer without it measures about 4 points below the floor CI enforces.
24+
25+
### The Stub Grows One Level Deeper, And A Third Registry Gets Swept
26+
27+
Yesterday's sweep replaced each adapter's callee with a value built from its
28+
return annotation. Three things it could not reach, and now does:
29+
30+
**A dataclass return is not the end of the contract, it is one more level of
31+
it.** `Optional[X]` → None and containers → empty containers stopped at the
32+
first `-> HealOutcome`, so 92 adapters sat out. The stub is now an instance
33+
built from the dataclass's own field annotations, which means the adapter's
34+
`.to_dict()` runs too — against the declared shape rather than a mock that
35+
answers everything.
36+
37+
**Some adapters import a singleton, not a function.** `default_observer`,
38+
`default_scheduler`, `registry` — the adapter calls one method on the object.
39+
That is the same wiring shape one indirection along, so the callee is the method
40+
and its annotation is the contract; 101 more adapters. Calling *two* methods
41+
means the adapter orchestrates the object rather than standing in front of one
42+
call, and those stay out.
43+
44+
**The REST route table is the third registry of this shape.** Its handlers'
45+
own module docstring says they are "pure … trivial to unit-test without an HTTP
46+
layer", and the existing REST tests go through the HTTP layer instead — so on a
47+
headless runner most of them reached a handler only to watch it fall into its
48+
own `except` and answer 500. `test_rest_route_sweep.py` calls all 31 routes with
49+
the arguments their own OpenAPI document declares, and asserts what the
50+
dispatcher relies on: `(status, dict)`, a real HTTP code, a payload `json.dumps`
51+
accepts — for a documented request, for the empty one an unhelpful client sends,
52+
and for a body of the wrong shape. It also compares the route table against the
53+
document, so a route nobody describes or a documented route nobody serves is
54+
now a named failure.
55+
56+
The machinery the three sweeps share moved to `test/unit_test/headless/
57+
_contract_sweep.py`. Each keeps its own argument source — a JSON schema, the
58+
Script Builder's field specs, an OpenAPI document — which is what stops a sweep
59+
from passing by restating the code it checks.
60+
61+
Two things fell out of building it. `ac_rrule_next` and its neighbours now
62+
declare `"format": "date-time"` on the properties they parse, which their
63+
descriptions already said in prose and their schemas did not; a client
64+
generating values from the schema alone used to get a `ValueError` out of
65+
`datetime.fromisoformat`. And `AddressBook.set_tags()` cleaned its input with
66+
`str(t).strip()`, so a JSON `null` — what a client sends for an omitted tag —
67+
became a tag literally named `"None"`, which `all_tags()` then listed next to
68+
the real ones.
69+
70+
### The Trust Store And The Auth Boundary Get Tests, Because Now They Can
71+
72+
Four modules decide who may drive this machine unattended, whether the host
73+
answering is the one that answered last time, what the viewer reconnects to, and
74+
how hard the encoder is pushed when the link degrades. All are ordinary Python —
75+
a JSON file, a lock and some arithmetic — and none was imported by any test on
76+
any square, because the subsystem could not be loaded without the extra.
77+
78+
117 tests over the decisions they make in the operator's absence: a trust entry
79+
that must not lose its label on re-add, a store that opens empty rather than
80+
throwing on a truncated file, a fingerprint comparison that survives an SDP
81+
spelling the same certificate in a different case, a token accepted only when it
82+
is an equal string, an IP whitelist that matches by network rather than by
83+
string, a grace period that closes a peer which never authenticated, and five
84+
derived rates that must refuse to invent a number from one sample, a zero
85+
interval, or a counter that went backwards.
86+
87+
The auth host double supplies exactly the attribute list `ViewerAuthMixin`'s own
88+
docstring asks for, so a mixin that starts reaching for something else fails
89+
there rather than leaning on whatever the real host happens to own.
90+
91+
### The Floor Is 74, And The Comment Now Says Which Number That Is
92+
93+
The nine-way matrix runs 74.99% (ubuntu-22.04 / 3.14) to 76.19% (windows-2022),
94+
up from 69.67–70.97, so `fail_under` goes 69 → 74 on the usual convention: floor
95+
of the lowest square.
96+
97+
`Progress.md` had recorded that the floor "had to be dug out of the XML
98+
artifact". That is wrong, and following it would set a floor the suite cannot
99+
clear. `coverage report` — the step that enforces `fail_under` — includes branch
100+
coverage, because `branch = true`; Cobertura's `line-rate` attribute does not.
101+
On the same square and the same run those differ by about 1.8 points (76.14%
102+
against 78.33%). The artifact is the right thing to read for a single
103+
subsystem's gap and the wrong thing to set a floor from.
104+
3105
## What's new (2026-08-23)
4106

5107
### A Thousand Adapters Now Get Called, Because the Type Contract Can Build Their Stubs

0 commit comments

Comments
 (0)