## 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>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
"""Characterization test cho khu vực SKILLS trong sidebar của ``Co4ETab``
|
||||
(``ui/co4e_tab.py``): ``sk_manage_btn``/``skill_list``/``_manage_skills`` (nối
|
||||
click) và phần "populate skill_list" bên trong ``_reload_sidebar`` — đúng các
|
||||
đoạn được giao: dòng 569-580 (dựng widget qua ``SkillsListPanel`` + nối
|
||||
signal), 602 (vòng lặp ``setMinimumHeight(56)`` dùng CHUNG cho
|
||||
``wf_list``/``agent_list``/``skill_list``/``runs_side_list``), 690-723
|
||||
(``_reload_sidebar``, CHỈ đoạn skill) và 1370-1374 (``_manage_skills``).
|
||||
|
||||
VÌ SAO GHI LẠI CHỨ KHÔNG PHÁN XÉT: đây là lưới an toàn cho đợt tách
|
||||
``ui/co4e_tab.py`` (2000+ dòng) thành các module con dưới ``presentation/co4e/``
|
||||
(xem ``docs/architecture/co4e-split-map.md`` — khu vực SKILLS đã dời phần
|
||||
DỰNG WIDGET sang ``presentation/co4e/skills_list_panel.py::SkillsListPanel``,
|
||||
còn phần NỐI SIGNAL + populate vẫn ở ``Co4ETab``). Mọi ``assert`` dưới đây
|
||||
được chốt lại từ giá trị THẬT in ra khi chạy code (quy trình ngược: chạy
|
||||
trước, in ra, dán vào assert) — không phải giá trị tôi nghĩ nó "nên" là gì.
|
||||
|
||||
VÌ SAO CHẠY TRONG TIẾN TRÌNH CON CÔ LẬP HOME: giống hệt kỹ thuật của
|
||||
``tests/test_build_co4e_tab.py`` (đọc docstring đầu file đó để thấy khuôn gốc)
|
||||
và ``tests/characterization/test_co4e_agent_panel.py`` — dựng ``Co4ETab`` thật
|
||||
kéo theo ``AppConfig``/``CONFIG_DIR`` (config.py) và ``SKILLS_DIR``
|
||||
(core/skills.py) đều là hằng số module tính MỘT LẦN lúc import từ
|
||||
``Path.home()``. Monkeypatch thuộc tính module SAU khi import không đủ (còn
|
||||
``AppConfig.load()`` có tham số mặc định đóng băng lúc định nghĩa hàm — xem
|
||||
``test_build_co4e_tab.py``), nên phải cô lập ``HOME``/``USERPROFILE`` TRƯỚC
|
||||
bất kỳ import ``cowork_local.*`` nào, trong một tiến trình con sạch hoàn toàn.
|
||||
|
||||
AN TOÀN DỮ LIỆU: script con assert ``str(CONFIG_DIR).startswith(sandbox)``
|
||||
NGAY sau khi import, trước khi ghi bất kỳ file skill nào xuống
|
||||
``skills_mod.SKILLS_DIR`` (``CONFIG_DIR / "skills"``) — chạy nhầm trên máy
|
||||
thật sẽ ghi/xoá skill thật của người dùng.
|
||||
|
||||
KHÔNG gọi provider AI thật / spawn thread thật: test này không đụng tới
|
||||
``Co4ERunManager``/``AgentWorker``. ``SkillsDialog.exec()`` (modal, sẽ treo
|
||||
tiến trình headless) được monkeypatch thành một lớp giả NGAY TRONG tiến trình
|
||||
con của test — không sửa code sản phẩm; lớp giả chỉ đếm số lần được gọi và trả
|
||||
về một giá trị falsy (giống bấm Cancel) để lộ ra quirk "luôn reload" bên dưới.
|
||||
|
||||
QUIRK ĐÃ ĐÓNG ĐINH (xem case tương ứng bên dưới):
|
||||
* ``skill_list`` không lọc theo ``Skill.enabled`` — ``_skill_names()`` gọi
|
||||
``list_skills() + builtin_skills()`` không quan tâm cờ ``enabled``, nên
|
||||
một skill được TẠO nhưng chưa được người dùng tick bật (``enabled=False``)
|
||||
vẫn xuất hiện trong danh sách kéo-thả của Co4E y hệt một skill đã bật.
|
||||
Cờ ``enabled`` chỉ ảnh hưởng nơi khác (``active_skills_text`` cho chat),
|
||||
không ảnh hưởng palette này.
|
||||
* Payload kéo-thả của một skill KHÔNG chứa ``instructions`` thô của skill,
|
||||
mà chứa nguyên khối trả về bởi ``skills_mod.skill_prefix_for(name)`` —
|
||||
tức đã có tiền tố ``"## Skill: <tên>\\n"`` dán trước nội dung. Ai "dọn"
|
||||
chỗ này để dùng thẳng ``skill.instructions`` sẽ làm mọi flow kéo-thả sẵn
|
||||
mất dòng tiêu đề đó.
|
||||
* Khi skill có ``instructions`` rỗng, ``skill_prefix_for`` trả về chuỗi rỗng
|
||||
(không phải ``None``, không ném lỗi) — payload kéo-thả của skill đó có
|
||||
``instructions == ""`` dù skill vẫn hiện trong danh sách với đúng tên.
|
||||
* ``_manage_skills`` LUÔN gọi ``self._reload_sidebar()`` sau
|
||||
``SkillsDialog(...).exec()``, bất kể dialog trả về gì (khác với
|
||||
``_new_agent``/``_edit_agent`` ở khu vực AGENTS, nơi chỉ reload khi
|
||||
``dlg.exec()`` truthy) — đóng Skills manager bằng Cancel/Esc vẫn khiến
|
||||
``skill_list`` bị dựng lại từ đĩa.
|
||||
* Thư mục skills không tồn tại (chưa từng tạo skill nào) không ném lỗi:
|
||||
``_skill_names()`` có ``try/except`` bọc quanh, trả về ``[]`` một cách im
|
||||
lặng — ``skill_list`` rỗng, không có thông báo lỗi nào cho người dùng.
|
||||
|
||||
VÒNG ĐỜI: đây là giàn giáo cho đợt tách phần NỐI SIGNAL + populate của khu vực
|
||||
Skills sang ``presentation/co4e/`` (dựng widget đã tách xong thành
|
||||
``SkillsListPanel`` — xem cột "Trạng thái" dòng liên quan trong
|
||||
``docs/architecture/co4e-split-map.md``). Sau khi phần còn lại được tách (ví
|
||||
dụ một ``SkillsListController`` biết ``_manage_skills``/populate mà không cần
|
||||
biết toàn bộ ``Co4ETab``), các case ở đây nên viết lại thành test đặc tả cho
|
||||
controller mới (input rõ ràng, không cần dựng cả ``Co4ETab``/``QApplication``
|
||||
nặng nề qua subprocess). Quirk "payload dùng khối có tiền tố thay vì
|
||||
instructions thô" và quirk "luôn reload dù Cancel" đáng mở issue hỏi ý kiến
|
||||
sản phẩm trước khi ai đó "dọn" chúng trong lúc tách.
|
||||
"""
|
||||
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 json
|
||||
import sys
|
||||
sys.path.insert(0, REPO_PARENT_PLACEHOLDER)
|
||||
|
||||
from PySide6.QtWidgets import QAbstractItemView, QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||
from cowork_local.state import AppContext
|
||||
from cowork_local.core import skills as skills_mod
|
||||
from cowork_local.ui.co4e_tab import Co4ETab, _PaletteList, _skill_names
|
||||
from cowork_local.ui.icons import icon as _icon
|
||||
from cowork_local.i18n import tr
|
||||
|
||||
sandbox = sys.argv[1]
|
||||
assert str(CONFIG_DIR).startswith(sandbox), "khong co lap: CONFIG_DIR=" + str(CONFIG_DIR)
|
||||
|
||||
app = QApplication([])
|
||||
ctx = AppContext(AppConfig.load())
|
||||
tab = Co4ETab(ctx)
|
||||
|
||||
# ---- 569-580: widget cua khu vuc SKILLS duoc dung qua SkillsListPanel ------
|
||||
assert tab.sk_manage_btn.text() == tr("co4e.manage_skills"), tab.sk_manage_btn.text()
|
||||
assert tab.sk_manage_btn.toolTip() == tr("co4e.tt_manage_skills"), tab.sk_manage_btn.toolTip()
|
||||
assert tab.sk_manage_btn.objectName() == "co4eSectionAction"
|
||||
assert tab.sk_manage_btn.isFlat() is True
|
||||
assert tab.sk_manage_btn.cursor().shape() == Qt.PointingHandCursor
|
||||
assert isinstance(tab.skill_list, _PaletteList), type(tab.skill_list)
|
||||
assert tab.skill_list.dragEnabled() is True
|
||||
assert tab.skill_list.dragDropMode() == QAbstractItemView.DragOnly
|
||||
print("CASE_WIDGETS_OK")
|
||||
|
||||
# ---- 602: bon danh sach dung CHUNG mot vong lap setMinimumHeight(56) -------
|
||||
heights = {
|
||||
"wf_list": tab.wf_list.minimumHeight(),
|
||||
"agent_list": tab.agent_list.minimumHeight(),
|
||||
"skill_list": tab.skill_list.minimumHeight(),
|
||||
"runs_side_list": tab.runs_side_list.minimumHeight(),
|
||||
}
|
||||
assert heights == {
|
||||
"wf_list": 56, "agent_list": 56, "skill_list": 56, "runs_side_list": 56,
|
||||
}, heights
|
||||
print("CASE_MIN_HEIGHT_SHARED_OK")
|
||||
|
||||
# ---- quirk: chua tao skill nao -> thu muc SKILLS_DIR khong ton tai ---------
|
||||
# _skill_names() boc try/except quanh list_skills()/builtin_skills(); thu muc
|
||||
# chua ton tai (Co4ETab.__init__ khong tu tao no) khong nem loi, tra ve rong.
|
||||
assert not skills_mod.SKILLS_DIR.exists(), skills_mod.SKILLS_DIR
|
||||
assert _skill_names() == []
|
||||
tab._reload_sidebar()
|
||||
assert tab.skill_list.count() == 0, tab.skill_list.count()
|
||||
print("CASE_EMPTY_DIR_OK")
|
||||
|
||||
# ---- 690-723 (doan skill): mot skill co instructions, da bat enabled -------
|
||||
skills_mod.SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(skills_mod.SKILLS_DIR / "s1.json").write_text(json.dumps({
|
||||
"name": "Viet test",
|
||||
"description": "desc 1",
|
||||
"instructions": "Luon viet test",
|
||||
"enabled": True,
|
||||
}), encoding="utf-8")
|
||||
tab._reload_sidebar()
|
||||
assert tab.skill_list.count() == 1, tab.skill_list.count()
|
||||
it0 = tab.skill_list.item(0)
|
||||
assert it0.text() == "Viet test", it0.text()
|
||||
assert it0.icon().isNull() is False
|
||||
# Dong lo hong da bi mutation test bat duoc (xem docs/architecture/
|
||||
# co4e-refactor-run-report.md muc 4, luot 3): doi icon_name truyen vao
|
||||
# _palette_item(name, "sparkle", payload) tu "sparkle" sang "robot" o
|
||||
# ui/co4e_tab.py (dong _reload_sidebar, khu vuc SKILLS) truoc day KHONG bi bat,
|
||||
# vi assert cu chi kiem "co icon" (isNull() is False) ma khong kiem la icon NAO.
|
||||
# So sanh pixmap that (icon() la ham thuan, cung name/size/color -> cung anh)
|
||||
# thay vi so QIcon truc tiep (QIcon khong dinh nghia __eq__ theo noi dung).
|
||||
assert it0.icon().pixmap(16, 16).toImage() == _icon("sparkle").pixmap(16, 16).toImage(), (
|
||||
"icon cua skill trong palette phai dung 'sparkle' (_palette_item(name, 'sparkle', payload))"
|
||||
)
|
||||
payload0 = it0.data(Qt.UserRole)
|
||||
assert payload0 == {
|
||||
"variant": "step", "label": "Viet test", "agent_slug": "viet-test",
|
||||
"role": "SKILL", "icon": "sparkle",
|
||||
"instructions": "## Skill: Viet test\nLuon viet test",
|
||||
"context": "", "model": "", "self_verify": True, "max_verify_rounds": 1,
|
||||
"permission_preset": "full", "skills": ["Viet test"], "attachments": [],
|
||||
"sub_agents": [],
|
||||
}, payload0
|
||||
assert it0.data(Qt.UserRole + 1) is None
|
||||
print("CASE_ONE_SKILL_OK")
|
||||
|
||||
# ---- quirk: skill CHUA duoc bat (enabled=False) van hien trong palette -----
|
||||
# list_skills()/builtin_skills() khong loc theo enabled - _skill_names() lay
|
||||
# ca hai, nen mot skill "tat" van keo-tha duoc tu Co4E y het skill "bat".
|
||||
(skills_mod.SKILLS_DIR / "s2-disabled.json").write_text(json.dumps({
|
||||
"name": "Chua bat",
|
||||
"description": "",
|
||||
"instructions": "Noi dung chua bat",
|
||||
"enabled": False,
|
||||
}), encoding="utf-8")
|
||||
tab._reload_sidebar()
|
||||
assert tab.skill_list.count() == 2, tab.skill_list.count()
|
||||
names = [tab.skill_list.item(i).text() for i in range(tab.skill_list.count())]
|
||||
assert names == ["Viet test", "Chua bat"], names
|
||||
it1 = tab.skill_list.item(1)
|
||||
payload1 = it1.data(Qt.UserRole)
|
||||
assert payload1["instructions"] == "## Skill: Chua bat\nNoi dung chua bat", payload1
|
||||
print("CASE_DISABLED_SKILL_STILL_SHOWN_QUIRK_OK")
|
||||
|
||||
# ---- quirk: skill co instructions RONG -> payload instructions == "" ------
|
||||
# skill_prefix_for() tra ve "" khi instructions rong (khong None, khong loi);
|
||||
# skill van hien dung ten trong danh sach.
|
||||
(skills_mod.SKILLS_DIR / "s3-empty-instr.json").write_text(json.dumps({
|
||||
"name": "Rong noi dung",
|
||||
"description": "",
|
||||
"instructions": "",
|
||||
"enabled": True,
|
||||
}), encoding="utf-8")
|
||||
tab._reload_sidebar()
|
||||
assert tab.skill_list.count() == 3, tab.skill_list.count()
|
||||
it2 = tab.skill_list.item(2)
|
||||
assert it2.text() == "Rong noi dung", it2.text()
|
||||
payload2 = it2.data(Qt.UserRole)
|
||||
assert payload2["instructions"] == "", payload2
|
||||
assert payload2["skills"] == ["Rong noi dung"], payload2
|
||||
print("CASE_EMPTY_INSTRUCTIONS_QUIRK_OK")
|
||||
|
||||
# ---- xoa het skill roi reload lai -> khong con dong nao (khong con sot) ----
|
||||
for p in skills_mod.SKILLS_DIR.glob("*.json"):
|
||||
p.unlink()
|
||||
tab._reload_sidebar()
|
||||
assert tab.skill_list.count() == 0, tab.skill_list.count()
|
||||
print("CASE_CLEAR_ON_RELOAD_OK")
|
||||
|
||||
# ---- 1370-1374: _manage_skills() mo SkillsDialog(self, self.ctx) -----------
|
||||
import cowork_local.ui.skills_dialog as skills_dialog_mod
|
||||
|
||||
dialog_calls = []
|
||||
|
||||
|
||||
class _FakeSkillsDialog:
|
||||
def __init__(self, parent, ctx_arg):
|
||||
dialog_calls.append((parent is tab, ctx_arg is ctx))
|
||||
|
||||
def exec(self):
|
||||
return 0 # falsy, gia lap bam Cancel/Esc
|
||||
|
||||
|
||||
orig_dialog_cls = skills_dialog_mod.SkillsDialog
|
||||
skills_dialog_mod.SkillsDialog = _FakeSkillsDialog
|
||||
|
||||
reload_calls = {"n": 0}
|
||||
orig_reload = tab._reload_sidebar
|
||||
|
||||
|
||||
def _counting_reload():
|
||||
reload_calls["n"] += 1
|
||||
return orig_reload()
|
||||
|
||||
|
||||
tab._reload_sidebar = _counting_reload
|
||||
tab._manage_skills()
|
||||
skills_dialog_mod.SkillsDialog = orig_dialog_cls
|
||||
tab._reload_sidebar = orig_reload
|
||||
|
||||
assert dialog_calls == [(True, True)], dialog_calls
|
||||
# quirk: dialog tra ve gia tri falsy (Cancel) nhung sidebar VAN duoc reload.
|
||||
assert reload_calls["n"] == 1, reload_calls
|
||||
print("CASE_MANAGE_SKILLS_ALWAYS_RELOADS_QUIRK_OK")
|
||||
|
||||
# ---- 573: sk_manage_btn.clicked noi thang toi _manage_skills ---------------
|
||||
recorded = []
|
||||
tab._manage_skills = lambda: recorded.append(1)
|
||||
tab.sk_manage_btn.click()
|
||||
assert recorded == [1], recorded
|
||||
print("CASE_CLICK_WIRES_TO_MANAGE_SKILLS_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_co4e_skills_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_WIDGETS_OK",
|
||||
"CASE_MIN_HEIGHT_SHARED_OK",
|
||||
"CASE_EMPTY_DIR_OK",
|
||||
"CASE_ONE_SKILL_OK",
|
||||
"CASE_DISABLED_SKILL_STILL_SHOWN_QUIRK_OK",
|
||||
"CASE_EMPTY_INSTRUCTIONS_QUIRK_OK",
|
||||
"CASE_CLEAR_ON_RELOAD_OK",
|
||||
"CASE_MANAGE_SKILLS_ALWAYS_RELOADS_QUIRK_OK",
|
||||
"CASE_CLICK_WIRES_TO_MANAGE_SKILLS_OK",
|
||||
"ALL_OK",
|
||||
):
|
||||
assert marker in result.stdout, f"thieu marker {marker}\n{result.stdout}"
|
||||
|
||||
# Sandbox chi duoc dung boi cac file skill test tu tao xuong SKILLS_DIR
|
||||
# (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").exists()
|
||||
Reference in New Issue
Block a user