## Summary What changed and why? ## Change Type - [ ] 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. Reviewed-on: #11 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"""Cổng project: Cowork và GraphRAG chỉ hiện khi đã chọn một project cụ thể.
|
||||
|
||||
Cổng có hai mặt và trước đây chỉ mặt thứ nhất làm đúng:
|
||||
|
||||
* **Sub-tab trong màn Workspace** — ``_update_tab_visibility`` vốn đã ẩn/hiện
|
||||
đúng. Chỗ hỏng nằm ở ``refresh()``: nó mặc định ``row_to_select = 0`` nên lúc
|
||||
mở app (chưa ai bấm gì) danh sách tự chọn hộ project đầu tiên, mở cổng cho một
|
||||
project người dùng chưa hề chọn.
|
||||
* **Hàng trên menu trái** — ``NavRailMixin._rebuild_nav`` từng dựng hàng ở dạng
|
||||
mờ kèm tooltip thay vì bỏ đi ("shown instead of hidden"), nên người dùng vẫn
|
||||
thấy Cowork/GraphRAG trên menu dù cổng đang đóng.
|
||||
|
||||
Các bài dưới đây chốt cả hai mặt, ở cả ba trạng thái: chưa chọn → ẩn, chọn rồi →
|
||||
hiện, bỏ chọn → ẩn lại.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PySide6", reason="cần PySide6 để dựng cửa sổ thật")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def win(qapp, tmp_path):
|
||||
"""MainWindow thật — cần cả cửa sổ vì phải kiểm cả menu trái.
|
||||
|
||||
Đọc project từ ``~/.cowork_local`` như bản cài thật (``core/projects.py``
|
||||
gắn ``PROJECTS_DIR`` vào đó) nên các bài này KHÔNG tạo/xoá project nào.
|
||||
Bài nào cần cổng MỞ thì gọi thẳng ``_update_tab_visibility(True)`` thay vì
|
||||
tạo project trên đĩa của người chạy test.
|
||||
"""
|
||||
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||
from cowork_local.presentation.shell.main_window import MainWindow
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
build_config(config_path)
|
||||
window = MainWindow(build_context(config_path))
|
||||
yield window
|
||||
window.close()
|
||||
|
||||
|
||||
def _cong(ws):
|
||||
"""Hai sub-tab nằm sau cổng project, bỏ qua bản dựng không có chúng."""
|
||||
return [(ten, idx) for ten, idx in
|
||||
(("Cowork", ws._cowork_tab_idx), ("GraphRAG", ws._graphrag_tab_idx))
|
||||
if idx >= 0]
|
||||
|
||||
|
||||
def _hang_menu(win):
|
||||
"""Nhãn của mọi hàng đang có trên cột menu trái."""
|
||||
return [win.nav.topLevelItem(i).text(0) for i in range(win.nav.topLevelItemCount())]
|
||||
|
||||
|
||||
# ---- mặt 1: không tự chọn hộ project ------------------------------------
|
||||
|
||||
def test_mo_app_len_chua_chon_thi_khong_tu_chon_ho(win):
|
||||
"""Đây là nguyên nhân gốc: ``refresh()`` từng mặc định chọn dòng 0."""
|
||||
ws = win.workspace
|
||||
|
||||
assert ws._current_id == ""
|
||||
assert ws.project_list.currentRow() == -1
|
||||
|
||||
|
||||
def test_chua_chon_project_thi_hai_sub_tab_deu_an(win):
|
||||
ws = win.workspace
|
||||
|
||||
for ten, idx in _cong(ws):
|
||||
assert ws.tabs.isTabVisible(idx) is False, f"{ten} hiện khi chưa chọn project"
|
||||
assert ws.subtab_available(idx) is False, f"{ten} vẫn mở cổng"
|
||||
|
||||
|
||||
def test_chua_chon_project_thi_dung_o_tab_project(win):
|
||||
"""Ẩn hai tab kia mà lại đứng ở một tab đã ẩn thì màn hình trống trơn."""
|
||||
ws = win.workspace
|
||||
|
||||
assert ws.current_subtab() == ws._project_tab_idx
|
||||
|
||||
|
||||
def test_refresh_giu_nguyen_project_dang_chon(win):
|
||||
"""Sửa cổng không được làm mất lựa chọn hiện có: ``keep`` vẫn phải thắng."""
|
||||
ws = win.workspace
|
||||
if ws.project_list.count() == 0:
|
||||
pytest.skip("máy chạy test chưa có project nào để chọn")
|
||||
|
||||
ws.project_list.setCurrentRow(0)
|
||||
dang_chon = ws._current_id
|
||||
|
||||
ws.refresh()
|
||||
|
||||
assert ws._current_id == dang_chon
|
||||
assert ws.project_list.currentRow() >= 0
|
||||
|
||||
|
||||
# ---- mặt 2: menu trái bỏ hẳn hàng, không hiện dạng mờ -------------------
|
||||
|
||||
def test_chua_chon_project_thi_menu_trai_khong_co_hai_hang(win):
|
||||
"""Đây là thứ người dùng nhìn thấy — trước đây hai hàng vẫn nằm đó, chỉ mờ."""
|
||||
nhan = _hang_menu(win)
|
||||
|
||||
assert "Cowork" not in nhan, f"Cowork vẫn trên menu: {nhan}"
|
||||
assert "GraphRAG" not in nhan, f"GraphRAG vẫn trên menu: {nhan}"
|
||||
|
||||
|
||||
def test_mo_cong_thi_hai_hang_quay_lai_menu_trai(win):
|
||||
"""Bỏ hàng phải đảo ngược được, nếu không thì chọn project xong vẫn kẹt."""
|
||||
ws = win.workspace
|
||||
ws._current_id = "gia-lap"
|
||||
ws._update_tab_visibility(True)
|
||||
|
||||
nhan = _hang_menu(win)
|
||||
assert "Cowork" in nhan, f"Cowork không quay lại: {nhan}"
|
||||
assert "GraphRAG" in nhan, f"GraphRAG không quay lại: {nhan}"
|
||||
|
||||
|
||||
def test_mo_cong_thi_hai_sub_tab_cung_hien_lai(win):
|
||||
ws = win.workspace
|
||||
ws._current_id = "gia-lap"
|
||||
ws._update_tab_visibility(True)
|
||||
|
||||
for ten, idx in _cong(ws):
|
||||
assert ws.tabs.isTabVisible(idx) is True, f"{ten} vẫn ẩn khi cổng đã mở"
|
||||
|
||||
|
||||
def test_dong_cong_lai_thi_hai_hang_bien_mat(win):
|
||||
"""Cổng phải đóng lại được, không chỉ mở một chiều."""
|
||||
ws = win.workspace
|
||||
ws._current_id = "gia-lap"
|
||||
ws._update_tab_visibility(True)
|
||||
ws._current_id = ""
|
||||
ws._update_tab_visibility(False)
|
||||
|
||||
nhan = _hang_menu(win)
|
||||
assert "Cowork" not in nhan and "GraphRAG" not in nhan, nhan
|
||||
|
||||
|
||||
def test_cac_hang_khac_khong_bi_anh_huong(win):
|
||||
"""Chỉ hai hàng sau cổng bị bỏ — phần còn lại của menu giữ nguyên."""
|
||||
nhan = _hang_menu(win)
|
||||
|
||||
for bat_buoc in ("Project", "Co4E"):
|
||||
assert bat_buoc in nhan, f"{bat_buoc} biến mất khỏi menu: {nhan}"
|
||||
@@ -1,23 +1,21 @@
|
||||
"""Sandbox Security unlock — chốt các đường KHÔNG được mở khoá (SEC-20260907-01).
|
||||
"""Sandbox Security Layer: bốn công tắc luôn sửa được, không còn khoá mật khẩu.
|
||||
|
||||
``DEFAULT_CONFIG`` ship ``agent_security.sandbox_pw = ""`` kể từ commit
|
||||
``3827552 fix(security): remove shared unlock defaults``, và cấu hình đưa tới
|
||||
dialog LUÔN được deep-merge với defaults đó
|
||||
(``infrastructure/config/json_config_repository.py``). Nghĩa là trên mọi bản cài
|
||||
không đặt ``COWORK_SANDBOX_PASSWORD``, mật khẩu đã lưu là chuỗi rỗng — và phép so
|
||||
sánh ``pw == self._sandbox_pw`` nhận luôn ô nhập trống.
|
||||
Trước đây nhóm này bị khoá: bốn công tắc dựng ra ở trạng thái ``setEnabled(False)``
|
||||
và chỉ mở khi nhập đúng mật khẩu qua ``_sandbox_unlock()``. Bộ bài cũ ở file này
|
||||
(SEC-20260907-01) chốt các đường KHÔNG được mở khoá — chúng mất đối tượng kiểm khi
|
||||
tính năng khoá bị bỏ theo yêu cầu, nên được thay bằng các bài dưới đây.
|
||||
|
||||
Ba nhóm bài ở đây:
|
||||
Docstring của ``_sandbox_unlock()`` cũ đã tự nói rõ nó là gì: *"khoá phía giao diện
|
||||
để chặn bấm nhầm vào một mục nhạy cảm, KHÔNG phải cơ chế bảo mật thật"*. Rào thật
|
||||
nằm ở tầng sandbox lúc chạy lệnh, không ở hộp thoại Cài đặt.
|
||||
|
||||
* **đường tấn công** — chốt đúng lỗ trên;
|
||||
* **đường đi đúng** — bản vá không được phá, kể cả với mật khẩu có dấu;
|
||||
* **chặn cả lớp lỗi** — commit ``3827552`` sửa ``config.py`` nhưng bỏ sót bản sao
|
||||
thứ hai của literal trong ``ui/settings_dialog.py``. Bài cuối quét chéo mọi thư
|
||||
mục nguồn để lần sau không sót kiểu đó nữa.
|
||||
Hai nhóm bài:
|
||||
|
||||
* **hành vi mới** — mở hộp thoại là bật/tắt được ngay, không qua bước nào;
|
||||
* **guardrail** — quét mã nguồn để lần sau không ai lặng lẽ khoá lại.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -27,209 +25,76 @@ import pytest
|
||||
from .test_settings_dialog_dac_ta import _Ctx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shown(monkeypatch):
|
||||
"""Ghi lại mọi QMessageBox thay vì bật modal thật (modal sẽ treo test).
|
||||
|
||||
Trả về list các ``(loại, tiêu_đề, nội_dung)`` — cần thiết để phân biệt
|
||||
"chưa cấu hình mật khẩu" với "sai mật khẩu"; nếu chỉ nuốt hộp thoại đi thì
|
||||
hai nhánh gộp lại làm một mà test vẫn xanh.
|
||||
"""
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
calls: list[tuple[str, str, str]] = []
|
||||
|
||||
def _record(kind):
|
||||
def _fn(_parent, title, text, *a, **k):
|
||||
calls.append((kind, title, text))
|
||||
return staticmethod(_fn)
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "warning", _record("warning"))
|
||||
monkeypatch.setattr(QMessageBox, "information", _record("information"))
|
||||
return calls
|
||||
|
||||
|
||||
def _dialog(stored_pw: str):
|
||||
"""SettingsDialog với ``sandbox_pw`` đúng như bản cài thật: key CÓ mặt."""
|
||||
def _dialog():
|
||||
"""SettingsDialog dựng đúng như bản cài thật."""
|
||||
from cowork_local.ui.settings_dialog import SettingsDialog
|
||||
ctx = _Ctx()
|
||||
ctx.config.data["agent_security"]["sandbox_pw"] = stored_pw
|
||||
return SettingsDialog(ctx)
|
||||
return SettingsDialog(_Ctx())
|
||||
|
||||
|
||||
# ---- đường tấn công ------------------------------------------------------
|
||||
|
||||
def test_o_trong_khong_mo_duoc_khoa(qapp, shown):
|
||||
"""Chưa đặt mật khẩu (sandbox_pw == "") thì ô nhập trống KHÔNG được mở khoá."""
|
||||
dlg = _dialog("")
|
||||
dlg.sandbox_pw_edit.setText("")
|
||||
|
||||
dlg._sandbox_unlock()
|
||||
|
||||
assert dlg._sandbox_unlocked is False
|
||||
dlg.deleteLater()
|
||||
_CONG_TAC = ("sandbox_confirm", "sandbox_block_network", "sec_enabled", "ai_check")
|
||||
|
||||
|
||||
def test_go_bua_khi_chua_dat_mat_khau_cung_khong_mo_duoc(qapp, shown):
|
||||
"""Mật khẩu lưu rỗng thì KHÔNG chuỗi nào mở được, kể cả chuỗi khác rỗng."""
|
||||
dlg = _dialog("")
|
||||
dlg.sandbox_pw_edit.setText("bat ky")
|
||||
# ---- hành vi mới: sửa được ngay, không cần mật khẩu ----------------------
|
||||
|
||||
dlg._sandbox_unlock()
|
||||
@pytest.mark.parametrize("ten", _CONG_TAC)
|
||||
def test_cong_tac_sua_duoc_ngay_khi_mo_hop_thoai(qapp, ten):
|
||||
"""Đây là chính yêu cầu: không còn bước nhập mật khẩu nào chắn ở giữa."""
|
||||
dlg = _dialog()
|
||||
|
||||
assert dlg._sandbox_unlocked is False
|
||||
dlg.deleteLater()
|
||||
assert getattr(dlg, ten).isEnabled() is True, f"{ten} vẫn bị khoá"
|
||||
|
||||
|
||||
def test_mat_khau_sai_khong_mo_duoc(qapp, shown):
|
||||
"""Đã đặt mật khẩu thì gõ sai vẫn khoá."""
|
||||
dlg = _dialog("K7MNP2QRSTVW")
|
||||
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVX")
|
||||
@pytest.mark.parametrize("ten", _CONG_TAC)
|
||||
def test_bat_tat_duoc_va_luu_dung_gia_tri(qapp, ten):
|
||||
"""Bật/tắt phải ăn vào widget — khoá cũ chặn đúng ở bước này."""
|
||||
dlg = _dialog()
|
||||
w = getattr(dlg, ten)
|
||||
|
||||
dlg._sandbox_unlock()
|
||||
|
||||
assert dlg._sandbox_unlocked is False
|
||||
dlg.deleteLater()
|
||||
truoc = w.isChecked()
|
||||
w.setChecked(not truoc)
|
||||
assert w.isChecked() is (not truoc)
|
||||
w.setChecked(truoc)
|
||||
assert w.isChecked() is truoc
|
||||
|
||||
|
||||
# ---- thông báo phải phân biệt được hai tình huống -------------------------
|
||||
def test_khong_con_widget_mat_khau_nao(qapp):
|
||||
"""Ô nhập, nút Mở khoá và nhãn "Đang khoá" phải biến mất khỏi hộp thoại."""
|
||||
dlg = _dialog()
|
||||
|
||||
def test_chua_cau_hinh_bao_khac_voi_sai_mat_khau(qapp, shown):
|
||||
"""Hai nhánh phải nói hai chuyện khác nhau.
|
||||
|
||||
Người chưa từng đặt mật khẩu mà nhận "Password incorrect" sẽ gõ lại mãi một
|
||||
thứ không tồn tại. Không có bài này thì gộp hai nhánh về một thông báo chung
|
||||
vẫn xanh hết.
|
||||
"""
|
||||
from cowork_local.i18n import tr
|
||||
|
||||
dlg = _dialog("")
|
||||
dlg.sandbox_pw_edit.setText("")
|
||||
dlg._sandbox_unlock()
|
||||
chua_cau_hinh = list(shown)
|
||||
dlg.deleteLater()
|
||||
|
||||
shown.clear()
|
||||
dlg2 = _dialog("K7MNP2QRSTVW")
|
||||
dlg2.sandbox_pw_edit.setText("sai roi")
|
||||
dlg2._sandbox_unlock()
|
||||
sai_mat_khau = list(shown)
|
||||
dlg2.deleteLater()
|
||||
|
||||
assert len(chua_cau_hinh) == 1, "phải hiện đúng một thông báo"
|
||||
assert len(sai_mat_khau) == 1
|
||||
assert chua_cau_hinh[0][2] == tr("settings.sandbox_pw_unset_body")
|
||||
assert chua_cau_hinh[0][2] != sai_mat_khau[0][2], (
|
||||
"chưa cấu hình mật khẩu và sai mật khẩu phải là hai thông báo khác nhau")
|
||||
for ten in ("sandbox_pw_edit", "sandbox_unlock_btn", "sandbox_locked_status",
|
||||
"sandbox_pw_label"):
|
||||
assert not hasattr(dlg, ten), f"{ten} vẫn còn trên hộp thoại"
|
||||
|
||||
|
||||
# ---- đường đi đúng vẫn phải chạy ----------------------------------------
|
||||
def test_khong_con_duong_mo_khoa_trong_ma(qapp):
|
||||
"""Hàm mở khoá và cờ trạng thái khoá không còn tồn tại."""
|
||||
import cowork_local.ui.settings_dialog as mod
|
||||
|
||||
def test_mat_khau_dung_van_mo_duoc(qapp, shown):
|
||||
"""Bản vá không được phá đường đi hợp lệ."""
|
||||
dlg = _dialog("K7MNP2QRSTVW")
|
||||
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVW")
|
||||
|
||||
dlg._sandbox_unlock()
|
||||
|
||||
assert dlg._sandbox_unlocked is True
|
||||
dlg.deleteLater()
|
||||
dlg = _dialog()
|
||||
assert not hasattr(dlg, "_sandbox_unlock")
|
||||
assert not hasattr(dlg, "_sandbox_unlocked")
|
||||
assert not hasattr(dlg, "_sandbox_widgets")
|
||||
assert not hasattr(mod, "_sandbox_password_matches")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pw", ["mật khẩu", "パスワード", "sénhà-2026"])
|
||||
def test_mat_khau_co_dau_khong_lam_crash(qapp, shown, pw):
|
||||
"""``secrets.compare_digest`` ném TypeError nếu str có ký tự ngoài ASCII.
|
||||
# ---- guardrail: không ai khoá lại mà không sửa bài test này --------------
|
||||
|
||||
App mặc định tiếng Việt và phục vụ khách Nhật, nên chữ có dấu trong ô mật
|
||||
khẩu là input bình thường. Phải so sánh trên bytes.
|
||||
"""
|
||||
dlg = _dialog(pw)
|
||||
dlg.sandbox_pw_edit.setText(pw)
|
||||
def test_ma_nguon_khong_con_khoa_nhom_sandbox():
|
||||
"""Chặn cả lớp lỗi: lần sau ai thêm lại ``setEnabled(False)`` cho nhóm này
|
||||
thì bài này đỏ ngay, không đợi có người mở app mới thấy."""
|
||||
src = (Path(__file__).resolve().parents[2]
|
||||
/ "ui" / "settings_dialog.py").read_text(encoding="utf-8")
|
||||
code = "\n".join(l for l in src.splitlines() if not l.strip().startswith("#"))
|
||||
|
||||
dlg._sandbox_unlock() # không được ném TypeError
|
||||
|
||||
assert dlg._sandbox_unlocked is True
|
||||
dlg.deleteLater()
|
||||
for dau_hieu in ("_sandbox_unlock", "_sandbox_widgets", "_sandbox_unlocked",
|
||||
"sandbox_pw"):
|
||||
assert dau_hieu not in code, f"khoá sandbox đã quay lại: {dau_hieu}"
|
||||
|
||||
|
||||
def test_mat_khau_co_dau_sai_thi_van_khoa(qapp, shown):
|
||||
"""Chữ có dấu không được biến thành đường mở khoá dễ dãi."""
|
||||
dlg = _dialog("mật khẩu")
|
||||
dlg.sandbox_pw_edit.setText("mat khau")
|
||||
def test_phep_quet_thuc_su_doc_duoc_file():
|
||||
"""Lưới an toàn: đổi tên file làm bài trên quét rỗng mà vẫn xanh."""
|
||||
src = (Path(__file__).resolve().parents[2]
|
||||
/ "ui" / "settings_dialog.py").read_text(encoding="utf-8")
|
||||
|
||||
dlg._sandbox_unlock()
|
||||
|
||||
assert dlg._sandbox_unlocked is False
|
||||
dlg.deleteLater()
|
||||
|
||||
|
||||
# ---- hàm so khớp, gọi thẳng ----------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("entered,stored,expected", [
|
||||
("", "", False), # cả hai rỗng
|
||||
("", "K7MNP2QRSTVW", False), # ô nhập rỗng
|
||||
("K7MNP2QRSTVW", "", False), # chưa đặt mật khẩu — nhánh phòng thủ
|
||||
("K7MNP2QRSTVW", "K7MNP2QRSTVW", True),
|
||||
("mật khẩu", "mật khẩu", True), # ngoài ASCII
|
||||
("mật khẩu", "mat khau", False),
|
||||
])
|
||||
def test_ham_so_khop(entered, stored, expected):
|
||||
"""Gọi thẳng ``_sandbox_password_matches`` — phủ cả nhánh mà call site đã
|
||||
chặn trước bằng return sớm."""
|
||||
from cowork_local.ui.settings_dialog import _sandbox_password_matches
|
||||
assert _sandbox_password_matches(entered, stored) is expected
|
||||
|
||||
|
||||
# ---- chặn cả lớp lỗi -----------------------------------------------------
|
||||
|
||||
#: ``.get("<khoá kiểu credential>", "<literal khác rỗng>")`` — mặc định trông có
|
||||
#: vẻ an toàn nhưng thực ra là credential nằm trong mã nguồn. Nó cũng là code
|
||||
#: chết: cấu hình đã deep-merge với DEFAULT_CONFIG nên key luôn tồn tại.
|
||||
#:
|
||||
#: Cố ý KHÔNG bắt ``key`` và ``code`` trần: ``it.get("key", "?")`` của Jira
|
||||
#: (``core/jira_tool.py``) là mã issue, không phải credential. Danh sách dưới đây
|
||||
#: chỉ gồm tên đã mang nghĩa bí mật.
|
||||
_CREDENTIAL_FALLBACK = re.compile(
|
||||
r'\.get\(\s*["\'][a-z_]*'
|
||||
r'(?:pw|passwd|password|secret|token|api_key|unlock_code|access_code)'
|
||||
r'[a-z_]*["\']\s*,\s*["\'][^"\']+["\']'
|
||||
)
|
||||
|
||||
#: Quét CHÉO mọi thư mục nguồn, không chỉ tầng giao diện. Sai sót gốc của commit
|
||||
#: ``3827552`` là sửa ``config.py`` mà quên bản sao trong ``ui/`` — tức là lỗi đi
|
||||
#: xuyên thư mục, nên phép quét cũng phải đi xuyên thư mục.
|
||||
_SCANNED = (
|
||||
"ui", "presentation", "core", "infrastructure", "application", "domain",
|
||||
"mcp_servers", "providers", "security", "theme", "config.py", "state.py",
|
||||
)
|
||||
|
||||
|
||||
def test_khong_con_fallback_credential_trong_ma_nguon():
|
||||
"""Không file nguồn nào được đặt credential làm giá trị mặc định của ``.get()``."""
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
offenders = []
|
||||
for name in _SCANNED:
|
||||
target = root / name
|
||||
if target.is_file():
|
||||
files = [target]
|
||||
elif target.is_dir():
|
||||
files = [p for p in target.rglob("*.py") if "__pycache__" not in p.parts]
|
||||
else: # thư mục bị đổi tên/xoá
|
||||
continue
|
||||
for path in files:
|
||||
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if _CREDENTIAL_FALLBACK.search(line):
|
||||
offenders.append(
|
||||
f"{path.relative_to(root).as_posix()}:{lineno}: {line.strip()}")
|
||||
|
||||
assert not offenders, "credential nằm trong mã nguồn:\n " + "\n ".join(offenders)
|
||||
|
||||
|
||||
def test_phep_quet_thuc_su_nhin_thay_file():
|
||||
"""Lưới an toàn cho bài trên: đổi tên thư mục làm nó quét rỗng mà vẫn xanh."""
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
seen = sum(
|
||||
1 for name in _SCANNED
|
||||
for _ in ([root / name] if (root / name).is_file()
|
||||
else (root / name).rglob("*.py") if (root / name).is_dir() else [])
|
||||
)
|
||||
assert seen > 200, f"chỉ quét được {seen} file — phạm vi quét đã hỏng"
|
||||
assert "class SettingsDialog" in src
|
||||
assert len(src) > 2000, f"chỉ đọc được {len(src)} ký tự — đường dẫn đã hỏng"
|
||||
|
||||
@@ -128,19 +128,24 @@ def test_bam_project_tren_thanh_menu_an_ngay_lan_dau(window):
|
||||
|
||||
|
||||
def test_khi_cong_project_MO_thi_ha_canh_o_cowork_va_vet_sang_theo(window):
|
||||
"""Nhánh của người dùng ĐÃ có project — nhánh mà bug được báo.
|
||||
"""Nhánh của người dùng ĐÃ chọn một project — nhánh mà bug được báo.
|
||||
|
||||
Môi trường test không có project nào (cố ý: ``core/projects.py`` ghi vào
|
||||
``~/.cowork_local`` thật). Mở cổng bằng tay để đi đúng nhánh đó mà không
|
||||
phải tạo project trên đĩa.
|
||||
Trước đây bài này mở cổng bằng cửa sau ``setTabVisible(True)`` vì môi trường
|
||||
test không có project nào (``core/projects.py`` ghi vào ``~/.cowork_local``
|
||||
thật, nên test không tạo project). Cửa sau đó hết tác dụng từ khi cổng được
|
||||
điều khiển bằng ``_current_id``: ``refresh()``/``goto_all_projects()`` đóng
|
||||
lại ngay. Giờ mở cổng bằng đúng đường thật — chọn một project — và bỏ qua
|
||||
bài này trên máy chưa có project nào.
|
||||
"""
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
ws = window.workspace
|
||||
if ws._cowork_tab_idx < 0:
|
||||
pytest.skip("bản dựng này không có sub-tab Cowork")
|
||||
if ws.project_list.count() == 0:
|
||||
pytest.skip("máy chạy test chưa có project nào — cổng không mở được")
|
||||
|
||||
ws.tabs.setTabVisible(ws._cowork_tab_idx, True)
|
||||
ws.project_list.setCurrentRow(0)
|
||||
try:
|
||||
window.goto_all_projects()
|
||||
|
||||
@@ -154,5 +159,5 @@ def test_khi_cong_project_MO_thi_ha_canh_o_cowork_va_vet_sang_theo(window):
|
||||
f"nội dung ở Cowork ({ws._cowork_tab_idx}) "
|
||||
f"nhưng thanh menu sáng ở {data.get('sub')}")
|
||||
finally:
|
||||
ws.tabs.setTabVisible(ws._cowork_tab_idx, False)
|
||||
ws.project_list.setCurrentRow(-1) # đóng cổng lại đúng đường thật
|
||||
window.goto_all_projects()
|
||||
|
||||
Reference in New Issue
Block a user