Files
cowork-local/tools/check_dialogs.py
T
NamPDTandClaude Opus 5 b6dee044c9 feat(ui): adapt to the screen and its scaling, not to fixed pixels
Two things differ between machines and only one of them is width: a 4K panel
has more pixels, while a 125%/150% display has the same logical pixels holding
LESS, because every label and margin is taller. Breakpoints written as raw
pixels only hold on the machine they were tuned on.

  * ui_scale() derives a factor from font height (1.0 at the 15px line the
    layouts were measured against) and every narrow-guard threshold is
    multiplied by it, so panes fold when the content is cramped rather than
    when a number is crossed.
  * The window takes a share of the available screen (80% × 85%) with the old
    1180×760 as the floor, instead of opening at that size on any monitor.
  * Moving the window to another screen re-pins the assistant and re-decides
    the fold, since the new screen's work area and scaling may differ.

Found by tools/check_multi_screen.py, which walks 5 window sizes × 3 font
scales:

  * At 150%, Schedule was clipped on 1280 and 1366 screens and the window's
    own minimum grew to 1459px — wider than a 1280 laptop, so the app could
    not fit at all. The cause was not the lanes: the one-line lane-count
    summary in the header reported a sizeHint wide enough to set the minimum
    width of the entire window. It now yields first (its text stays in the
    tooltip); the window minimum drops 1459 → 752 and holds there at every
    scale.

Also: these checkers exited 0xC0000409 from a Qt teardown crash AFTER printing
their verdict. check_probes_bite decides whether a probe caught its mutation by
reading exit codes, so a crash would have counted as "caught" — the round could
have passed while proving nothing. They now flush and os._exit with the real
verdict, and round 5 still catches all six mutations.

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

138 lines
4.8 KiB
Python

"""Check the left-list + right-panel navigation in Settings and Task editor.
Both dialogs are navigated the same way, as the audit page asks: a list of the
real group boxes on the left, one panel shown at a time on the right. So the
test is that picking a row swaps the panel, that the rows match the groups, and
— since this is a rearrangement — that no input control went missing.
Run: python tools/check_dialogs.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 _isolate_home, _load_fonts # noqa: E402
# Every field each dialog must still offer after the move.
SETTINGS_FIELDS = [
"language_combo", "theme_combo", "tray_chk", "notify_chk",
"provider_combo", "prov_base", "prov_key", "prov_model",
"sandbox_pw_edit", "sandbox_unlock_btn", "sandbox_confirm",
"sandbox_block_network", "sec_enabled", "ai_check",
]
TASK_FIELDS = [
"title_edit", "desc_edit", "gen_desc_btn", "priority_combo", "status_combo",
"workspace_combo", "provider_combo", "model_combo", "skill_combo",
"sched_enabled", "run_at_edit", "files_list", "files_add_btn", "links_list",
"links_add_btn", "next_combo", "run_next_combo", "pass_output_chk",
"depends_list", "retry_spin", "timeout_spin", "approval_chk",
]
def check(name, dlg, app, expect_rows, fields):
fails = []
print(f"--- {name} ---")
idx, stack = dlg.section_list, dlg.section_stack
rows = [idx.item(i).text() for i in range(idx.count())]
print(f"muc : {rows}")
if len(rows) != expect_rows:
fails.append(f"{name}: cho {expect_rows} muc, thay {len(rows)}")
if idx.count() != stack.count():
fails.append(f"{name}: {idx.count()} muc nhung {stack.count()} panel")
# Picking a row must swap the panel — and each panel must hold something.
swapped, empty = 0, []
for i in range(idx.count()):
idx.setCurrentRow(i)
for _ in range(3):
app.processEvents()
if stack.currentIndex() == i:
swapped += 1
page = stack.widget(i).widget()
if not page.findChildren(type(page)):
empty.append(rows[i])
print(f"chon muc -> doi panel : {swapped}/{idx.count()}")
if swapped != idx.count():
fails.append(f"{name}: chon muc khong doi panel")
if empty:
fails.append(f"{name}: panel rong {empty}")
missing = [f for f in fields if getattr(dlg, f, None) is None]
print(f"field con nguyen : {len(fields) - len(missing)}/{len(fields)}")
if missing:
fails.append(f"{name}: mat field {missing}")
return fails
def main() -> int:
sandbox = _isolate_home()
from PySide6.QtWidgets import QApplication
app = QApplication([])
_load_fonts()
from cowork_local.config import AppConfig, CONFIG_DIR
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
from cowork_local.i18n import set_language, tr
from cowork_local.state import AppContext
from cowork_local.ui.settings_dialog import SettingsDialog
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
set_language("vi")
ctx = AppContext(AppConfig.load())
fails = []
s = SettingsDialog(ctx)
s.resize(900, 640)
s.show()
app.processEvents()
fails += check("Cai dat", s, app, 5, SETTINGS_FIELDS)
t = TaskEditorDialog(ctx=ctx)
t.resize(900, 640)
t.show()
app.processEvents()
fails += check("Task editor", t, app, 5, TASK_FIELDS)
# Both dialogs must be navigated the SAME way — that is the stated point.
same = (type(s.section_list) is type(t.section_list)
and type(s.section_stack) is type(t.section_stack))
print()
print(f"hai hop thoai cung kieu dieu huong: {same}")
if not same:
fails.append("hai hop thoai dieu huong khac kieu")
for lang in ("vi", "en", "ja"):
set_language(lang)
print(f" {lang}: general={tr('settings.group.general')!r} "
f"basic={tr('schedtask.g_basic')!r}")
set_language("vi")
print()
if fails:
print("*** LOI ***")
for x in fails:
print(" " + x)
return 1
print("KET QUA: hai hop thoai dung danh sach trai + panel phai, khong mat field")
return 0
if __name__ == "__main__":
_rc = main()
# Qt (WebEngine especially) crashes during interpreter teardown with
# 0xC0000409 AFTER the work is done, which would mask the real result —
# and check_probes_bite reads these exit codes to decide whether a probe
# caught its mutation. Leave immediately with the verdict instead.
sys.stdout.flush()
sys.stderr.flush()
os._exit(_rc)