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:
python-version: "3.11"
cache: pip
cache-dependency-path: cowork_local/requirements-test.txt
cache-dependency-path: cowork_local/requirements.txt
- name: Install test dependencies
run: python -m pip install --disable-pip-version-check -r requirements-test.txt
# Mot file duy nhat: requirements-test.txt cu chi co pytest, nhung
# 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
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:
```bash
python -m pip install -r requirements-test.txt
python -m pip install -r requirements.txt
python -m pytest tests -q
```
+7 -1
View File
@@ -59,10 +59,16 @@ python -m cowork_local
### 3. Run Automated Tests
```bash
python -m pip install -r requirements-test.txt
python -m pip install -r requirements.txt
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
+20 -20
View File
@@ -27,30 +27,30 @@ _current = DEFAULT_LANGUAGE
_listeners: List[Callable[[], None]] = []
# key -> {"en": ..., "ja": ..., "vi": ...}
from . import i18n_login_dialog as _i18n_login_dialog
from . import i18n_sidebar as _i18n_sidebar
from . import i18n_composer as _i18n_composer
from . import i18n_hint as _i18n_hint
from . import i18n_cowork_tab as _i18n_cowork_tab
from . import i18n_settings_dialog as _i18n_settings_dialog
from . import i18n_skills_dialog as _i18n_skills_dialog
from . import i18n_libreoffice_view as _i18n_libreoffice_view
from . import i18n_agents_admin_tab as _i18n_agents_admin_tab
from . import i18n_monitoring_overview as _i18n_monitoring_overview
from . import login_dialog as _login_dialog
from . import sidebar as _sidebar
from . import composer as _composer
from . import hint as _hint
from . import cowork_tab as _cowork_tab
from . import settings_dialog as _settings_dialog
from . import skills_dialog as _skills_dialog
from . import libreoffice_view as _libreoffice_view
from . import agents_admin_tab as _agents_admin_tab
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ả
# còn nằm chung một dict literal.
STRINGS: Dict[str, Dict[str, str]] = {
**_i18n_login_dialog.STRINGS,
**_i18n_sidebar.STRINGS,
**_i18n_composer.STRINGS,
**_i18n_hint.STRINGS,
**_i18n_cowork_tab.STRINGS,
**_i18n_settings_dialog.STRINGS,
**_i18n_skills_dialog.STRINGS,
**_i18n_libreoffice_view.STRINGS,
**_i18n_agents_admin_tab.STRINGS,
**_i18n_monitoring_overview.STRINGS,
**_login_dialog.STRINGS,
**_sidebar.STRINGS,
**_composer.STRINGS,
**_hint.STRINGS,
**_cowork_tab.STRINGS,
**_settings_dialog.STRINGS,
**_skills_dialog.STRINGS,
**_libreoffice_view.STRINGS,
**_agents_admin_tab.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 Cach 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 --force dung lai moi truong ao tu dau
rem
@@ -26,13 +25,11 @@ set "APPHOME=%LOCALAPPDATA%\CoworkLocal"
set "VENV=%APPHOME%\venv"
set "LAUNCHER=%APPHOME%\launcher"
set "DEV=0"
set "USE_SYSTEM=0"
set "FORCE=0"
:parse_args
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"=="--force" set "FORCE=1" & shift & goto parse_args
if /I "%~1"=="-h" goto usage
@@ -120,15 +117,6 @@ if errorlevel 1 (
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 4. Lien ket de goi import duoc dung ten
rem
@@ -188,9 +176,8 @@ exit /b 0
:usage
echo.
echo install.bat [--dev] [--system] [--force]
echo install.bat [--system] [--force]
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 --force xoá môi trường ảo cũ rồi tạo lại từ đầu
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ủ ý:
# # application/workspaces/graph_index_service.py tự cài
# # 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 = [
"domain", "application", "infrastructure", "presentation",
"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
@@ -60,7 +64,7 @@ SCAN_ROOT_MODULES = True
#: đúng là tách file.
LEGACY_ALLOWANCE = {
"ui/workspace_tab.py": 566,
"ui/widgets.py": 505,
"ui/widgets.py": 466,
"ui/task_editor_dialog.py": 627,
"ui/accounts_tab.py": 559,
"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:
# The MCP SDK is a RUNTIME dependency (requirements.txt) and is deliberately
# absent from requirements-test.txt, which is all CI installs. Importing it at
# module scope aborted collection for the ENTIRE suite, so the guard lives here,
# inside the only test that touches the SDK. Guarding per-test rather than
# per-module keeps the other cases -- pure-Python contract checks that need no
# SDK -- running on CI instead of silently skipping with it.
# Importing the MCP SDK at module scope aborted collection for the ENTIRE
# suite whenever the SDK was missing, so the guard lives here, inside the only
# test that touches it. Guarding per-test rather than per-module keeps the
# other cases -- pure-Python contract checks that need no SDK -- running
# 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")
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 .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,
)
from .theme_qss import _TEMPLATE
from .qss import _TEMPLATE
from dataclasses import dataclass, asdict
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
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
@@ -12,7 +12,7 @@ from dataclasses import dataclass, asdict
from string import Template
from .theme_qss_controls import QSS_CONTROLS
from .qss_controls import QSS_CONTROLS
_QSS_SHELL = """
/* ---- reset ------------------------------------------------------------ */
@@ -1,10 +1,10 @@
"""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.
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
+1 -1
View File
@@ -26,7 +26,7 @@ class SegmentedControl(QWidget):
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ì
#: phải đổi cả ở đây, nếu không chữ lại bị cắt.
_CHECKED_WEIGHT = QFont.DemiBold