diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index dce0038..8f68dce 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -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: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e92ee4..fa49083 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ``` diff --git a/README.md b/README.md index 469d9f8..f90b846 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/i18n.py b/i18n/__init__.py similarity index 77% rename from i18n.py rename to i18n/__init__.py index 9f0267f..20f004a 100644 --- a/i18n.py +++ b/i18n/__init__.py @@ -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, } diff --git a/i18n_agents_admin_tab.py b/i18n/agents_admin_tab.py similarity index 100% rename from i18n_agents_admin_tab.py rename to i18n/agents_admin_tab.py diff --git a/i18n_composer.py b/i18n/composer.py similarity index 100% rename from i18n_composer.py rename to i18n/composer.py diff --git a/i18n_cowork_tab.py b/i18n/cowork_tab.py similarity index 100% rename from i18n_cowork_tab.py rename to i18n/cowork_tab.py diff --git a/i18n_hint.py b/i18n/hint.py similarity index 100% rename from i18n_hint.py rename to i18n/hint.py diff --git a/i18n_libreoffice_view.py b/i18n/libreoffice_view.py similarity index 100% rename from i18n_libreoffice_view.py rename to i18n/libreoffice_view.py diff --git a/i18n_login_dialog.py b/i18n/login_dialog.py similarity index 100% rename from i18n_login_dialog.py rename to i18n/login_dialog.py diff --git a/i18n_monitoring_overview.py b/i18n/monitoring_overview.py similarity index 100% rename from i18n_monitoring_overview.py rename to i18n/monitoring_overview.py diff --git a/i18n_settings_dialog.py b/i18n/settings_dialog.py similarity index 100% rename from i18n_settings_dialog.py rename to i18n/settings_dialog.py diff --git a/i18n_sidebar.py b/i18n/sidebar.py similarity index 100% rename from i18n_sidebar.py rename to i18n/sidebar.py diff --git a/i18n_skills_dialog.py b/i18n/skills_dialog.py similarity index 100% rename from i18n_skills_dialog.py rename to i18n/skills_dialog.py diff --git a/install.bat b/install.bat index c689691..cb1787a 100644 --- a/install.bat +++ b/install.bat @@ -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. diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index e3b9720..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pydantic>=2,<3 -pytest>=8,<10 diff --git a/requirements.txt b/requirements.txt index 3151a94..f25beab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/check_loc.py b/scripts/check_loc.py index 03eebbd..4bdc5b6 100644 --- a/scripts/check_loc.py +++ b/scripts/check_loc.py @@ -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, diff --git a/tests/test_project_context_mcp_template.py b/tests/test_project_context_mcp_template.py index b88f435..55ca6d1 100644 --- a/tests/test_project_context_mcp_template.py +++ b/tests/test_project_context_mcp_template.py @@ -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 diff --git a/theme.py b/theme/__init__.py similarity index 97% rename from theme.py rename to theme/__init__.py index 21ef8d4..2adc35d 100644 --- a/theme.py +++ b/theme/__init__.py @@ -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 diff --git a/theme_palettes.py b/theme/palettes.py similarity index 100% rename from theme_palettes.py rename to theme/palettes.py diff --git a/theme_qss.py b/theme/qss.py similarity index 98% rename from theme_qss.py rename to theme/qss.py index 6e6c753..ee76110 100644 --- a/theme_qss.py +++ b/theme/qss.py @@ -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 ------------------------------------------------------------ */ diff --git a/theme_qss_controls.py b/theme/qss_controls.py similarity index 98% rename from theme_qss_controls.py rename to theme/qss_controls.py index b8b0075..2f89ebf 100644 --- a/theme_qss_controls.py +++ b/theme/qss_controls.py @@ -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 diff --git a/ui/segmented_control.py b/ui/segmented_control.py index 6b8df2d..46a04f5 100644 --- a/ui/segmented_control.py +++ b/ui/segmented_control.py @@ -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