Files
cowork-local/tests/characterization/test_co4e_runs_page.py
T
lamhv7andClaude Sonnet 5 0631abf85f feat(co4e): tách 6 widget UI khỏi ui/co4e_tab.py sang presentation/co4e/*
Lane N3 (Co4E Studio) — dùng bộ workflow refactor-god-file, mỗi bước có
characterization test trước khi tách, hậu kiểm ranh giới tầng sau mỗi bước:

- skills_list_panel.py / agent_list_panel.py — 2 khu vực sidebar
- co4e_canvas_widget.py + canvas_items.py + canvas_interaction_mixin.py —
  Co4ECanvas tách 3 file (vượt 400 dòng nếu đứng một mình)
- node_property_panel.py + node_property_actions_mixin.py +
  step_config_section.py — StepConfigPanel, cùng lý do
- co4e_run_control_widget.py — RunsPagePanel (trang Flow Status)
- co4e_chat_view.py — ChatPanel + _ChatInput + helper autocomplete
- palette_list.py — _PaletteList dời khỏi ui/co4e_tab.py, hết import ngược
  presentation -> ui (agent/skills panel giờ import top-level)

ui/co4e_tab.py giảm 2089 -> 1878 dòng, chỉ còn phần wiring + business logic
(Co4ERunManager/AgentWorker chưa đổi — nằm ngoài phạm vi này, xem docstring
presentation/co4e/co4e_tab.py). ui/co4e_canvas.py và ui/co4e_config_panel.py
còn lại là compat shim re-export, không đổi API cho bên gọi.

Thêm tests/test_co4e_integration.py — dựng thật Co4ETab qua build_co4e_tab(),
lái luồng qua nhiều panel trong cùng instance (thêm node, mở/gập chat, chuyển
trang Flow Status rồi quay lại không mất state canvas) — bắt lỗi wiring
xuyên-panel mà characterization test từng panel riêng không thấy được.

Đã xác minh: pytest 348 passed/1 skipped, tools/check_co4e.py sạch, không
file nào >400 dòng, domain/application không import PySide6, và so pixel
before/after (git worktree tại HEAD cũ) ra 0/1.125.000 pixel khác biệt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:43:42 +09:00

525 lines
23 KiB
Python

"""Characterization test cho phần DỰNG WIDGET của trang "Runs" trong Co4E
(``ui/co4e_tab.py``, method ``_build_runs_page``, dòng 869-928).
VÒNG ĐỜI: đây là giàn giáo (scaffolding), KHÔNG phải đặc tả cuối cùng. Mục
đích DUY NHẤT là lưới an toàn cho đợt tách "Run Control" sắp tới (xem
``docs/architecture/co4e-split-map-run-control.md`` — dự kiến ``_build_runs_page``
sẽ dời sang ``presentation/co4e/co4e_run_control_widget.py``). Sau khi việc
tách hoàn tất và ổn định, các test ở đây nên được viết lại thành test đặc tả
(specification test) cho widget/API mới; quirk nào liệt kê dưới đây nên có
issue riêng nếu ai đó muốn "dọn" chúng — ĐỪNG tự sửa code sản phẩm để "dọn"
quirk khi đọc thấy test này.
PHẠM VI: CHỈ ``_build_runs_page`` — phần dựng ``QWidget``/``runs_table`` (5
cột) + 7 widget con (``runs_back_btn``, ``runs_title``, ``ws_folder_btn``,
``run_stop_btn``, ``run_rename_btn``, ``run_del_btn``, ``run_clear_btn``) và
việc NỐI (connect) các signal của chúng tới các method xử lý. CÁC METHOD XỬ LÝ
(``_refresh_runs``, ``_stop_selected_run``, ``_delete_selected_run``,
``_runs_context_menu``, ``_rename_selected_run``, ``_open_run_from_table``,
``_open_workspace_folder``, ``_refresh_ws_folder_btn``) VẪN Ở NGUYÊN trên
``Co4ETab`` và KHÔNG được characterize ở đây (đề bài giao đúng phạm vi
constructor cho lượt này) — test này chỉ xác nhận rằng bấm nút/emit signal
tương ứng CÓ gọi tới đúng method trên ``self`` (qua fake/stub), không xác
nhận method đó làm gì bên trong.
CÁCH DỰNG: ``_build_runs_page`` là instance method cần rất nhiều state của
``Co4ETab`` thật (``self.ctx``, ``self.manager``, hàng chục method khác) để
dựng trọn vẹn — dựng cả ``Co4ETab`` chỉ để test 60 dòng constructor này là
tốn kém và kéo theo rủi ro chạm những phần KHÔNG thuộc phạm vi. Nên ở đây
gọi thẳng ``Co4ETab._build_runs_page(fake_self)`` (unbound, theo đúng gợi ý
của đề bài) trên một ``_FakeTab`` tối giản: chỉ có ``ctx``/``_project_dir``/
``manager`` (để ``_flow_output_root``/``_refresh_ws_folder_btn`` — hai
method DÙNG THẬT của ``Co4ETab``, được gọi ngay TRONG lúc dựng ở dòng 893 —
chạy được) và các method xử lý còn lại được thay bằng stub ghi lại số lần gọi
(KHÔNG gọi ``Co4ERunManager.start()``/dựng ``QThread``/``AgentWorker`` thật —
đúng ràng buộc "không spawn thread/gọi provider thật").
AN TOÀN DỮ LIỆU: dù bước dựng widget này tự nó KHÔNG chạm đĩa, việc gọi
``_flow_output_root()``/``_refresh_ws_folder_btn()`` NGAY trong lúc dựng lại
tính ``CONFIG_DIR`` (qua ``cowork_local.config.CONFIG_DIR = Path.home() /
".cowork_local"``, xuyên qua ``core.co4e.CO4E_DIR``) — nên toàn bộ probe chạy
trong TIẾN TRÌNH CON riêng (tránh xung đột ``QApplication`` singleton với các
test khác trong cùng lượt chạy pytest) với ``HOME``/``USERPROFILE`` trỏ vào
một ``tmp_path`` sandbox ĐẶT TRƯỚC khi script import bất kỳ thứ gì thuộc
``cowork_local`` (đúng kỹ thuật ``tools/capture_screens.py::_isolate_home()``:
đặt ``USERPROFILE``/``HOME``, xoá ``HOMEDRIVE``/``HOMEPATH``), và
``QT_QPA_PLATFORM=offscreen`` được đặt TRƯỚC khi import PySide6 (đúng khuôn
``tools/check_co4e.py`` dòng 22+40-42). Một assert ngay trong tiến trình con
chốt ``CONFIG_DIR`` nằm trong sandbox trước khi làm gì khác (kiểu
``tools/check_co4e.py:47``).
CẦN QApplication: ``_build_runs_page`` dựng ``QWidget``/``QTableWidget`` thật,
đọc ``.text()``/``.toolTip()``/``.icon()``/``.cursor()`` và bấm nút thật qua
``.click()`` — không phải kiểu giá trị thuần, nên bắt buộc ``QApplication``
(``offscreen``), khác các test hình học thuần (``test_co4e_canvas_geometry.py``).
CÁCH CHỐT ASSERT: mọi giá trị dưới đây lấy bằng cách CHẠY code thật (script
``_PROBE_SCRIPT`` bên dưới, qua ``.venv/Scripts/python.exe``, giống hệt lệnh ở
cuối file) rồi dán NGUYÊN VĂN JSON in được vào assert — không suy luận lý
thuyết. Các chuỗi hiển thị phụ thuộc ``tr()`` (tiếng Việt mặc định) được so
sánh bằng cách gọi LẠI ``tr()`` thật ngay trong tiến trình test (không cần
QApplication, ``i18n.py`` là tra bảng thuần) thay vì chép tay chuỗi có dấu —
tránh gõ nhầm ký tự Unicode khi transcribe.
QUIRK ĐÃ ĐÓNG ĐINH (xem case tương ứng bên dưới — ĐỪNG "dọn" các chỗ này khi
tách, chúng trông như bất đối xứng khó hiểu nhưng là hành vi đang chạy thật
hôm nay):
* ``run_clear_btn`` là nút DUY NHẤT trong 5 nút hàng thao tác KHÔNG có
``setIcon(...)`` — icon của nó luôn rỗng (``.icon().isNull() is True``),
khác hẳn ``run_stop_btn``/``run_rename_btn``/``run_del_btn``/
``ws_folder_btn`` đều có icon SVG built-in.
* ``run_clear_btn.clicked`` nối THẲNG tới
``lambda: self.manager.clear_finished()`` — gọi trực tiếp method trên
``manager``, KHÔNG đi qua một method riêng trên ``self`` như 3 nút hàng
xóm (``_stop_selected_run``/``_rename_selected_run``/
``_delete_selected_run``) — không có lớp bọc nào để thêm xác nhận
(confirm dialog) sau này mà không sửa trực tiếp dòng connect này.
* ``ws_folder_btn`` text được dựng bằng ``"…/" + "/".join(parts[-2:])``
(đúng 3 ký tự: dấu chấm lửng Unicode U+2026 rồi dấu gạch chéo xuôi) khi
đường dẫn có hơn 2 phần — dùng ``"/"`` LUÔN LUÔN, kể cả trên Windows
(khác separator ``\\`` của phần còn lại của path), và không có fallback
hiển thị full path trừ khi ``len(parts) <= 2``.
* ``_flow_output_root()`` (được ``_refresh_ws_folder_btn()`` gọi NGAY
trong lúc dựng, dòng 893) bọc ``ctx.config.cowork_output_dir()`` trong
``except Exception`` RỘNG — bất kỳ lỗi nào từ ``ctx.config`` (kể cả
``AttributeError`` vì ``ctx`` không có ``.config`` như trong probe này)
đều rơi vào nhánh fallback ``co4e.CO4E_DIR / "runs"`` một cách im lặng,
không log, không báo cho người gọi biết đã fallback.
* ``runs_table`` sau khi dựng xong LUÔN có 0 hàng bất kể ``manager`` đang
có run nào hay không — ``_build_runs_page`` không tự seed dữ liệu, việc
đó thuộc về ``_refresh_runs`` (method riêng, KHÔNG thuộc phạm vi test
này) — ai tách file mà tưởng constructor này "phải" gọi ``_refresh_runs``
luôn thì sẽ đổi hành vi so với hôm nay.
Lệnh thủ công đã dùng để chốt các con số trên (quy trình ngược, xem cuối file
để chạy lại nếu cần chốt lại sau khi code đổi có chủ đích).
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
REPO_PARENT = REPO_ROOT.parent
_PROBE_SCRIPT = r"""
import json
import os
import sys
from pathlib import Path
sandbox = sys.argv[1]
repo_parent = sys.argv[2]
sys.path.insert(0, repo_parent)
os.environ["QT_QPA_PLATFORM"] = "offscreen"
for var in ("USERPROFILE", "HOME"):
os.environ[var] = sandbox
os.environ.pop("HOMEDRIVE", None)
os.environ.pop("HOMEPATH", None)
from PySide6.QtCore import QPoint, Qt
from PySide6.QtWidgets import (
QApplication, QHeaderView, QTableWidget, QTableWidgetItem,
)
app = QApplication([])
from cowork_local.config import CONFIG_DIR
assert str(Path(sandbox).resolve()) in str(CONFIG_DIR.resolve()), (
"khong co lap: CONFIG_DIR=" + str(CONFIG_DIR))
from cowork_local.ui.co4e_tab import Co4ETab
result = {}
calls = {"show_runs": [], "stop": 0, "rename": 0, "delete": 0, "open_run": 0,
"ws_open": 0, "ctx_menu": 0}
class _Manager:
# Stand-in cho Co4ERunManager - KHONG dung QThread/AgentWorker that.
def __init__(self):
self.clear_finished_calls = 0
def clear_finished(self):
self.clear_finished_calls += 1
class _Config:
def cowork_output_dir(self):
# co tinh nem loi de _flow_output_root roi vao nhanh fallback
# (quirk duoc dong dinh trong docstring cua file test).
raise RuntimeError("no config in probe")
class _Ctx:
def __init__(self):
self.config = _Config()
class _FakeTab:
# Chi mang du state de goi duoc Co4ETab._build_runs_page(self) unbound -
# KHONG dung ca Co4ETab that (tranh keo theo sidebar/canvas/config panel
# ngoai pham vi).
# Hai method NAY la method THAT cua Co4ETab, duoc _build_runs_page goi
# NGAY trong luc dung (dong 893) - phai la ban that, khong stub.
_flow_output_root = Co4ETab._flow_output_root
_refresh_ws_folder_btn = Co4ETab._refresh_ws_folder_btn
_build_runs_page = Co4ETab._build_runs_page
def __init__(self):
self.ctx = _Ctx()
self._project_dir = None
self.manager = _Manager()
def _show_runs(self, on):
calls["show_runs"].append(on)
def _stop_selected_run(self):
calls["stop"] += 1
def _rename_selected_run(self):
calls["rename"] += 1
def _delete_selected_run(self):
calls["delete"] += 1
def _open_run_from_table(self, item):
calls["open_run"] += 1
def _open_workspace_folder(self):
calls["ws_open"] += 1
def _runs_context_menu(self, pos):
calls["ctx_menu"] += 1
fake = _FakeTab()
w = fake._build_runs_page()
result["w_type"] = type(w).__name__
result["layout_type"] = type(w.layout()).__name__
result["table_is_child_of_w"] = w.findChild(type(fake.runs_table)) is fake.runs_table
result["col_count"] = fake.runs_table.columnCount()
result["row_count"] = fake.runs_table.rowCount()
result["header_resize_mode_is_stretch"] = (
fake.runs_table.horizontalHeader().sectionResizeMode(0) == QHeaderView.Stretch
)
result["vheader_visible"] = fake.runs_table.verticalHeader().isVisible()
# isVisible() luon False vi 'w' khong bao gio duoc .show() trong probe nay
# (widget khong nam trong mot top-level dang hien thi that) - dung
# isVisibleTo(ancestor) de bat dung co hidden explicit da duoc setVisible(...)
# dat tren verticalHeader, khong phu thuoc chuoi ancestor co duoc show hay
# khong. Xem PySide6 doc QWidget.isVisibleTo(): tra ve True/False dua tren co
# WA_WState_Hidden explicit cua chinh widget do (va cac ancestor tinh den
# truoc 'ancestor'), bat ke ancestor da .show() hay chua.
result["vheader_visible_to_table"] = (
fake.runs_table.verticalHeader().isVisibleTo(fake.runs_table)
)
result["edit_triggers_is_no_edit"] = (
fake.runs_table.editTriggers() == QTableWidget.NoEditTriggers
)
result["selection_behavior_is_select_rows"] = (
fake.runs_table.selectionBehavior() == QTableWidget.SelectRows
)
result["context_menu_policy_is_custom"] = (
fake.runs_table.contextMenuPolicy() == Qt.CustomContextMenu
)
result["table_tooltip"] = fake.runs_table.toolTip()
result["back_btn_text"] = fake.runs_back_btn.text()
result["back_btn_tooltip"] = fake.runs_back_btn.toolTip()
result["back_icon_isnull"] = fake.runs_back_btn.icon().isNull()
result["title_text"] = fake.runs_title.text()
result["title_object_name"] = fake.runs_title.objectName()
result["ws_folder_flat"] = fake.ws_folder_btn.isFlat()
result["ws_folder_cursor_is_pointing_hand"] = (
fake.ws_folder_btn.cursor().shape() == Qt.PointingHandCursor
)
result["ws_folder_text"] = fake.ws_folder_btn.text()
result["ws_folder_tooltip"] = fake.ws_folder_btn.toolTip()
result["ws_folder_icon_isnull"] = fake.ws_folder_btn.icon().isNull()
result["stop_btn_text"] = fake.run_stop_btn.text()
result["stop_btn_object_name"] = fake.run_stop_btn.objectName()
result["stop_btn_tooltip"] = fake.run_stop_btn.toolTip()
result["stop_btn_icon_isnull"] = fake.run_stop_btn.icon().isNull()
result["rename_btn_text"] = fake.run_rename_btn.text()
result["rename_btn_tooltip"] = fake.run_rename_btn.toolTip()
result["rename_btn_icon_isnull"] = fake.run_rename_btn.icon().isNull()
result["del_btn_text"] = fake.run_del_btn.text()
result["del_btn_tooltip"] = fake.run_del_btn.toolTip()
result["del_btn_icon_isnull"] = fake.run_del_btn.icon().isNull()
result["clear_btn_text"] = fake.run_clear_btn.text()
result["clear_btn_tooltip"] = fake.run_clear_btn.toolTip()
result["clear_btn_object_name"] = fake.run_clear_btn.objectName()
result["clear_btn_icon_isnull"] = fake.run_clear_btn.icon().isNull()
# --- bam nut / emit signal that -> xac nhan CO goi dung method tren self ----
fake.runs_back_btn.click()
result["calls_after_back_click"] = list(calls["show_runs"])
fake.ws_folder_btn.click()
result["ws_open_calls_after_click"] = calls["ws_open"]
fake.run_stop_btn.click()
result["stop_calls_after_click"] = calls["stop"]
fake.run_rename_btn.click()
result["rename_calls_after_click"] = calls["rename"]
fake.run_del_btn.click()
result["delete_calls_after_click"] = calls["delete"]
fake.run_clear_btn.click()
result["manager_clear_finished_calls_after_click"] = fake.manager.clear_finished_calls
item = QTableWidgetItem("row0")
fake.runs_table.setRowCount(1)
fake.runs_table.setItem(0, 0, item)
fake.runs_table.itemDoubleClicked.emit(item)
result["open_run_calls_after_dbl_click"] = calls["open_run"]
fake.runs_table.customContextMenuRequested.emit(QPoint(5, 5))
result["ctx_menu_calls_after_signal"] = calls["ctx_menu"]
result["flow_output_root_fallback"] = str(fake._flow_output_root())
result["config_dir"] = str(CONFIG_DIR)
print(json.dumps(result, sort_keys=True, ensure_ascii=True))
print("PROBE_OK")
"""
@pytest.fixture(scope="module")
def probe_result(tmp_path_factory):
"""Chạy ``_PROBE_SCRIPT`` một lần cho cả module trong TIẾN TRÌNH CON, trả
về dict JSON đã in được cùng ``sandbox`` đã dùng (để test tính lại các giá
trị phụ thuộc đường dẫn, thay vì chép tay chuỗi tuyệt đối)."""
sandbox = tmp_path_factory.mktemp("co4e-runs-page-home")
env = dict(os.environ)
env["HOME"] = str(sandbox)
env["USERPROFILE"] = str(sandbox)
env["QT_QPA_PLATFORM"] = "offscreen"
env.pop("HOMEDRIVE", None)
env.pop("HOMEPATH", None)
result = subprocess.run(
[sys.executable, "-c", _PROBE_SCRIPT, str(sandbox), str(REPO_PARENT)],
cwd=str(REPO_ROOT),
env=env,
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, (
f"probe co4e runs page that bai (exit {result.returncode}):\n"
f"--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}"
)
assert "PROBE_OK" in result.stdout, result.stdout
json_line = result.stdout.strip().splitlines()[-2]
data = json.loads(json_line)
data["_sandbox"] = str(sandbox)
return data
# ---------------------------------------------------------------------------
# runs_table: cau truc/co so mac dinh
# ---------------------------------------------------------------------------
def test_runs_table_has_5_columns_and_starts_empty(probe_result):
assert probe_result["col_count"] == 5
# quirk: _build_runs_page KHONG tu seed hang nao, du manager co run hay
# khong - seed la viec cua _refresh_runs (ngoai pham vi test nay).
assert probe_result["row_count"] == 0
def test_runs_table_is_parented_into_the_returned_widget(probe_result):
# DA CAP NHAT sau khi tach "Run Control" (xem
# docs/architecture/co4e-split-map-run-control.md): _build_runs_page gio
# tra ve mot RunsPagePanel (subclass QWidget dung trong presentation/co4e/
# co4e_run_control_widget.py) thay vi mot QWidget tran - van la mot QWidget
# that su (layout/table van nguyen), chi ten class cu the doi.
assert probe_result["w_type"] == "RunsPagePanel"
assert probe_result["layout_type"] == "QVBoxLayout"
assert probe_result["table_is_child_of_w"] is True
def test_runs_table_display_settings(probe_result):
assert probe_result["header_resize_mode_is_stretch"] is True
# KHONG dung "vheader_visible" (QWidget.isVisible()) o day: gia tri do
# luon False bat ke setVisible(True/False) trong code san pham, vi
# runs_table/verticalHeader khong bao gio duoc .show() thuc su trong luc
# probe (isVisible() phu thuoc CA chuoi ancestor co dang hien tren man
# hinh hay khong). Dung "vheader_visible_to_table" (isVisibleTo(ancestor))
# de bat dung co explicit hidden ma setVisible(False) dat len
# verticalHeader, khong phu thuoc runs_table co duoc show hay khong -
# mutation setVisible(False) -> True lam gia tri nay lat tu False len True.
assert probe_result["vheader_visible_to_table"] is False
assert probe_result["edit_triggers_is_no_edit"] is True
assert probe_result["selection_behavior_is_select_rows"] is True
assert probe_result["context_menu_policy_is_custom"] is True
def test_runs_table_tooltip_matches_tr_key(probe_result):
from cowork_local.i18n import tr
assert probe_result["table_tooltip"] == tr("co4e.tt_runs_list")
# ---------------------------------------------------------------------------
# runs_back_btn
# ---------------------------------------------------------------------------
def test_back_button_text_tooltip_and_icon(probe_result):
from cowork_local.i18n import tr
assert probe_result["back_btn_text"] == tr("co4e.back_to_flow")
assert probe_result["back_btn_tooltip"] == tr("co4e.tt_back_to_flow")
assert probe_result["back_icon_isnull"] is False
def test_back_button_click_calls_show_runs_with_false(probe_result):
# _build_runs_page noi runs_back_btn.clicked -> lambda: self._show_runs(False)
assert probe_result["calls_after_back_click"] == [False]
# ---------------------------------------------------------------------------
# runs_title
# ---------------------------------------------------------------------------
def test_runs_title_text_and_object_name(probe_result):
from cowork_local.i18n import tr
assert probe_result["title_text"] == tr("co4e.running_flows")
assert probe_result["title_object_name"] == "hint"
# ---------------------------------------------------------------------------
# ws_folder_btn
# ---------------------------------------------------------------------------
def test_ws_folder_button_is_flat_with_pointing_hand_cursor(probe_result):
assert probe_result["ws_folder_flat"] is True
assert probe_result["ws_folder_cursor_is_pointing_hand"] is True
assert probe_result["ws_folder_icon_isnull"] is False
def test_ws_folder_button_text_uses_ellipsis_and_forward_slash_regardless_of_os(
probe_result,
):
# quirk: "…/" (dau cham lung That, KHONG phai 3 dau cham thuong) roi
# noi 2 phan cuoi cua path bang "/" luon luon - kha nang fallback rong day
# du duoc kich hoat boi ctx.config gia lap khong co .config that.
assert probe_result["ws_folder_text"] == "…/runs/co4e"
def test_ws_folder_button_tooltip_embeds_the_fallback_root_path(probe_result):
from cowork_local.i18n import tr
root = str(Path(probe_result["config_dir"]) / "co4e" / "runs" / "co4e")
assert probe_result["ws_folder_tooltip"] == tr(
"co4e.tt_open_workspace", path=root
)
assert probe_result["flow_output_root_fallback"] == root
def test_ws_folder_button_click_calls_open_workspace_folder(probe_result):
assert probe_result["ws_open_calls_after_click"] == 1
def test_config_dir_is_isolated_inside_the_sandbox(probe_result):
# chot lai rang qua trinh dung widget khong lam chuyen sang HOME that.
assert probe_result["_sandbox"] in probe_result["config_dir"]
# ---------------------------------------------------------------------------
# run_stop_btn / run_rename_btn / run_del_btn / run_clear_btn
# ---------------------------------------------------------------------------
def test_stop_button_text_object_name_tooltip_icon(probe_result):
from cowork_local.i18n import tr
assert probe_result["stop_btn_text"] == tr("co4e.stop")
assert probe_result["stop_btn_object_name"] == "danger"
assert probe_result["stop_btn_tooltip"] == tr("co4e.tt_stop_run")
assert probe_result["stop_btn_icon_isnull"] is False
def test_stop_button_click_calls_stop_selected_run(probe_result):
assert probe_result["stop_calls_after_click"] == 1
def test_rename_button_text_tooltip_icon(probe_result):
from cowork_local.i18n import tr
assert probe_result["rename_btn_text"] == tr("co4e.rename_run")
assert probe_result["rename_btn_tooltip"] == tr("co4e.tt_rename_run")
assert probe_result["rename_btn_icon_isnull"] is False
def test_rename_button_click_calls_rename_selected_run(probe_result):
assert probe_result["rename_calls_after_click"] == 1
def test_delete_button_text_tooltip_icon(probe_result):
from cowork_local.i18n import tr
assert probe_result["del_btn_text"] == tr("co4e.delete_run")
assert probe_result["del_btn_tooltip"] == tr("co4e.tt_delete_run")
assert probe_result["del_btn_icon_isnull"] is False
def test_delete_button_click_calls_delete_selected_run(probe_result):
assert probe_result["delete_calls_after_click"] == 1
def test_clear_button_text_tooltip_and_no_object_name(probe_result):
from cowork_local.i18n import tr
assert probe_result["clear_btn_text"] == tr("co4e.clear_done")
assert probe_result["clear_btn_tooltip"] == tr("co4e.tt_clear_runs")
# quirk: khac 4 nut hang xom, run_clear_btn khong setObjectName.
assert probe_result["clear_btn_object_name"] == ""
def test_clear_button_has_no_icon_unlike_its_siblings(probe_result):
# quirk: run_clear_btn la nut DUY NHAT trong hang khong co setIcon(...).
assert probe_result["clear_btn_icon_isnull"] is True
def test_clear_button_click_calls_manager_clear_finished_directly(probe_result):
# quirk: noi THANG toi lambda: self.manager.clear_finished() - khong di
# qua mot method rieng tren self nhu 3 nut hang xom (stop/rename/delete).
assert probe_result["manager_clear_finished_calls_after_click"] == 1
# ---------------------------------------------------------------------------
# runs_table: double-click / context-menu signals
# ---------------------------------------------------------------------------
def test_double_click_on_a_row_calls_open_run_from_table(probe_result):
assert probe_result["open_run_calls_after_dbl_click"] == 1
def test_context_menu_request_calls_runs_context_menu(probe_result):
assert probe_result["ctx_menu_calls_after_signal"] == 1
# ---------------------------------------------------------------------------
# Lenh thu cong da dung de chot cac gia tri JSON o tren (quy trinh nguoc):
#
# .venv/Scripts/python.exe -c "<noi dung _PROBE_SCRIPT>" <sandbox> <repo_parent>
#
# voi <sandbox> la mot thu muc rong duoc gan vao HOME/USERPROFILE TRUOC khi
# script import bat ky thu gi thuoc cowork_local, va <repo_parent> la thu muc
# cha cua repo (de "import cowork_local" hoat dong dung nhu conftest.py lam).
# ---------------------------------------------------------------------------