Files
cowork-local/tools/check_controls_alive.py
T
NamPDTandClaude Opus 5 f381fd298a test(ui): five verification rounds, and two things they found
Five rounds, each looking from an angle the previous one cannot:

  1. text     — every "Thay đổi" bullet on the page vs a probe (existing)
  2. geometry — check_layout_geometry.py: reading order of the rail, section
                order down Monitoring, which side each pane is on, and the size
                relationships the design names (hero card, 26px dot)
  3. inventory— check_controls_alive.py: every control in controls.json still
                present in the built app, attributed to its owning CLASS via the
                baseline commit (a file holds several classes, so a per-file
                check reported eight dialog controls as missing)
  4. eyes     — rendered screens read against the wireframes
  5. adversarial — check_probes_bite.py: break one feature at a time and fail if
                the matching check still passes

What rounds 4 and 5 caught, which 1-3 could not:

  * Schedule showed six of seven lanes; the seventh needed a horizontal
    scroll. The design says "giữ đủ 7 lane, thu hẹp cho vừa một màn". Lane
    minimum width 190 → 150, so 7 × 150 + gaps fits a 1280 window. Round 2 now
    measures this instead of relying on someone noticing.

  * check_design_parity ALWAYS returned 0. It was a report, not a check: every
    probe in it was incapable of failing, so a regression would print on screen
    and still exit green. It now exits non-zero when anything is CHUA — which
    is what let round 5 detect the two mutations it had been sleeping through.

Also fixed in the harness itself: it rewrote line endings while restoring
mutated files (read_text/write_text translate both ways), and it compared the
tree against "clean" rather than against its own starting state.

All ten checkers green by exit code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:55:47 +09:00

203 lines
7.3 KiB
Python

"""Round 3: is every control in the inventory still in the built app?
docs/screens/controls.json was extracted from the source by AST before the
redesign started. Rounds 1 and 2 ask whether the new shape is right; this one
asks the opposite question — whether rearranging dropped anything.
A control counts as alive if the attribute still exists on its screen's widget
AND is a real QWidget. Ones that were deliberately moved or replaced are listed
in MOVED with where they went, so an intentional change reads differently from
an accidental loss.
Run: python tools/check_controls_alive.py
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO.parent))
sys.path.insert(0, str(Path(__file__).resolve().parent))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
# controls.json lists every control in a FILE, and several files hold more than
# one class (schedule_task_tab.py alone has the tab plus three dialogs). Only
# the screen's own class lives on the widget we can inspect, so each control is
# attributed to its class first, using the file as it was when the inventory
# was taken — that is the baseline commit, not today's line numbers.
BASELINE = "291a611"
def class_ranges(path: str) -> list[tuple[str, int, int]]:
"""(class name, first line, last line) from the file at BASELINE."""
import ast
import subprocess
try:
src = subprocess.run(["git", "show", f"{BASELINE}:{path}"],
cwd=REPO, capture_output=True, text=True,
encoding="utf-8", check=True).stdout
except Exception: # noqa: BLE001
return []
try:
tree = ast.parse(src)
except SyntaxError:
return []
return [(n.name, n.lineno, max(getattr(x, "lineno", n.lineno)
for x in ast.walk(n)))
for n in tree.body if isinstance(n, ast.ClassDef)]
def owning_class(ranges, line: int) -> str:
for name, start, end in ranges:
if start <= line <= end:
return name
return ""
# Controls that are gone ON PURPOSE, with what replaced them. Anything missing
# and NOT listed here is a regression.
MOVED = {
"ui\\help_agent_widget.py": {
"self.collapse_btn": "→ mục 'Ẩn trợ lý' trong menu ⋯ của panel",
},
"ui\\schedule_task_tab.py": {
"self.view_combo": "→ cặp tab Kanban | Lịch (view_tabs)",
},
"ui\\folder_tab.py": {
"self.path_edit": "→ tiêu đề màn (path_lbl)",
},
"ui\\structure_graph_view.py": {
"self._msgs_toggle_btn": "→ cặp tab Đồ thị | Tin nhắn (view_tabs)",
},
"app.py": {
"self.settings_btn": "→ nút Cài đặt ở đáy rail (_nav_settings_btn)",
},
}
# The class whose controls each owner widget actually holds.
MAIN_CLASS = {
"app.py": "MainWindow",
"ui\\workspace_tab.py": "WorkspaceTab",
"ui\\cowork_tab.py": "CoworkTab",
"ui\\chat_panel.py": "ChatPanel",
"ui\\composer.py": "Composer",
"ui\\sidebar.py": "HistorySidebar",
"ui\\co4e_tab.py": "Co4ETab",
"ui\\folder_tab.py": "FolderTab",
"ui\\structure_graph_view.py": "StructureGraphView",
"ui\\schedule_task_tab.py": "ScheduleTaskTab",
"ui\\dashboard_tab.py": "DashboardTab",
"ui\\monitoring_tab.py": "MonitoringTab",
"ui\\help_agent_widget.py": "HelpAgentWidget",
}
# Which built widget owns each source file's controls.
def owners(win):
ws = win.workspace
import cowork_local.ui.co4e_tab as co4e_mod
return {
"app.py": win,
"ui\\workspace_tab.py": ws,
"ui\\cowork_tab.py": ws._cowork,
"ui\\chat_panel.py": ws._cowork,
"ui\\composer.py": ws._cowork.composer,
"ui\\sidebar.py": win.sidebar,
"ui\\co4e_tab.py": win.findChildren(co4e_mod.Co4ETab)[0],
"ui\\folder_tab.py": ws.tabs.widget(ws._folder_tab_idx),
"ui\\structure_graph_view.py": ws.tabs.widget(ws._graphrag_tab_idx),
"ui\\schedule_task_tab.py": win._page_widgets[win._ROW_SCHEDULE],
"ui\\dashboard_tab.py": win._page_widgets[win._ROW_DASHBOARD],
"ui\\monitoring_tab.py": win._page_widgets[win._ROW_MONITORING],
"ui\\help_agent_widget.py": win.help_agent,
}
def main() -> int:
sandbox = _isolate_home()
from PySide6.QtWidgets import QApplication
app = QApplication([])
_load_fonts()
_freeze_schedulers()
from cowork_local.config import AppConfig, CONFIG_DIR
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
from seed_demo_data import seed
seed()
from cowork_local.app import MainWindow
from cowork_local.i18n import set_language
from cowork_local.state import AppContext
set_language("vi")
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
win.resize(1600, 950)
win.show()
# Build every lazy page before looking for its controls.
for row in (win._ROW_DASHBOARD, win._ROW_SCHEDULE, win._ROW_MONITORING):
win._goto(row, None)
for _ in range(6):
app.processEvents()
for sub in range(win.workspace.tabs.count()):
win._goto(win._ROW_WORKSPACE, sub)
for _ in range(6):
app.processEvents()
index = json.loads((REPO / "docs" / "screens" / "controls.json")
.read_text(encoding="utf-8"))
own = owners(win)
alive = dead = moved = skipped = other_class = 0
losses: list[tuple[str, str, str]] = []
for rec in index:
holder = own.get(rec["file"])
if holder is None:
skipped += len(rec["controls"])
continue
ranges = class_ranges(rec["file"].replace("\\", "/"))
want = MAIN_CLASS.get(rec["file"], "")
for c in rec["controls"]:
var = c["var"]
if not var.startswith("self."):
skipped += 1
continue
# Belongs to a dialog defined in the same file → not on this widget.
if ranges and want and owning_class(ranges, c["line"]) != want:
other_class += 1
continue
name = var.split(".", 1)[1]
if getattr(holder, name, None) is not None:
alive += 1
elif var in MOVED.get(rec["file"], {}):
moved += 1
else:
dead += 1
losses.append((rec["file"], var,
c.get("label_vi") or c.get("label") or "?"))
print(f"control con song : {alive}")
print(f"co y doi cho : {moved}")
for f, v, w in [(f, v, MOVED[f][v]) for f in MOVED for v in MOVED[f]]:
print(f" {v:26} {w}")
print(f"thuoc class khac : {other_class} (hop thoai dinh nghia cung file)")
print(f"khong kiem duoc : {skipped} (dialog dung rieng, bien cuc bo)")
print(f"MAT : {dead}")
for f, var, label in losses:
print(f" ! {f}: {var} ({label})")
print()
if dead:
print("*** VONG 3 THAT BAI: co control bien mat ***")
return 1
print("KET QUA VONG 3: khong control nao bien mat ngoai y muon")
return 0
if __name__ == "__main__":
raise SystemExit(main())