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>
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
"""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())
|
||||||
@@ -346,7 +346,10 @@ def main() -> int:
|
|||||||
print(f" OK = dung thiet ke moi ({counts['OK']}/{len(R)})")
|
print(f" OK = dung thiet ke moi ({counts['OK']}/{len(R)})")
|
||||||
print(f" KHAC = co y lam khac, da ghi ly do")
|
print(f" KHAC = co y lam khac, da ghi ly do")
|
||||||
print(f" CHUA = chua lam")
|
print(f" CHUA = chua lam")
|
||||||
return 0
|
# This used to return 0 unconditionally — a report, not a check. Every probe
|
||||||
|
# in it was therefore unable to fail, so a regression would have been shown
|
||||||
|
# on screen and still exited green for any script that only reads the code.
|
||||||
|
return 1 if counts.get("CHUA") else 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""Round 2: does the built layout have the SHAPE the wireframes draw?
|
||||||
|
|
||||||
|
Round 1 asks "does the feature exist". A screen can pass that and still be laid
|
||||||
|
out wrongly — right widgets, wrong order, wrong side, wrong proportions. This
|
||||||
|
round measures real geometry against what the audit page's wireframes depict:
|
||||||
|
reading order of the rail, section order down Monitoring, which side each pane
|
||||||
|
is on, and the size relationships the design calls out (hero card, 26px dot).
|
||||||
|
|
||||||
|
Run: python tools/check_layout_geometry.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# The rail, top to bottom, as the audit page's rail() helper draws it.
|
||||||
|
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
|
||||||
|
RAIL_BOTTOM = ["Dashboard", "Giám sát"]
|
||||||
|
# Monitoring ▸ Tổng quan, as the wireframe stacks it.
|
||||||
|
MON_ORDER = ["ov_usage_group", "ov_activity_group", "ov_resource_group",
|
||||||
|
"ov_sandbox_details_group", "ov_pricing_group", "ov_audit_group"]
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
ws = win.workspace
|
||||||
|
fails: list[str] = []
|
||||||
|
|
||||||
|
def top_of(w, ref):
|
||||||
|
return w.mapTo(ref, w.rect().topLeft()).y()
|
||||||
|
|
||||||
|
def left_of(w, ref):
|
||||||
|
return w.mapTo(ref, w.rect().topLeft()).x()
|
||||||
|
|
||||||
|
# --- 1. rail: reading order, and the rail is on the LEFT ---------------
|
||||||
|
rows = [win.nav.topLevelItem(i).text(0) for i in range(win.nav.topLevelItemCount())]
|
||||||
|
bottom = [win.nav_bottom.topLevelItem(i).text(0)
|
||||||
|
for i in range(win.nav_bottom.topLevelItemCount())]
|
||||||
|
print(f"thanh menu : {rows}")
|
||||||
|
print(f"nhom day : {bottom}")
|
||||||
|
if rows != RAIL_ORDER:
|
||||||
|
fails.append(f"thu tu thanh menu lech: {rows} != {RAIL_ORDER}")
|
||||||
|
if bottom != RAIL_BOTTOM:
|
||||||
|
fails.append(f"thu tu nhom day lech: {bottom} != {RAIL_BOTTOM}")
|
||||||
|
rail_x = left_of(win._nav_wrap, win)
|
||||||
|
content_x = left_of(win.pages, win)
|
||||||
|
print(f"rail x={rail_x} · noi dung x={content_x}")
|
||||||
|
if rail_x >= content_x:
|
||||||
|
fails.append("rail khong nam ben trai noi dung")
|
||||||
|
|
||||||
|
# --- 2. rail header order: picker ABOVE the new-chat button ------------
|
||||||
|
py, by = top_of(win.nav_project, win), top_of(win.nav_new_chat, win)
|
||||||
|
ry = top_of(win.nav_recents, win)
|
||||||
|
ay = top_of(win._account_row, win)
|
||||||
|
print(f"bo chon y={py} · nut chat moi y={by} · GAN DAY y={ry} · tai khoan y={ay}")
|
||||||
|
if not (py < by < ry < ay):
|
||||||
|
fails.append("thu tu doc cua rail sai (bo chon → chat moi → GAN DAY → tai khoan)")
|
||||||
|
|
||||||
|
# --- 3. Monitoring: one column, sections in the drawn order ------------
|
||||||
|
win._goto(win._ROW_MONITORING, None)
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
mon = win._page_widgets[win._ROW_MONITORING]
|
||||||
|
tops = [(n, top_of(getattr(mon, n), mon)) for n in MON_ORDER if getattr(mon, n, None)]
|
||||||
|
lefts = {n: left_of(getattr(mon, n), mon) for n, _y in tops}
|
||||||
|
print("Monitoring, tu tren xuong:")
|
||||||
|
for n, y in tops:
|
||||||
|
print(f" {n:28} y={y:5} x={lefts[n]}")
|
||||||
|
if [n for n, _ in sorted(tops, key=lambda t: t[1])] != [n for n, _ in tops]:
|
||||||
|
fails.append("thu tu muc trong Monitoring khong khop ban ve")
|
||||||
|
# Sandbox and Permissions share a row; everything else is full width.
|
||||||
|
perm_y = top_of(mon.ov_permissions_group, mon)
|
||||||
|
sbx_y = top_of(mon.ov_sandbox_details_group, mon)
|
||||||
|
same_row = abs(perm_y - sbx_y) < 20
|
||||||
|
print(f"Sandbox | Quyen cung hang: {same_row}")
|
||||||
|
if not same_row:
|
||||||
|
fails.append("Sandbox va Quyen khong cung mot hang")
|
||||||
|
price_w = mon.ov_pricing_group.width()
|
||||||
|
res_w = mon.ov_resource_group.width()
|
||||||
|
print(f"bang gia rong {price_w}px · tai nguyen {res_w}px (deu tron be ngang)")
|
||||||
|
if price_w < res_w * 0.95:
|
||||||
|
fails.append("bang gia model khong chiem tron be ngang")
|
||||||
|
|
||||||
|
# --- 3b. Schedule: all seven lanes on screen, no horizontal scroll -----
|
||||||
|
win._goto(win._ROW_SCHEDULE, None)
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
sched = win._page_widgets[win._ROW_SCHEDULE]
|
||||||
|
from PySide6.QtWidgets import QScrollArea
|
||||||
|
lanes = list(sched.columns.values())
|
||||||
|
# The page holds more than one scroll area — take the one the lanes live in.
|
||||||
|
board = next(sa for sa in sched.findChildren(QScrollArea)
|
||||||
|
if sa.isAncestorOf(lanes[0]))
|
||||||
|
rightmost = max(left_of(c, board.widget()) + c.width() for c in lanes)
|
||||||
|
fits = rightmost <= board.viewport().width() + 2
|
||||||
|
print(f"Schedule: {len(lanes)} lane · mep phai x={rightmost} · "
|
||||||
|
f"khung rong {board.viewport().width()} · vua mot man = {fits}")
|
||||||
|
if len(lanes) != 7:
|
||||||
|
fails.append(f"chi co {len(lanes)} lane, thiet ke la 7")
|
||||||
|
if not fits:
|
||||||
|
fails.append(f"lane thu 7 nam ngoai man ({rightmost} > "
|
||||||
|
f"{board.viewport().width()}) — phai cuon ngang")
|
||||||
|
|
||||||
|
# --- 4. Dashboard: hero left, taller; supporting tiles in a 2x2 --------
|
||||||
|
win._goto(win._ROW_DASHBOARD, None)
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
dash = win._page_widgets[win._ROW_DASHBOARD]
|
||||||
|
hero, small = dash.card_cost, dash.card_total
|
||||||
|
print(f"the Chi phi : x={left_of(hero, dash)} cao={hero.height()} · "
|
||||||
|
f"the phu x={left_of(small, dash)} cao={small.height()}")
|
||||||
|
if left_of(hero, dash) >= left_of(small, dash):
|
||||||
|
fails.append("the Chi phi khong nam ben trai cac the phu")
|
||||||
|
if hero.height() < small.height() * 1.5:
|
||||||
|
fails.append("the Chi phi khong cao gap ruoi the phu")
|
||||||
|
row1 = top_of(dash.card_total, dash)
|
||||||
|
row2 = top_of(dash.card_out, dash)
|
||||||
|
print(f"the phu hang 1 y={row1} · hang 2 y={row2} (phai la 2 hang)")
|
||||||
|
if row2 <= row1:
|
||||||
|
fails.append("4 the phu khong xep 2x2")
|
||||||
|
|
||||||
|
# --- 5. Cowork: the dot clears the composer, dot is 26px --------------
|
||||||
|
win._goto(win._ROW_WORKSPACE, ws._cowork_tab_idx)
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
dock = win.help_agent
|
||||||
|
comp = ws._cowork.composer
|
||||||
|
dock_bottom = top_of(dock, win) + dock.height()
|
||||||
|
comp_top = top_of(comp, win)
|
||||||
|
print(f"cham {dock.width()}x{dock.height()} · day y={dock_bottom} · o nhap dinh y={comp_top}")
|
||||||
|
if dock.width() > 30:
|
||||||
|
fails.append(f"cham tro ly rong {dock.width()}px, thiet ke la 26px")
|
||||||
|
if dock_bottom > comp_top:
|
||||||
|
fails.append("cham tro ly de len o nhap")
|
||||||
|
if left_of(dock, win) + dock.width() > win.width():
|
||||||
|
fails.append("cham tro ly tran ra ngoai cua so")
|
||||||
|
|
||||||
|
# --- 6. Co4E: sidebar left, canvas middle, config right ---------------
|
||||||
|
win._goto(win._ROW_WORKSPACE, ws._co4e_tab_idx)
|
||||||
|
for _ in range(8):
|
||||||
|
app.processEvents()
|
||||||
|
import cowork_local.ui.co4e_tab as co4e_mod
|
||||||
|
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
||||||
|
xs = [c4._split.widget(i).x() for i in range(c4._split.count())]
|
||||||
|
print(f"Co4E 3 pane x = {xs}")
|
||||||
|
if xs != sorted(xs):
|
||||||
|
fails.append("thu tu 3 pane cua Co4E sai (trai → giua → phai)")
|
||||||
|
heads = [h.text() for h, _b, _s in c4._sections.values()]
|
||||||
|
print(f"cot sidebar: {heads}")
|
||||||
|
if len(heads) != 4:
|
||||||
|
fails.append(f"cot sidebar co {len(heads)} muc, ban ve ve 4")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if fails:
|
||||||
|
print("*** LECH BO CUC ***")
|
||||||
|
for f in fails:
|
||||||
|
print(" " + f)
|
||||||
|
return 1
|
||||||
|
print("KET QUA VONG 2: hinh hoc khop ban ve")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Round 5: do the checks actually bite?
|
||||||
|
|
||||||
|
Rounds 1–4 all report green. That is only worth something if the checks would
|
||||||
|
have turned red had the work not been done. So this round breaks the app on
|
||||||
|
purpose, one feature at a time, and fails if the corresponding check still
|
||||||
|
passes — a check that cannot fail is not evidence.
|
||||||
|
|
||||||
|
Each mutation is applied by monkey-patching the module BEFORE the checker
|
||||||
|
builds its own window, then undone.
|
||||||
|
|
||||||
|
Run: python tools/check_probes_bite.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import runpy
|
||||||
|
import subprocess
|
||||||
|
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")
|
||||||
|
|
||||||
|
# (name, file, find, replace, checker that must FAIL because of it)
|
||||||
|
MUTATIONS = [
|
||||||
|
("bo cham tro ly 26px -> 64px",
|
||||||
|
"ui/help_agent_widget.py", "_DOT = 26", "_DOT = 64",
|
||||||
|
"check_layout_geometry.py"),
|
||||||
|
("tra lane Running ve khong vien",
|
||||||
|
"ui/schedule_task_tab.py",
|
||||||
|
'if status == "running" and counts[status]:',
|
||||||
|
'if False:',
|
||||||
|
"check_design_parity.py"),
|
||||||
|
("bo cot muc luc cua Cai dat",
|
||||||
|
"ui/settings_dialog.py",
|
||||||
|
"self.section_list, self.section_stack = section_panels(pages)",
|
||||||
|
"self.section_list, self.section_stack = section_panels(pages[:1])",
|
||||||
|
"check_dialogs.py"),
|
||||||
|
("noi lai dai tab flow Co4E",
|
||||||
|
"ui/co4e_tab.py",
|
||||||
|
"self.flow_scroll.setVisible(False)",
|
||||||
|
"self.flow_scroll.setVisible(True)",
|
||||||
|
"check_co4e.py"),
|
||||||
|
("bo dong 'Tat ca project...' khoi GAN DAY",
|
||||||
|
"app.py",
|
||||||
|
'more.setData(0, Qt.UserRole, {"all": True})',
|
||||||
|
'more.setData(0, Qt.UserRole, {})',
|
||||||
|
"check_design_parity.py"),
|
||||||
|
("tra thanh menu ve accordion (bo nhom day)",
|
||||||
|
"app.py",
|
||||||
|
'rows.append((self.nav_bottom, self._ROW_DASHBOARD, None,',
|
||||||
|
'rows.append((self.nav, self._ROW_DASHBOARD, None,',
|
||||||
|
"check_layout_geometry.py"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_checker(script: str) -> int:
|
||||||
|
"""Run a checker in a fresh process; return its exit code."""
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, str(REPO / "tools" / script)],
|
||||||
|
cwd=REPO, capture_output=True, text=True, encoding="utf-8",
|
||||||
|
errors="replace", env={**os.environ, "QT_QPA_PLATFORM": "offscreen",
|
||||||
|
"PYTHONIOENCODING": "utf-8"})
|
||||||
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def tree_state() -> str:
|
||||||
|
return subprocess.run(["git", "status", "--short"], cwd=REPO,
|
||||||
|
capture_output=True, text=True).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
fails: list[str] = []
|
||||||
|
# Compare the tree BEFORE and AFTER, not against a clean tree: work in
|
||||||
|
# progress is legitimately uncommitted, and demanding a clean tree made this
|
||||||
|
# round fail for a reason that has nothing to do with the mutations.
|
||||||
|
before = tree_state()
|
||||||
|
print(f"{'hong gi':44} {'phep do':26} ket qua")
|
||||||
|
print("-" * 88)
|
||||||
|
for name, rel, find, repl, checker in MUTATIONS:
|
||||||
|
path = REPO / rel
|
||||||
|
# newline="" both ways: the default translates on read AND write, so a
|
||||||
|
# LF file came back as CRLF and every mutated file was left "modified"
|
||||||
|
# even after being restored.
|
||||||
|
with io.open(path, "r", encoding="utf-8", newline="") as fh:
|
||||||
|
original = fh.read()
|
||||||
|
if find not in original:
|
||||||
|
fails.append(f"{name}: khong tim thay doan can sua trong {rel}")
|
||||||
|
print(f"{name:44} {checker:26} *** KHONG AP DUNG DUOC ***")
|
||||||
|
continue
|
||||||
|
|
||||||
|
def write(text: str) -> None:
|
||||||
|
with io.open(path, "w", encoding="utf-8", newline="") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
write(original.replace(find, repl, 1))
|
||||||
|
try:
|
||||||
|
code = run_checker(checker)
|
||||||
|
finally:
|
||||||
|
write(original) # always restore
|
||||||
|
bit = code != 0
|
||||||
|
print(f"{name:44} {checker:26} {'BAT DUOC' if bit else '*** KHONG BAT ***'}")
|
||||||
|
if not bit:
|
||||||
|
fails.append(f"{name}: {checker} van bao xanh du da lam hong")
|
||||||
|
|
||||||
|
# Everything must be back exactly as it was before this run.
|
||||||
|
after = tree_state()
|
||||||
|
same = after == before
|
||||||
|
print()
|
||||||
|
print("cay lam viec sau khi thu giong het truoc:", "co" if same else "*** KHAC ***")
|
||||||
|
if not same:
|
||||||
|
print(" truoc:", before.replace("\n", " | ") or "(sach)")
|
||||||
|
print(" sau :", after.replace("\n", " | ") or "(sach)")
|
||||||
|
fails.append("file chua duoc khoi phuc sau khi thu")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if fails:
|
||||||
|
print("*** VONG 5 THAT BAI ***")
|
||||||
|
for f in fails:
|
||||||
|
print(" " + f)
|
||||||
|
return 1
|
||||||
|
print(f"KET QUA VONG 5: ca {len(MUTATIONS)} phep do deu bat duoc loi khi co tinh lam hong")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -53,7 +53,11 @@ class _KanbanColumn(QListWidget):
|
|||||||
# → "Delete N selected" to bulk-remove tasks instead of one at a time.
|
# → "Delete N selected" to bulk-remove tasks instead of one at a time.
|
||||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
self.setWordWrap(True)
|
self.setWordWrap(True)
|
||||||
self.setMinimumWidth(190)
|
# Narrow enough that all SEVEN lanes fit on one screen, which is what
|
||||||
|
# the design asks for — at 190 the seventh (Paused) fell off the right
|
||||||
|
# edge and needed a horizontal scroll to reach.
|
||||||
|
# 7 × 150 + 6 gaps = 1098px, inside the content area of a 1280 window.
|
||||||
|
self.setMinimumWidth(150)
|
||||||
|
|
||||||
def dropEvent(self, event): # noqa: N802
|
def dropEvent(self, event): # noqa: N802
|
||||||
source = event.source()
|
source = event.source()
|
||||||
|
|||||||
Reference in New Issue
Block a user