CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
221 lines
9.4 KiB
Python
221 lines
9.4 KiB
Python
"""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, the dot).
|
|
|
|
Run: python tools/check_layout_geometry.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
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 ( # noqa: E402
|
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts, control,
|
|
)
|
|
|
|
# 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, in the order the wireframe stacks it: what it cost →
|
|
# what the machine is doing → what the agent may touch → per-model prices →
|
|
# what actually happened.
|
|
MON_ORDER = ["ov_usage_group", "ov_resource_group", "ov_sandbox_details_group",
|
|
"ov_pricing_group", "ov_activity_group", "ov_audit_group"]
|
|
|
|
|
|
def main() -> int:
|
|
sandbox = _isolate_home()
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
app = QApplication([])
|
|
_load_fonts()
|
|
_freeze_schedulers()
|
|
|
|
_apply_theme(app) # measure the styled window, not a bare one
|
|
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
|
|
# R08-T11 moved the Kanban lanes onto KanbanBoardWidget; the shell only
|
|
# holds the header and the view switch.
|
|
lanes = list(control(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]
|
|
# R08-T13 moved the stat cards onto TokenUsageCardWidget.
|
|
hero, small = control(dash, "card_cost"), control(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(control(dash, "card_total"), dash)
|
|
row2 = top_of(control(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, and is the declared size --
|
|
win._goto(win._ROW_WORKSPACE, ws._cowork_tab_idx)
|
|
for _ in range(8):
|
|
app.processEvents()
|
|
dock = win.help_agent
|
|
comp = control(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}")
|
|
# Reading _DOT and comparing against it makes this unfailable — change the
|
|
# constant and the expectation moves with it (check_probes_bite caught
|
|
# exactly that). Bound what the design actually claims instead: a square
|
|
# chip, big enough to hit, far smaller than the 84x64 button it replaced.
|
|
# 26px was drawn, 52px is what the user asked for; 64 is the ceiling past
|
|
# which "gọn" stops being true.
|
|
if not 24 <= dock.width() <= 64 or dock.width() != dock.height():
|
|
fails.append(f"cham tro ly {dock.width()}x{dock.height()}px, "
|
|
f"cho o khoang 24..64 va phai vuong")
|
|
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]
|
|
c4_split = control(c4, "_split")
|
|
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 (control(c4, "_sections") or {}).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__":
|
|
_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)
|