Files
cowork-local/tests/test_config_gan_duoc.py
T
Nam Pham Dinh ThanhandClaude Opus 5 bc282c71d0 refactor(config): AppConfig thành vỏ mỏng trên repository + vá 3 chỗ gán im lặng hỏng
config.py 623 -> 377 dòng (qua ngưỡng 400 của CASAN Check 2).

Class AppConfig 278 dòng giờ còn 30: mọi lối vào dẫn tới JsonConfigRepository.
Không xoá hẳn vì cái tên còn nằm ở 41 file — 23 checker trong tools/ và 18 file
test, trong đó có test của cả ba người. Sửa 41 chỗ trong một commit là đổi thứ
không cần đổi và làm review không đọc nổi. Giữ tên, đổi ruột.

Thêm JsonConfigRepository.from_data() cho dạng AppConfig(data=..., path=...) mà
13 file test đang dùng: dựng thẳng từ dict, không đọc đĩa, không chạy migration
trên dữ liệu test.

MỘT LỖI TÔI GÂY RA HÔM 25/08, HÔM NAY MỚI LỘ
---------------------------------------------
Lúc tráo R02 tôi có đối chiếu API và kết luận "đủ 34/34 thành viên, thay được".
Đối chiếu đó chỉ so TÊN, không so việc một property có setter hay không.

AppConfig cũ là dataclass nên `config.language = "vi"` chạy bình thường.
Repository để language là property chỉ đọc -> gán vào là AttributeError. Ba chỗ
trong app.py đang gán: đổi ngôn ngữ, đổi giao diện, đổi provider trên thanh bên.

Khó thấy vì cả ba nằm trong slot của Qt, mà Qt NUỐT ngoại lệ trong slot. Không
traceback, không thông báo — bấm đổi ngôn ngữ thì không có gì xảy ra. 709 test
đơn vị vẫn xanh suốt. Chỉ check_nav bắt được vì nó bấm thật vào combo rồi kiểm.

Thêm setter cho theme/language/active_provider, và tests/test_config_gan_duoc.py
đi ngược từ mã nguồn: quét cả repo tìm mọi chỗ `config.X = ...` rồi thử gán
thật. Đã kiểm ngược — bỏ setter đi thì 2 bài đỏ.

BẮC CẦU CHO 55 CONTROL MONITORING
----------------------------------
check_controls_alive so với mốc git 291a611 và đòi 55 control ov_* của Tổng
quan phải còn tới được. Sau khi Hiệp tách 8 tab, chúng về đúng tab/thẻ của mình
và rụng tiền tố -> 3 checker đỏ.

Control còn đủ, chỉ đổi chỗ ở. Bắc cầu bằng __getattr__ định tuyến theo tiền tố
(ov_perm_ -> permissions_card, ov_sbx_ -> sandbox_card, ov_price_/ov_pricing_ ->
pricing_panel, còn lại -> overview_tab), cộng 3 hộp nhóm mà bản thân widget con
chính là hộp đó.

Định tuyến theo tiền tố chứ không dò mờ: overview_tab và permissions_card đều
có network_lbl — một cái là mức dùng mạng, một cái là quyền truy cập mạng. Bản
dò mờ đầu tiên tôi viết vớ nhầm cái đầu tiên tìm thấy.

714 test xanh. 24/24 checker qua (3 cái đã đỏ từ trước khi tôi bắt đầu, do phần
monitoring, nay xanh lại). CASAN Check 1 sạch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 01:03:28 +09:00

98 lines
3.9 KiB
Python

"""Mọi chỗ trong repo gán ``config.X = ...`` thì repository phải nhận được.
Bài này sinh ra từ một lỗi thật ngày 26/08.
R02 tráo ``config.py::AppConfig`` bằng ``JsonConfigRepository``. Trước khi
tráo tôi có đối chiếu API: đếm đủ 34/34 thành viên công khai, không thiếu cái
nào, nên kết luận là thay được. Đối chiếu đó **chỉ so tên**, không so việc một
``property`` có setter hay không.
``AppConfig`` cũ là dataclass, ``config.language = "vi"`` chạy bình thường.
Repository để ``language`` là property chỉ đọc, gán vào là ``AttributeError``.
Ba chỗ trong ``app.py`` đang gán như thế: đổi ngôn ngữ, đổi giao diện, đổi
provider trên thanh bên.
Điều làm nó khó thấy: cả ba đều nằm trong slot của Qt, mà Qt **nuốt ngoại lệ
trong slot**. Không có traceback, không có thông báo — người dùng bấm đổi ngôn
ngữ thì không có gì xảy ra. Bộ test đơn vị vẫn 709 xanh; chỉ ``check_nav`` bắt
được vì nó bấm thật vào combo rồi kiểm ngôn ngữ có đổi không.
Nên bài này đi ngược từ mã nguồn: tìm mọi chỗ gán, rồi thử gán thật.
"""
from __future__ import annotations
import copy
import re
import subprocess
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
#: ``config.X = ...`` nhưng không phải ``==``.
GAN = re.compile(r"\bconfig\.([a-z_][a-z_0-9]*)\s*=(?!=)")
#: Không phải thuộc tính cấu hình — là chỗ chứa chính đối tượng config.
BO_QUA = {"data", "config"}
def _cho_gan() -> set[str]:
out = subprocess.run(["git", "ls-files", "*.py"], cwd=REPO,
capture_output=True, text=True, encoding="utf-8",
errors="replace").stdout.split()
ten: set[str] = set()
for f in out:
p = REPO / f
if not p.is_file():
continue
for m in GAN.finditer(p.read_text(encoding="utf-8", errors="replace")):
if m.group(1) not in BO_QUA and not m.group(1).startswith("_"):
ten.add(m.group(1))
return ten
def _repo(tmp_path):
from cowork_local.config import DEFAULT_CONFIG
from cowork_local.infrastructure.config.json_config_repository import (
JsonConfigRepository,
)
return JsonConfigRepository.from_data(copy.deepcopy(DEFAULT_CONFIG),
tmp_path / "config.json")
def test_tim_duoc_cho_gan():
"""Bảo vệ chính bài test: biểu thức tìm kiếm hỏng thì nó lặng lẽ xanh."""
ten = _cho_gan()
assert ten, "không tìm thấy chỗ nào gán config.X — kiểm lại GAN"
assert "language" in ten, f"phải thấy config.language (thấy: {sorted(ten)})"
def test_moi_thuoc_tinh_bi_gan_deu_gan_duoc(tmp_path):
cfg = _repo(tmp_path)
hong = []
for ten in sorted(_cho_gan()):
if not hasattr(type(cfg), ten) and not hasattr(cfg, ten):
continue # thuộc tính của lớp khác, không phải config
cu = getattr(cfg, ten, None)
try:
setattr(cfg, ten, cu)
except AttributeError:
hong.append(ten)
assert not hong, (
"Repository không nhận gán, nhưng trong mã nguồn có chỗ gán:\n "
+ "\n ".join("config.%s = ..." % t for t in hong)
+ "\nQt nuốt AttributeError trong slot, nên chỗ đó sẽ im lặng không "
"làm gì. Thêm @<tên>.setter vào JsonConfigRepository."
)
@pytest.mark.parametrize("ten,gia_tri", [("language", "ja"), ("theme", "light"),
("active_provider", "ollama")])
def test_ba_cho_app_py_dang_gan(tmp_path, ten, gia_tri):
"""Chốt riêng ba cái app.py gán, để bài trên có hỏng thì vẫn còn lưới."""
cfg = _repo(tmp_path)
setattr(cfg, ten, gia_tri)
assert getattr(cfg, ten) == gia_tri