Files
cowork-local/tests/characterization/test_node_property_panel.py
T
anhtnm1andClaude Opus 5 ab04d68a22
CI / test (pull_request) Canceled after 0s
fix(i18n): dịch nốt chữ do Qt tự vẽ, và thêm dòng phiên bản ở góc phải
Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:

* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
  phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
  lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
  ``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của Qt; ứng dụng không cài
  ``QTranslator`` nào và bản PySide6 đang dùng cũng không đóng gói file
  ``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.

``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.

Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:

* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
  nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
  mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
  MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
  thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").

Kèm theo, phần giao diện người dùng yêu cầu:

* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
  và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
  giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.

Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.

Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 11:09:42 +09:00

470 lines
21 KiB
Python

"""Characterization test cho ``StepConfigPanel`` (``ui/co4e_config_panel.py``,
dong 133-528) — KHONG bao gom ``_SectionHeader``/``_add_section`` (khung UI
chung, khong co hanh vi nghiep vu rieng dang characterize).
VI SAO GHI LAI CHU KHONG PHAN XET: day la luoi an toan cho dot tach
``StepConfigPanel`` ra khoi ``ui/co4e_config_panel.py`` sang cac module con
duoi ``presentation/co4e/`` (vi du ``node_property_panel.py``,
``node_property_actions_mixin.py``, ``step_config_section.py`` — xem
``docs/architecture/co4e-split-map-node-property.md``). Moi ``assert`` duoi
day duoc chot lai tu gia tri THAT in ra khi chay code that (quy trinh nguoc:
chay truoc, in ra, dan vao assert) — khong phai gia tri "nen" la gi theo suy
doan.
VI SAO CAN TIEN TRINH CON + CO LAP HOME: ban than ``StepConfigPanel.__init__``/
``load_step`` KHONG cham dia/mang, nhung ``_available_agent_names()`` (goi tu
``_add_subagent``/``_edit_subagent``) doc THAT tu
``core.co4e.list_custom_agents()`` duoi ``CONFIG_DIR/co4e/agents`` — mot
hang so module tinh MOT LAN tu ``Path.home()`` luc import. Vi vay HOME/
USERPROFILE phai duoc tro sang thu muc tam TRUOC BAT KY import
``cowork_local.*`` nao, trong mot tien trinh con sach (patch thuoc tinh sau
khi import la khong du) — dung khuon ``tools/capture_screens.py::_isolate_home``
va ``tests/characterization/test_co4e_agent_panel.py``. Tien trinh con rieng
cung tranh xung dot QApplication singleton neu mot tien trinh pytest khac da
tao QApplication trong cung luot chay. ``QT_QPA_PLATFORM=offscreen`` duoc dat
TRUOC khi import PySide6 (dung khuon ``tools/check_co4e.py`` dong 22+40-42);
script con assert ngay ``str(CONFIG_DIR).startswith(sandbox)`` truoc khi goi
bat ky ham co4e nao (giong ``tools/check_co4e.py:47``) — khong co lap la doc
(va co the ghi) du lieu that cua nguoi dung qua ``list_custom_agents()``/
``save_custom_agent()``.
KHONG goi provider AI that / spawn thread that: ``_ai_draft()`` va
``_load_models()`` (dung ``AgentWorker``/``QThread`` that) KHONG duoc goi o
bat ky case nao trong file nay — hai ham do nam NGOAI pham vi duoc giao
(139-462) va can duoc characterize rieng, tach biet, voi worker/thread duoc
gia lap chu khong start that.
QUIRK DA DONG DINH (xem case tuong ung ben duoi):
* ``gen_btn``/``load_models_btn`` duoc set ``setEnabled(ctx is not None)``
ngay trong ``__init__`` (dong 181, 208), NHUNG dong 313 goi
``self.setEnabled(False)`` cho CA PANEL o cuoi ``__init__`` — vi Qt tinh
``isEnabled()`` hieu qua tu ca chuoi ancestor, ca hai nut deu tra ve
``False`` NGAY SAU KHI DUNG XONG bat ke ``ctx`` la gi, cho toi khi
``load_step()`` (hoac ``setEnabled(True)`` truc tiep) bat lai ca panel.
Doc rieng dong 181/208 se de tuong "ctx=None thi nut luon tat, ctx khac
None thi nut luon bat" — sai, ca hai deu tat cho den khi co step duoc nap.
* ``rounds_spin`` = ``QSpinBox(range=1..5)`` nhung code goi
``setValue(max(1, step.max_verify_rounds))`` (dong 330) — voi
``max_verify_rounds=10``, ``max(1, 10) == 10`` nhung ``QSpinBox`` tu kep
ve tran cua no nen gia tri hien thi la ``5``, khong phai ``10``. Doc code
ma khong chay se tuong gia tri duoc giu nguyen.
* ``perm_combo`` voi ``step.permission_preset`` khong nam trong
``PERMISSION_PRESETS`` (nhanh fallback cua ``findData`` tra ve -1) roi ve
index 0 == ``"inherit"`` — im lang, khong bao loi.
* ``load_step`` chi dung skill_names de dung skills_list; mot skill trong
``step.skills`` khong con trong ``skill_names`` (skill da bi xoa khoi
registry) don gian BIEN MAT khoi checklist — va khi ``_on_edit`` chay lan
ke tiep (do BAT KY thay doi field nao khac, khong can dung vao
skills_list), ``s.skills`` duoc GHI DE lai chi bang cac item dang hien
trong checklist, nen ten skill "mo coi" do bi RUNG VINH VIEN khoi
``step.skills`` — mot edit khong lien quan (vi du sua role) am tham xoa
du lieu skill cu.
* ``variant != "parallel"``: ``_parallel_card`` bi an VA ``sub_list.clear()``
duoc goi, nhung ``step.sub_agents`` KHONG bi dung vao neu ``is_par`` False
— du liệu van con trong step, chi khong hien tren UI.
* ``_add_subagent``: khi ``_available_agent_names()`` tra ve danh sach RONG,
code chuyen sang ``ask_text`` thay vi ``ask_item`` (dong
401-402) — nhanh fallback nay chi cham toi khi khong con agent nao (builtin
+ custom) de chon, hiem khi xay ra tren du lieu that nhung van la mot
nhanh code song.
* ``_edit_subagent``: neu agent hien tai cua sub-agent khong con trong danh
sach ten kha dung (``cur not in names``), dialog mo tai index 0 thay vi
bao loi hay giu nguyen lua chon cu.
VONG DOI: day la gian giao cho dot tach ``StepConfigPanel`` sang
``presentation/co4e/`` (xem ``docs/architecture/co4e-split-map-node-property.md``
cho ke hoach tach cu the). Sau khi tach xong thanh cac lop/mixin voi hop dong
ro rang, cac case o day nen duoc viet lai thanh test dac ta cho tung phan
(khong can dung ca ``QApplication``/tien trinh con neu phan tach ra la logic
thuan). Ba quirk "panel disabled de len enable cua nut", "orphan skill bi xoa
qua mot edit khong lien quan" va "sub_agents khong dong bo voi sub_list khi
doi variant" dang mo issue hoi y kien san pham truoc khi ai do "don" chung
trong luc tach — dac biet quirk orphan-skill, vi day la mot dang mat du lieu
tham lang de bi coi la bug can sua ngay khi gap lai, nhung sua no thay doi
hanh vi luu tru hien co ma khong ai ro co ai dang phu thuoc vao khong.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
REPO_PARENT = REPO_ROOT.parent
_SCRIPT = r"""
import sys
sys.path.insert(0, REPO_PARENT_PLACEHOLDER)
from pathlib import Path as _PP
from PySide6.QtWidgets import QApplication, QFileDialog, QListWidgetItem
from PySide6.QtCore import Qt
from cowork_local.config import CONFIG_DIR
sandbox = sys.argv[1]
assert str(CONFIG_DIR).startswith(str(_PP(sandbox))), "khong co lap: CONFIG_DIR=" + str(CONFIG_DIR)
app = QApplication([])
from cowork_local.ui.co4e_config_panel import StepConfigPanel
from cowork_local.core.co4e import Step, SubAgent, PERMISSION_PRESETS
from cowork_local.core import co4e as co4e_mod
from cowork_local.core.co4e_builtins import BUILTIN_AGENTS
# ---- __init__ (139-313) ----------------------------------------------------
p = StepConfigPanel()
assert p.gen_btn.isEnabled() is False
assert p.load_models_btn.isEnabled() is False
assert p.isEnabled() is False, "panel bat dau bi setEnabled(False) o dong 313"
assert p.perm_combo.count() == len(PERMISSION_PRESETS) == 4
assert [p.perm_combo.itemData(i) for i in range(4)] == list(PERMISSION_PRESETS)
assert (p.rounds_spin.minimum(), p.rounds_spin.maximum()) == (1, 5)
print("CASE_INIT_DEFAULT_CTX_NONE_OK")
# quirk: gen_btn/load_models_btn duoc set theo ctx, nhung setEnabled(False)
# cho CA PANEL o cuoi __init__ de len tren ca hai -> ca hai deu False cho den
# khi mot cai gi do bat lai ca panel (vd load_step() goi setEnabled(True)).
p_ctx = StepConfigPanel(ctx=object())
assert p_ctx.gen_btn.isEnabled() is False, "quirk: panel disabled de len len ctx!=None"
assert p_ctx.load_models_btn.isEnabled() is False
p_ctx.setEnabled(True) # mo phong dieu load_step() lam
assert p_ctx.gen_btn.isEnabled() is True
assert p_ctx.load_models_btn.isEnabled() is True
p_none = StepConfigPanel(ctx=None)
p_none.setEnabled(True)
assert p_none.gen_btn.isEnabled() is False, "ctx=None -> nut van tat sau khi panel duoc bat"
assert p_none.load_models_btn.isEnabled() is False
print("CASE_INIT_CTX_ENABLE_QUIRK_OK")
p.show()
app.processEvents()
assert p._parallel_card.isVisible() is True, "form4/_parallel_card khong tu an luc dung"
print("CASE_INIT_PARALLEL_CARD_DEFAULT_VISIBLE_OK")
# ---- load_step (316-354): step "step" thuong -------------------------------
step = Step(
variant="step", label="My Step", role="worker", icon="file",
instructions="Do X", context="bg info", model="gpt-4",
permission_preset="standard", self_verify=False, max_verify_rounds=3,
skills=["Test Skill", "OrphanSkill"], attachments=["C:/foo/bar/baz.txt", "note.md"],
sub_agents=[SubAgent(agent="Ghost")],
)
p.load_step("n1", step, ["Test Skill", "Other Skill"])
assert p.label_edit.text() == "My Step"
assert p.role_edit.text() == "worker"
assert p.icon_edit.currentText() == "file"
assert p.instructions_edit.toPlainText() == "Do X"
assert p.context_edit.toPlainText() == "bg info"
assert p.model_combo.currentText() == "gpt-4"
assert (p.perm_combo.currentIndex(), p.perm_combo.currentData()) == (2, "standard")
assert p.verify_chk.isChecked() is False
assert p.rounds_spin.value() == 3
assert p.skills_list.count() == 2, "chi 2 muc trong skill_names duoc ve, OrphanSkill khong co hang"
assert [p.skills_list.item(i).text() for i in range(2)] == ["Test Skill", "Other Skill"]
assert p.skills_list.item(0).checkState() == Qt.Checked
assert p.skills_list.item(1).checkState() == Qt.Unchecked
assert p.attach_list.count() == 2
assert p.attach_list.item(0).text() == "baz.txt"
assert p.attach_list.item(0).toolTip() == "C:/foo/bar/baz.txt"
assert p.attach_list.item(1).text() == "note.md"
app.processEvents()
assert p._parallel_card.isVisible() is False, "variant='step' -> card an"
assert p.sub_list.count() == 0, "quirk: sub_agents khong rong nhung khong dong bo vao UI khi khong phai parallel"
assert step.sub_agents == [SubAgent(agent="Ghost")], "du lieu step khong bi dong cham, chi UI khong ve"
print("CASE_LOAD_STEP_BASIC_OK")
# quirk: rounds_spin.setValue(max(1, n)) nhung QSpinBox tu kep tran o 5.
p.load_step("n0", Step(max_verify_rounds=0), [])
assert p.rounds_spin.value() == 1
p.load_step("nneg", Step(max_verify_rounds=-5), [])
assert p.rounds_spin.value() == 1
p.load_step("n10", Step(max_verify_rounds=10), [])
assert p.rounds_spin.value() == 5, "quirk: max(1,10)=10 nhung QSpinBox kep ve tran 5"
print("CASE_LOAD_STEP_ROUNDS_CLAMP_QUIRK_OK")
# quirk: permission_preset la khoa khong ton tai -> fallback ve index 0 (inherit)
p.load_step("nbad", Step(permission_preset="does-not-exist"), [])
assert (p.perm_combo.currentIndex(), p.perm_combo.currentData()) == (0, "inherit")
print("CASE_LOAD_STEP_UNKNOWN_PRESET_FALLBACK_OK")
# variant="parallel" -> card hien, sub_list duoc ve tu sub_agents
steppar = Step(variant="parallel", sub_agents=[SubAgent(agent="A1"), SubAgent(agent="A2")])
p.load_step("npar", steppar, [])
app.processEvents()
assert p._parallel_card.isVisible() is True
assert [p.sub_list.item(i).text() for i in range(p.sub_list.count())] == ["A1", "A2"]
print("CASE_LOAD_STEP_PARALLEL_OK")
# rong: skills/attachments rong -> list rong, khong loi
p.load_step("nempty", Step(), [])
assert p.skills_list.count() == 0
assert p.attach_list.count() == 0
print("CASE_LOAD_STEP_EMPTY_OK")
# ---- clear_step (356-359) ---------------------------------------------------
p.clear_step()
assert p._step is None
assert p._node_id == ""
assert p.isEnabled() is False
print("CASE_CLEAR_STEP_OK")
# _on_edit voi _step None -> khong loi, khong lam gi
p._on_edit()
print("CASE_ON_EDIT_NO_STEP_NOOP_OK")
# ---- _on_edit (362-378): guard _loading, fallback role, quirk orphan-skill --
p.load_step("npar2", step, ["Test Skill", "Other Skill"])
assert step.skills == ["Test Skill", "OrphanSkill"], "chua edit gi thi step chua bi dong den"
p.role_edit.setText("") # -> _on_edit tu dong chay qua signal that, khong mock
assert step.role == "AGENT", "role rong -> fallback 'AGENT' (dong 367)"
assert step.skills == ["Test Skill"], (
"quirk: mot edit KHONG LIEN QUAN (sua role) cung ghi de s.skills bang "
"danh sach dang checked trong UI -> OrphanSkill bi rung vinh vien"
)
print("CASE_ON_EDIT_ROLE_FALLBACK_AND_ORPHAN_SKILL_QUIRK_OK")
p._loading = True
step.role = "UNTOUCHED"
p.role_edit.setText("Something Else")
assert step.role == "UNTOUCHED", "guard _loading chan _on_edit khong ghi lai step"
p._loading = False
print("CASE_ON_EDIT_LOADING_GUARD_OK")
p.role_edit.setText("scout")
assert step.role == "SCOUT", "role duoc upper() hoa (dong 367)"
print("CASE_ON_EDIT_ROLE_UPPERCASE_OK")
# ---- _available_agent_names (380-390, static) ------------------------------
names0 = StepConfigPanel._available_agent_names()
assert names0 == [a.name for a in BUILTIN_AGENTS], "khong co custom agent -> chi builtin, dung thu tu"
assert names0[0] == "Business Analyst"
assert len(names0) == len(BUILTIN_AGENTS) == 19
print("CASE_AVAILABLE_NAMES_NO_CUSTOM_OK")
custom = co4e_mod.new_custom_agent("Zed Custom")
co4e_mod.save_custom_agent(custom)
names1 = StepConfigPanel._available_agent_names()
assert names1[0] == "Zed Custom", "custom agent dung TRUOC builtin"
assert len(names1) == len(BUILTIN_AGENTS) + 1
print("CASE_AVAILABLE_NAMES_ONE_CUSTOM_OK")
# quirk: custom trung ten voi mot builtin -> chi giu 1 lan (dong 389: "not in names")
dup = co4e_mod.new_custom_agent(BUILTIN_AGENTS[0].name)
co4e_mod.save_custom_agent(dup)
names2 = StepConfigPanel._available_agent_names()
assert names2.count(BUILTIN_AGENTS[0].name) == 1, "trung ten voi builtin bi loai bo, khong nhan doi"
assert names2[0] == "Zed Custom"
print("CASE_AVAILABLE_NAMES_DEDUPE_WITH_BUILTIN_OK")
# ---- _add_subagent (392-408) ------------------------------------------------
pstep = Step(variant="parallel")
p.load_step("nsub", pstep, [])
p._step = None
before_n = p.sub_list.count()
p._add_subagent()
assert p.sub_list.count() == before_n, "_step None -> _add_subagent la no-op"
print("CASE_ADD_SUBAGENT_NO_STEP_NOOP_OK")
p._step = pstep
# Panel goi ask_item/ask_text cua ui/dialog_buttons (khong con goi thang ham
# tinh QInputDialog.get*: ham tinh tu dung hop thoai ben trong nen nut "Cancel"
# cua no luon tieng Anh). Patch dung namespace cua mixin dang goi — patch vao
# QInputDialog nhu truoc se KHONG chan duoc gi va hop thoai that se mo ra treo.
from cowork_local.presentation.co4e import node_property_actions_mixin as _acts
orig_getItem = _acts.ask_item
orig_getText = _acts.ask_text
changed_n = {"n": 0}
p.changed.connect(lambda: changed_n.__setitem__("n", changed_n["n"] + 1))
_acts.ask_item = lambda *a, **k: ("Picked Agent", True)
p._add_subagent()
assert [s.agent for s in pstep.sub_agents] == ["Picked Agent"]
assert [p.sub_list.item(i).text() for i in range(p.sub_list.count())] == ["Picked Agent"]
assert changed_n["n"] == 1
print("CASE_ADD_SUBAGENT_PICKED_OK")
_acts.ask_item = lambda *a, **k: ("", True)
p._add_subagent()
assert [s.agent for s in pstep.sub_agents] == ["Picked Agent"], "ten rong (sau strip) -> khong them"
assert changed_n["n"] == 1, "khong them thi khong emit changed"
print("CASE_ADD_SUBAGENT_EMPTY_NAME_NOOP_OK")
_acts.ask_item = lambda *a, **k: ("Should Not Add", False)
p._add_subagent()
assert [s.agent for s in pstep.sub_agents] == ["Picked Agent"], "ok=False -> khong them"
print("CASE_ADD_SUBAGENT_CANCELLED_NOOP_OK")
# quirk: khi _available_agent_names() rong -> dung getText thay vi getItem (dong 401-402)
# GHI CHU DOT TACH mixin (khong doi hanh vi, chi doi CACH patch/restore trong
# test): sau khi StepConfigPanel._available_agent_names duoc dua vao mixin
# rieng (_StepConfigActionsMixin), no khong con nam trong
# StepConfigPanel.__dict__ nua (ma nam trong __dict__ cua mixin, StepConfigPanel
# chi ke thua qua MRO) nen "StepConfigPanel.__dict__['_available_agent_names']"
# nem KeyError. Gan de ghi de truc tiep len StepConfigPanel (nhu dong duoi) van
# shadow dung nhu truoc; khi xong chi can `del` thuoc tinh do khoi
# StepConfigPanel de no roi ve lai dung method ke thua tu mixin - tuong duong
# hanh vi voi cach "luu roi gan lai" cu, khong lam yeu di assert nao.
StepConfigPanel._available_agent_names = staticmethod(lambda: [])
_acts.ask_text = lambda *a, **k: ("Typed Agent", True)
p._add_subagent()
assert [s.agent for s in pstep.sub_agents] == ["Picked Agent", "Typed Agent"]
del StepConfigPanel._available_agent_names
_acts.ask_item = orig_getItem
_acts.ask_text = orig_getText
print("CASE_ADD_SUBAGENT_EMPTY_NAMES_USES_GETTEXT_QUIRK_OK")
# ---- _edit_subagent (410-428) -----------------------------------------------
pstep2 = Step(variant="parallel", sub_agents=[SubAgent(agent="Unknown Agent XYZ")])
p.load_step("nedit", pstep2, [])
captured = {}
def _fake_get_item(*a, **k):
captured["items"] = a[3]
captured["current_index"] = a[4]
return ("Renamed", True)
_acts.ask_item = _fake_get_item
item0 = p.sub_list.item(0)
p._edit_subagent(item0)
assert captured["current_index"] == 0, "quirk: cur khong nam trong names -> mo dialog tai index 0"
assert pstep2.sub_agents[0].agent == "Renamed"
assert item0.text() == "Renamed"
print("CASE_EDIT_SUBAGENT_UNKNOWN_CUR_STARTS_AT_0_OK")
foreign_item = QListWidgetItem("not in list")
snapshot = dict(captured)
p._edit_subagent(foreign_item)
assert captured == snapshot, "item khong thuoc sub_list (row=-1) -> no-op"
_acts.ask_item = orig_getItem
print("CASE_EDIT_SUBAGENT_FOREIGN_ITEM_NOOP_OK")
# ---- _del_subagent (430-437) -------------------------------------------------
pstep3 = Step(variant="parallel", sub_agents=[SubAgent(agent="A"), SubAgent(agent="B")])
p.load_step("ndel", pstep3, [])
p.sub_list.setCurrentRow(-1)
p._del_subagent()
assert [s.agent for s in pstep3.sub_agents] == ["A", "B"], "khong chon dong nao -> no-op"
p.sub_list.setCurrentRow(0)
p._del_subagent()
assert [s.agent for s in pstep3.sub_agents] == ["B"]
assert [p.sub_list.item(i).text() for i in range(p.sub_list.count())] == ["B"]
print("CASE_DEL_SUBAGENT_OK")
# ---- _add_attachment (439-453) -----------------------------------------------
astep = Step(attachments=["already/here.txt"])
p.load_step("natt", astep, [])
orig_getOpenFileNames = QFileDialog.getOpenFileNames
QFileDialog.getOpenFileNames = staticmethod(
lambda *a, **k: (["already/here.txt", "new/one.txt", ""], "")
)
att_changed = {"n": 0}
p.changed.connect(lambda: att_changed.__setitem__("n", att_changed["n"] + 1))
p._add_attachment()
assert astep.attachments == ["already/here.txt", "new/one.txt"], (
"duplicate bi loai (dong 447), chuoi rong bi loai boi 'if f and ...'"
)
assert p.attach_list.count() == 2
print("CASE_ADD_ATTACHMENT_DEDUPE_AND_SKIP_EMPTY_OK")
before_att_changed = att_changed["n"]
QFileDialog.getOpenFileNames = staticmethod(lambda *a, **k: ([], ""))
p._add_attachment()
assert att_changed["n"] == before_att_changed, "files rong -> khong emit changed (dong 452-453)"
QFileDialog.getOpenFileNames = orig_getOpenFileNames
print("CASE_ADD_ATTACHMENT_EMPTY_FILES_NO_CHANGED_OK")
p._step = None
before_att_count = p.attach_list.count()
p._add_attachment()
assert p.attach_list.count() == before_att_count, "_step None -> no-op"
p._step = astep
print("CASE_ADD_ATTACHMENT_NO_STEP_NOOP_OK")
# ---- _del_attachment (455-462) -----------------------------------------------
p.attach_list.setCurrentRow(-1)
p._del_attachment()
assert astep.attachments == ["already/here.txt", "new/one.txt"], "khong chon dong -> no-op"
p.attach_list.setCurrentRow(0)
p._del_attachment()
assert astep.attachments == ["new/one.txt"]
assert p.attach_list.count() == 1
print("CASE_DEL_ATTACHMENT_OK")
print("ALL_OK")
"""
def _run_isolated(sandbox: Path) -> subprocess.CompletedProcess:
env = dict(os.environ)
env["HOME"] = str(sandbox)
env["USERPROFILE"] = str(sandbox)
env["QT_QPA_PLATFORM"] = "offscreen"
env["PYTHONIOENCODING"] = "utf-8"
env.pop("HOMEDRIVE", None)
env.pop("HOMEPATH", None)
script = _SCRIPT.replace("REPO_PARENT_PLACEHOLDER", repr(str(REPO_PARENT)))
return subprocess.run(
[sys.executable, "-c", script, str(sandbox)],
cwd=str(REPO_ROOT),
env=env,
capture_output=True,
text=True,
timeout=60,
)
def test_step_config_panel_hanh_vi_hien_tai(tmp_path):
sandbox = tmp_path / "home"
sandbox.mkdir()
result = _run_isolated(sandbox)
assert result.returncode == 0, (
f"characterization script that bai (exit {result.returncode}):\n"
f"--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}"
)
for marker in (
"CASE_INIT_DEFAULT_CTX_NONE_OK",
"CASE_INIT_CTX_ENABLE_QUIRK_OK",
"CASE_INIT_PARALLEL_CARD_DEFAULT_VISIBLE_OK",
"CASE_LOAD_STEP_BASIC_OK",
"CASE_LOAD_STEP_ROUNDS_CLAMP_QUIRK_OK",
"CASE_LOAD_STEP_UNKNOWN_PRESET_FALLBACK_OK",
"CASE_LOAD_STEP_PARALLEL_OK",
"CASE_LOAD_STEP_EMPTY_OK",
"CASE_CLEAR_STEP_OK",
"CASE_ON_EDIT_NO_STEP_NOOP_OK",
"CASE_ON_EDIT_ROLE_FALLBACK_AND_ORPHAN_SKILL_QUIRK_OK",
"CASE_ON_EDIT_LOADING_GUARD_OK",
"CASE_ON_EDIT_ROLE_UPPERCASE_OK",
"CASE_AVAILABLE_NAMES_NO_CUSTOM_OK",
"CASE_AVAILABLE_NAMES_ONE_CUSTOM_OK",
"CASE_AVAILABLE_NAMES_DEDUPE_WITH_BUILTIN_OK",
"CASE_ADD_SUBAGENT_NO_STEP_NOOP_OK",
"CASE_ADD_SUBAGENT_PICKED_OK",
"CASE_ADD_SUBAGENT_EMPTY_NAME_NOOP_OK",
"CASE_ADD_SUBAGENT_CANCELLED_NOOP_OK",
"CASE_ADD_SUBAGENT_EMPTY_NAMES_USES_GETTEXT_QUIRK_OK",
"CASE_EDIT_SUBAGENT_UNKNOWN_CUR_STARTS_AT_0_OK",
"CASE_EDIT_SUBAGENT_FOREIGN_ITEM_NOOP_OK",
"CASE_DEL_SUBAGENT_OK",
"CASE_ADD_ATTACHMENT_DEDUPE_AND_SKIP_EMPTY_OK",
"CASE_ADD_ATTACHMENT_EMPTY_FILES_NO_CHANGED_OK",
"CASE_ADD_ATTACHMENT_NO_STEP_NOOP_OK",
"CASE_DEL_ATTACHMENT_OK",
"ALL_OK",
):
assert marker in result.stdout, f"thieu marker {marker}\n{result.stdout}"
# Sandbox chi duoc dung boi save_custom_agent() cua chinh test (khong co
# du lieu nguoi dung that nao bi cham vao) - CONFIG_DIR nam trong sandbox
# nhu da assert ngay dau script.
assert (sandbox / ".cowork_local" / "co4e").exists()