refactor: gom i18n/ và theme/ thành gói, gộp requirements về một file

Thư mục gốc: 22 file .py -> 7
--------------------------------
13 file "thành phần" nằm rải rác ngay ngoài thư mục gốc, mỗi file chỉ có ĐÚNG
MỘT nơi import — chính cái hub của nó:

    i18n.py + 10 file i18n_*.py    ->  i18n/__init__.py  + i18n/*.py
    theme.py + 3 file theme_*.py   ->  theme/__init__.py + theme/*.py

Đổi hub thành `__init__.py` nên 78 chỗ `from ..i18n import tr` và 24 chỗ
`from ..theme import current_palette` KHÔNG phải sửa một dòng nào. Git nhận ra
11/15 file là đổi tên thuần, 0 dòng thay đổi; 4 file còn lại chỉ sửa đúng dòng
import và mấy tham chiếu tên file trong docstring.

Đối chiếu với bản trước khi gom, cùng một phép băm:

    số khoá i18n  1431 -> 1431      hash STRINGS  a06cc34b... (trùng)
    QSS dark                        hash          7bb230a4... (trùng)
    QSS light                       hash          884f73ce... (trùng)

`check_loc.py` phải khai thêm "i18n", "theme" vào DEFAULT_TARGET_DIRS: chúng
từng được quét theo diện "module nằm ở thư mục gốc", gom vào gói rồi thì không
khai là lặng lẽ tuột khỏi tầm quét.

Bánh cóc `ui/widgets.py` siết 505 -> 466 sau khi tách SegmentedControl — nợ cũ
co lại thì con số phải co theo, không thì bánh cóc đứng yên mãi ở mức cũ.

Một file requirements
---------------------
Xoá `requirements-test.txt`. Nó chỉ có `pytest` + `pydantic`, nhưng 64/108 file
test dựng widget thật và 20 file trong đó import PySide6 thẳng ở đầu file không
có bảo vệ — nên CI cài mỗi file kia thì pytest chết ngay lúc thu thập test chứ
không phải "vài test bị bỏ qua". Hai file cho một danh sách gần trùng nhau chỉ
tạo thêm một chỗ để lệch phiên bản, và `pydantic` đã bị chép ở cả hai.

CI đổi sang cài `requirements.txt`. Người dùng cuối cài thừa pytest vài MB.

Kèm theo: `install.bat` bỏ cờ `--dev` (không còn gì để cài thêm). Khối `if`
rỗng còn sót lại làm cmd.exe báo "( was unexpected at this time" và script chết
ngay sau bước cài thư viện — đã gỡ hẳn.

859 test xanh · 4/4 cổng CASAN · check_design_parity 32/32 ·
check_layout_geometry trùng từng byte với bản trước refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 11:25:27 +09:00
co-authored by Claude Opus 5
parent e29a0ccdbd
commit be5c5747a9
24 changed files with 66 additions and 56 deletions
+7 -3
View File
@@ -34,10 +34,14 @@ jobs:
with: with:
python-version: "3.11" python-version: "3.11"
cache: pip cache: pip
cache-dependency-path: cowork_local/requirements-test.txt cache-dependency-path: cowork_local/requirements.txt
- name: Install test dependencies # Mot file duy nhat: requirements-test.txt cu chi co pytest, nhung
run: python -m pip install --disable-pip-version-check -r requirements-test.txt # 64/108 file test dung widget that (20 file import PySide6 thang o dau
# file, khong co bao ve) nen no van phai keo ve gan nhu ca danh sach
# runtime. Cai rieng file kia thi pytest chet ngay luc thu thap test.
- name: Install dependencies
run: python -m pip install --disable-pip-version-check -r requirements.txt
- name: Check Python syntax - name: Check Python syntax
run: | run: |
+1 -1
View File
@@ -48,7 +48,7 @@ Prefer the existing lightweight Conventional Commit prefixes: `feat:`, `fix:`, `
Run the application from the parent directory with `python -m cowork_local`. The current reliable test command is: Run the application from the parent directory with `python -m cowork_local`. The current reliable test command is:
```bash ```bash
python -m pip install -r requirements-test.txt python -m pip install -r requirements.txt
python -m pytest tests -q python -m pytest tests -q
``` ```
+7 -1
View File
@@ -59,10 +59,16 @@ python -m cowork_local
### 3. Run Automated Tests ### 3. Run Automated Tests
```bash ```bash
python -m pip install -r requirements-test.txt python -m pip install -r requirements.txt
pytest -q pytest -q
``` ```
There is one requirements file, not a runtime/test pair. A separate test file
would hold only `pytest`: 64 of the 108 test modules build real widgets, and 20
of them import PySide6 unguarded at module scope, so it would have to pull in
almost the whole runtime list anyway — two files for one near-identical list is
just a second place for the pins to drift.
--- ---
## 🛡️ CASAN Quality Gate & Verification ## 🛡️ CASAN Quality Gate & Verification
+20 -20
View File
@@ -27,30 +27,30 @@ _current = DEFAULT_LANGUAGE
_listeners: List[Callable[[], None]] = [] _listeners: List[Callable[[], None]] = []
# key -> {"en": ..., "ja": ..., "vi": ...} # key -> {"en": ..., "ja": ..., "vi": ...}
from . import i18n_login_dialog as _i18n_login_dialog from . import login_dialog as _login_dialog
from . import i18n_sidebar as _i18n_sidebar from . import sidebar as _sidebar
from . import i18n_composer as _i18n_composer from . import composer as _composer
from . import i18n_hint as _i18n_hint from . import hint as _hint
from . import i18n_cowork_tab as _i18n_cowork_tab from . import cowork_tab as _cowork_tab
from . import i18n_settings_dialog as _i18n_settings_dialog from . import settings_dialog as _settings_dialog
from . import i18n_skills_dialog as _i18n_skills_dialog from . import skills_dialog as _skills_dialog
from . import i18n_libreoffice_view as _i18n_libreoffice_view from . import libreoffice_view as _libreoffice_view
from . import i18n_agents_admin_tab as _i18n_agents_admin_tab from . import agents_admin_tab as _agents_admin_tab
from . import i18n_monitoring_overview as _i18n_monitoring_overview from . import monitoring_overview as _monitoring_overview
# Gộp theo đúng thứ tự cũ: khoá trùng thì cụm sau thắng, y như khi tất cả # Gộp theo đúng thứ tự cũ: khoá trùng thì cụm sau thắng, y như khi tất cả
# còn nằm chung một dict literal. # còn nằm chung một dict literal.
STRINGS: Dict[str, Dict[str, str]] = { STRINGS: Dict[str, Dict[str, str]] = {
**_i18n_login_dialog.STRINGS, **_login_dialog.STRINGS,
**_i18n_sidebar.STRINGS, **_sidebar.STRINGS,
**_i18n_composer.STRINGS, **_composer.STRINGS,
**_i18n_hint.STRINGS, **_hint.STRINGS,
**_i18n_cowork_tab.STRINGS, **_cowork_tab.STRINGS,
**_i18n_settings_dialog.STRINGS, **_settings_dialog.STRINGS,
**_i18n_skills_dialog.STRINGS, **_skills_dialog.STRINGS,
**_i18n_libreoffice_view.STRINGS, **_libreoffice_view.STRINGS,
**_i18n_agents_admin_tab.STRINGS, **_agents_admin_tab.STRINGS,
**_i18n_monitoring_overview.STRINGS, **_monitoring_overview.STRINGS,
} }
View File
View File
+1 -14
View File
@@ -4,7 +4,6 @@ rem Cowork-Local BamBOO - cai dat thu vien Python (chay MOT lan)
rem rem
rem Cach dung: rem Cach dung:
rem install.bat cai vao moi truong ao rieng (khuyen dung) rem install.bat cai vao moi truong ao rieng (khuyen dung)
rem install.bat --dev cai them thu vien de chay test
rem install.bat --system cai thang vao Python dang co, khong dung venv rem install.bat --system cai thang vao Python dang co, khong dung venv
rem install.bat --force dung lai moi truong ao tu dau rem install.bat --force dung lai moi truong ao tu dau
rem rem
@@ -26,13 +25,11 @@ set "APPHOME=%LOCALAPPDATA%\CoworkLocal"
set "VENV=%APPHOME%\venv" set "VENV=%APPHOME%\venv"
set "LAUNCHER=%APPHOME%\launcher" set "LAUNCHER=%APPHOME%\launcher"
set "DEV=0"
set "USE_SYSTEM=0" set "USE_SYSTEM=0"
set "FORCE=0" set "FORCE=0"
:parse_args :parse_args
if "%~1"=="" goto args_done if "%~1"=="" goto args_done
if /I "%~1"=="--dev" set "DEV=1" & shift & goto parse_args
if /I "%~1"=="--system" set "USE_SYSTEM=1" & shift & goto parse_args if /I "%~1"=="--system" set "USE_SYSTEM=1" & shift & goto parse_args
if /I "%~1"=="--force" set "FORCE=1" & shift & goto parse_args if /I "%~1"=="--force" set "FORCE=1" & shift & goto parse_args
if /I "%~1"=="-h" goto usage if /I "%~1"=="-h" goto usage
@@ -120,15 +117,6 @@ if errorlevel 1 (
goto fail goto fail
) )
if "%DEV%"=="1" (
echo [3/5] Cài thêm thư viện chạy test ^(--dev^)...
%PIP% install --disable-pip-version-check -r "%REPO%\requirements-test.txt"
if errorlevel 1 (
echo [LỖI] Cài thư viện test thất bại.
goto fail
)
)
rem -------------------------------------------------------------------------- rem --------------------------------------------------------------------------
rem 4. Lien ket de goi import duoc dung ten rem 4. Lien ket de goi import duoc dung ten
rem rem
@@ -188,9 +176,8 @@ exit /b 0
:usage :usage
echo. echo.
echo install.bat [--dev] [--system] [--force] echo install.bat [--system] [--force]
echo. echo.
echo --dev cài thêm thư viện để chạy test ^(pytest, pydantic^)
echo --system cài thẳng vào Python đang có, không tạo môi trường ảo echo --system cài thẳng vào Python đang có, không tạo môi trường ảo
echo --force xoá môi trường ảo cũ rồi tạo lại từ đầu echo --force xoá môi trường ảo cũ rồi tạo lại từ đầu
echo. echo.
-2
View File
@@ -1,2 +0,0 @@
pydantic>=2,<3
pytest>=8,<10
+9
View File
@@ -39,3 +39,12 @@ pywin32>=306; sys_platform == "win32" # Office -> PDF, thông báo Outlook
# opendataloader-pdf # bộ đọc PDF thay thế — KHÔNG cài sẵn có chủ ý: # opendataloader-pdf # bộ đọc PDF thay thế — KHÔNG cài sẵn có chủ ý:
# # application/workspaces/graph_index_service.py tự cài # # application/workspaces/graph_index_service.py tự cài
# # khi cần, qua core/deps.py::ensure_module. # # khi cần, qua core/deps.py::ensure_module.
# --- Chạy test ---
# Gộp vào đây thay vì để riêng requirements-test.txt: file kia chỉ có đúng
# `pytest`, mà 64/108 file test dựng widget thật nên nó vẫn phải kéo về gần
# như toàn bộ danh sách trên. Hai file cho một danh sách gần trùng nhau chỉ
# tạo thêm một chỗ để lệch phiên bản.
#
# Người dùng cuối cài thừa pytest vài MB — đổi lại chỉ còn MỘT file phải nhớ.
pytest>=8,<10
+5 -1
View File
@@ -40,6 +40,10 @@ if hasattr(sys.stdout, "reconfigure"):
DEFAULT_TARGET_DIRS = [ DEFAULT_TARGET_DIRS = [
"domain", "application", "infrastructure", "presentation", "domain", "application", "infrastructure", "presentation",
"ui", "core", "providers", "security", "mcp_servers", "ui", "core", "providers", "security", "mcp_servers",
# ``i18n/`` và ``theme/`` từng là 13 file rời nằm thẳng ở thư mục gốc nên
# được quét theo diện "module gốc"; gom vào gói rồi thì phải khai ở đây,
# không thì chúng lặng lẽ tuột khỏi tầm quét.
"i18n", "theme",
] ]
DEFAULT_MAX_LINES = 400 DEFAULT_MAX_LINES = 400
@@ -60,7 +64,7 @@ SCAN_ROOT_MODULES = True
#: đúng là tách file. #: đúng là tách file.
LEGACY_ALLOWANCE = { LEGACY_ALLOWANCE = {
"ui/workspace_tab.py": 566, "ui/workspace_tab.py": 566,
"ui/widgets.py": 505, "ui/widgets.py": 466,
"ui/task_editor_dialog.py": 627, "ui/task_editor_dialog.py": 627,
"ui/accounts_tab.py": 559, "ui/accounts_tab.py": 559,
"core/skills.py": 405, "core/skills.py": 405,
+8 -6
View File
@@ -87,12 +87,14 @@ def source() -> dict[str, str]:
def test_template_exposes_exactly_three_provider_neutral_tools() -> None: def test_template_exposes_exactly_three_provider_neutral_tools() -> None:
# The MCP SDK is a RUNTIME dependency (requirements.txt) and is deliberately # Importing the MCP SDK at module scope aborted collection for the ENTIRE
# absent from requirements-test.txt, which is all CI installs. Importing it at # suite whenever the SDK was missing, so the guard lives here, inside the only
# module scope aborted collection for the ENTIRE suite, so the guard lives here, # test that touches it. Guarding per-test rather than per-module keeps the
# inside the only test that touches the SDK. Guarding per-test rather than # other cases -- pure-Python contract checks that need no SDK -- running
# per-module keeps the other cases -- pure-Python contract checks that need no # instead of silently skipping with it.
# SDK -- running on CI instead of silently skipping with it. #
# ``mcp`` is in requirements.txt, so a correctly installed checkout runs this
# test for real; the guard only covers an environment installed by hand.
types = pytest.importorskip("mcp.types") types = pytest.importorskip("mcp.types")
assert set(TOOL_NAMES) == EXPECTED_TOOLS assert set(TOOL_NAMES) == EXPECTED_TOOLS
+2 -2
View File
@@ -34,10 +34,10 @@ original value, so the deviation is auditable rather than silent.
""" """
from __future__ import annotations from __future__ import annotations
from .theme_palettes import ( # noqa: F401 — giữ đường vào cũ from .palettes import ( # noqa: F401 — giữ đường vào cũ
DARK, LIGHT, Palette, _chevron_asset, _FONT, _MONO, _PALETTES, DARK, LIGHT, Palette, _chevron_asset, _FONT, _MONO, _PALETTES,
) )
from .theme_qss import _TEMPLATE from .qss import _TEMPLATE
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from string import Template from string import Template
+2 -2
View File
@@ -4,7 +4,7 @@ Tách khỏi ``theme.py`` vì nó là **dữ liệu**, không phải logic: mộ
``string.Template`` mà ``stylesheet()`` thay biến vào. Để chung thì mỗi lần ``string.Template`` mà ``stylesheet()`` thay biến vào. Để chung thì mỗi lần
muốn sửa một hàm nhỏ trong theme.py lại phải cuộn qua 470 dòng CSS. muốn sửa một hàm nhỏ trong theme.py lại phải cuộn qua 470 dòng CSS.
Sửa màu thì sang ``theme_palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. Sửa màu thì sang ``theme/palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách.
""" """
from __future__ import annotations from __future__ import annotations
@@ -12,7 +12,7 @@ from dataclasses import dataclass, asdict
from string import Template from string import Template
from .theme_qss_controls import QSS_CONTROLS from .qss_controls import QSS_CONTROLS
_QSS_SHELL = """ _QSS_SHELL = """
/* ---- reset ------------------------------------------------------------ */ /* ---- reset ------------------------------------------------------------ */
@@ -1,10 +1,10 @@
"""Nửa sau của khuôn QSS: bề mặt, tab, ô nhập, nút, badge, log. """Nửa sau của khuôn QSS: bề mặt, tab, ô nhập, nút, badge, log.
Cắt đôi khuôn QSS đúng mạch của chính nó: ``theme_qss.py`` giữ phần vỏ Cắt đôi khuôn QSS đúng mạch của chính nó: ``theme/qss.py`` giữ phần vỏ
(reset + shell: thanh rail, khung chính), file này giữ phần điều khiển. (reset + shell: thanh rail, khung chính), file này giữ phần điều khiển.
Hai nửa được nối lại trong ``theme_qss.py``. Hai nửa được nối lại trong ``theme/qss.py``.
Sửa màu thì sang ``theme_palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. Sửa màu thì sang ``theme/palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách.
""" """
from __future__ import annotations from __future__ import annotations
+1 -1
View File
@@ -26,7 +26,7 @@ class SegmentedControl(QWidget):
currentIndexChanged = Signal(int) currentIndexChanged = Signal(int)
#: Độ đậm mà ``theme_qss.py`` áp cho nút đang chọn #: Độ đậm mà ``theme/qss.py`` áp cho nút đang chọn
#: (``QPushButton#segItem:checked { font-weight: 600 }``). Đổi ở QSS thì #: (``QPushButton#segItem:checked { font-weight: 600 }``). Đổi ở QSS thì
#: phải đổi cả ở đây, nếu không chữ lại bị cắt. #: phải đổi cả ở đây, nếu không chữ lại bị cắt.
_CHECKED_WEIGHT = QFont.DemiBold _CHECKED_WEIGHT = QFont.DemiBold