fix(workspace): đổi thư mục project thì GraphRAG trỏ theo thư mục mới
Ở tab Project bấm "Đổi" sang đường dẫn khác: ô Thư mục làm việc cập nhật ngay, tên project vẫn đúng, nên nhìn qua tưởng xong. Nhưng màn GraphRAG giữ nguyên đường dẫn cũ và quét nhầm thư mục. Nguyên nhân có hai mảnh, thiếu mảnh nào cũng vẫn hỏng: - _pick_folder ghi output_dir rồi dừng. Đổi thư mục không làm project id đổi nên không có gì trong luồng chọn project chạy lại. - Kể cả có chạy lại, StructureGraphView.set_workspace_project chốt theo MỖI project id nên cùng một project là nó thoát ra ngay. Tab Thư mục vốn đã chốt theo ĐƯỜNG DẪN — comment ở đó tự nhận hai nơi cùng luật, thật ra thì không. Thêm ProjectFolderRuleMixin.rebind_workspace_folder để trỏ lại những màn đang bám vào thư mục, gọi từ cả hai đường đổi thư mục ở màn Project. Cố ý không gọi _load_current: hàm đó nạp lại cả biểu mẫu từ đĩa, gọi lúc đang sửa dở Tên/Mô tả là xoá mất phần chưa lưu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,9 @@ class StructureGraphView(QWidget):
|
|||||||
# Project ma man Workspace da ap xuong lan gan nhat. None = chua ap lan
|
# Project ma man Workspace da ap xuong lan gan nhat. None = chua ap lan
|
||||||
# nao, de lan goi dau tien khong bi bo qua ke ca khi pid la chuoi rong.
|
# nao, de lan goi dau tien khong bi bo qua ke ca khi pid la chuoi rong.
|
||||||
self._workspace_project = None
|
self._workspace_project = None
|
||||||
|
# Thư mục của project đó lúc áp gần nhất. Chốt theo CẢ đường dẫn chứ
|
||||||
|
# không chỉ theo id — xem set_workspace_project.
|
||||||
|
self._workspace_dir = None
|
||||||
|
|
||||||
root = QVBoxLayout(self)
|
root = QVBoxLayout(self)
|
||||||
self.renderer = GraphRenderer(ctx)
|
self.renderer = GraphRenderer(ctx)
|
||||||
@@ -208,13 +211,29 @@ class StructureGraphView(QWidget):
|
|||||||
|
|
||||||
Đổi sang project khác ở màn Workspace thì vẫn áp — cùng luật với tab Thư
|
Đổi sang project khác ở màn Workspace thì vẫn áp — cùng luật với tab Thư
|
||||||
mục (``FolderTab.set_project_root``). Chỉ lần refresh trong CÙNG một
|
mục (``FolderTab.set_project_root``). Chỉ lần refresh trong CÙNG một
|
||||||
project là không được đụng.
|
project VÀ cùng một thư mục là không được đụng.
|
||||||
|
|
||||||
|
Chốt theo cả đường dẫn chứ không chỉ theo id: đổi thư mục làm việc ở
|
||||||
|
màn Project không làm id đổi, nên chốt theo mỗi id thì màn này giữ
|
||||||
|
nguyên đường dẫn cũ và quét nhầm thư mục. Tab Thư mục vốn đã chốt theo
|
||||||
|
đường dẫn — đây là đưa hai nơi về đúng cùng một luật như comment này
|
||||||
|
vẫn nói.
|
||||||
"""
|
"""
|
||||||
if project_id == self._workspace_project:
|
thu_muc = self._thu_muc_cua(project_id)
|
||||||
|
if project_id == self._workspace_project and thu_muc == self._workspace_dir:
|
||||||
return
|
return
|
||||||
self._workspace_project = project_id
|
self._workspace_project = project_id
|
||||||
|
self._workspace_dir = thu_muc
|
||||||
self.set_project(project_id)
|
self.set_project(project_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _thu_muc_cua(project_id: str) -> str:
|
||||||
|
"""Thư mục làm việc hiện tại của project, chuỗi rỗng nếu không có."""
|
||||||
|
from cowork_local.core.projects import load_project
|
||||||
|
|
||||||
|
project = load_project(project_id) if project_id else None
|
||||||
|
return str(project.workspace_dir()) if project is not None else ""
|
||||||
|
|
||||||
def prewarm(self) -> None:
|
def prewarm(self) -> None:
|
||||||
"""Dựng sẵn khung đồ thị trước khi người dùng bấm vào, để lần mở đầu không giật."""
|
"""Dựng sẵn khung đồ thị trước khi người dùng bấm vào, để lần mở đầu không giật."""
|
||||||
self.renderer.prewarm()
|
self.renderer.prewarm()
|
||||||
|
|||||||
@@ -79,6 +79,32 @@ class ProjectFolderRuleMixin:
|
|||||||
self._gan_nhan_canh_bao_thu_muc()
|
self._gan_nhan_canh_bao_thu_muc()
|
||||||
self.project_selected.connect(self._sync_folder_warning)
|
self.project_selected.connect(self._sync_folder_warning)
|
||||||
|
|
||||||
|
def rebind_workspace_folder(self) -> None:
|
||||||
|
"""Thư mục làm việc vừa đổi — trỏ lại những màn đang bám vào nó.
|
||||||
|
|
||||||
|
Đổi thư mục KHÔNG làm project id đổi, nên không có gì trong luồng
|
||||||
|
chọn project chạy lại: ``_pick_folder`` ghi ``output_dir`` rồi dừng.
|
||||||
|
Tab Thư mục và màn GraphRAG vì thế giữ nguyên đường dẫn cũ — tên
|
||||||
|
project vẫn đúng nên nhìn qua tưởng ổn, nhưng GraphRAG quét nhầm
|
||||||
|
thư mục.
|
||||||
|
|
||||||
|
Cố ý KHÔNG gọi ``_load_current``: hàm đó nạp lại cả biểu mẫu từ đĩa,
|
||||||
|
nên gọi nó lúc người dùng đang sửa dở Tên/Mô tả là xoá mất phần chưa
|
||||||
|
lưu.
|
||||||
|
"""
|
||||||
|
from ...core.projects import load_project
|
||||||
|
|
||||||
|
pid = getattr(self, "_current_id", "")
|
||||||
|
project = load_project(pid) if pid else None
|
||||||
|
if project is None:
|
||||||
|
return
|
||||||
|
if getattr(self, "_folder", None) is not None:
|
||||||
|
self._folder.set_project_root(str(project.workspace_dir()))
|
||||||
|
if getattr(self, "_structure", None) is not None:
|
||||||
|
self._structure.set_workspace_project(pid)
|
||||||
|
# Thư mục mới có thể vừa gỡ bỏ (hoặc tạo ra) một cảnh báo dùng chung.
|
||||||
|
self._sync_folder_warning()
|
||||||
|
|
||||||
def _gan_nhan_canh_bao_thu_muc(self) -> None:
|
def _gan_nhan_canh_bao_thu_muc(self) -> None:
|
||||||
"""Chèn nhãn cảnh báo ngay DƯỚI hàng chứa ô Thư mục làm việc.
|
"""Chèn nhãn cảnh báo ngay DƯỚI hàng chứa ô Thư mục làm việc.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"""Đổi thư mục làm việc thì màn GraphRAG phải trỏ theo thư mục mới.
|
||||||
|
|
||||||
|
Triệu chứng: ở tab Project bấm "Đổi" sang một đường dẫn khác — ô Thư mục làm
|
||||||
|
việc cập nhật ngay, tên project vẫn đúng, nên nhìn qua tưởng xong. Nhưng màn
|
||||||
|
GraphRAG vẫn giữ đường dẫn cũ và quét nhầm thư mục.
|
||||||
|
|
||||||
|
Nguyên nhân có hai mảnh, thiếu mảnh nào cũng vẫn hỏng:
|
||||||
|
|
||||||
|
* ``_pick_folder`` ghi ``output_dir`` rồi dừng — đổi thư mục không làm project
|
||||||
|
id đổi nên không có gì trong luồng chọn project chạy lại.
|
||||||
|
* Kể cả có chạy lại, ``StructureGraphView.set_workspace_project`` ngày trước
|
||||||
|
chốt theo MỖI project id, nên cùng một project là nó thoát ra ngay. Tab Thư
|
||||||
|
mục vốn đã chốt theo ĐƯỜNG DẪN — hai nơi tưởng cùng luật mà thật ra không.
|
||||||
|
|
||||||
|
Giống ``test_graphrag_project_persists.py``: KHÔNG dựng renderer thật, vì nó
|
||||||
|
kéo theo QtWebEngine — dựng nó trong bộ ``tests/ui`` làm cả bộ chết giữa chừng.
|
||||||
|
Thứ cần chốt ở đây là luồng điều khiển, không phải phần vẽ.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("PySide6", reason="cần PySide6 để dựng widget thật")
|
||||||
|
|
||||||
|
from cowork_local.core import projects as projects_mod
|
||||||
|
from cowork_local.core.projects import Project
|
||||||
|
|
||||||
|
|
||||||
|
class _RendererGhi:
|
||||||
|
"""Thay GraphRenderer — chỉ ghi lại nó bị áp project mấy lần."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lan_ap = []
|
||||||
|
|
||||||
|
def set_project(self, project_id):
|
||||||
|
self.lan_ap.append(project_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def view(qapp):
|
||||||
|
"""``StructureGraphView`` với renderer bị thay, dựng qua ``__new__``."""
|
||||||
|
from cowork_local.presentation.graph.structure_graph_view import StructureGraphView
|
||||||
|
|
||||||
|
v = StructureGraphView.__new__(StructureGraphView)
|
||||||
|
v._workspace_project = None
|
||||||
|
v._workspace_dir = None
|
||||||
|
v.renderer = _RendererGhi()
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def _kho_mot_project(monkeypatch, thu_muc) -> Project:
|
||||||
|
"""Kho project giả gồm đúng một project trỏ vào ``thu_muc``."""
|
||||||
|
du_an = Project(project_id="p1", name="Mynt4Project1", output_dir=str(thu_muc))
|
||||||
|
monkeypatch.setattr(projects_mod, "list_projects", lambda directory=None: [du_an])
|
||||||
|
monkeypatch.setattr(projects_mod, "load_project",
|
||||||
|
lambda pid, directory=None: du_an if pid == "p1" else None)
|
||||||
|
monkeypatch.setattr(projects_mod, "save_project", lambda p, directory=None: None)
|
||||||
|
return du_an
|
||||||
|
|
||||||
|
|
||||||
|
# ---- mảnh 1: chốt của GraphRAG phải nhìn cả đường dẫn -------------------
|
||||||
|
|
||||||
|
def test_cung_project_nhung_thu_muc_moi_thi_ap_lai(view, monkeypatch, tmp_path):
|
||||||
|
"""Đây là chỗ chốt cũ bỏ lọt: id giống nhau nhưng đường dẫn đã khác."""
|
||||||
|
du_an = _kho_mot_project(monkeypatch, tmp_path / "cu")
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
assert view.renderer.lan_ap == ["p1"]
|
||||||
|
|
||||||
|
du_an.output_dir = str(tmp_path / "moi")
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
|
||||||
|
assert view.renderer.lan_ap == ["p1", "p1"], (
|
||||||
|
"đổi thư mục xong mà GraphRAG không được áp lại — sẽ quét nhầm chỗ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cung_project_cung_thu_muc_thi_khong_ap_lai(view, monkeypatch, tmp_path):
|
||||||
|
"""Chốt cũ phải giữ nguyên: mỗi lần vào lại màn Workspace,
|
||||||
|
``_bind_project`` chạy lại — áp vô điều kiện là kéo bộ chọn project của
|
||||||
|
chính màn GraphRAG về theo, chọn xong chuyển tab là mất."""
|
||||||
|
_kho_mot_project(monkeypatch, tmp_path / "yen")
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
|
||||||
|
assert view.renderer.lan_ap == ["p1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_doi_sang_project_khac_van_ap(view, monkeypatch, tmp_path):
|
||||||
|
"""Hành vi vốn có: đổi sang project khác thì vẫn phải áp."""
|
||||||
|
_kho_mot_project(monkeypatch, tmp_path / "a")
|
||||||
|
view.set_workspace_project("p1")
|
||||||
|
|
||||||
|
view.set_workspace_project("p2")
|
||||||
|
|
||||||
|
assert view.renderer.lan_ap == ["p1", "p2"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---- mảnh 2: đổi thư mục phải kích hoạt việc trỏ lại --------------------
|
||||||
|
|
||||||
|
class _StructureGhi:
|
||||||
|
"""Thay cả màn GraphRAG — ghi lại nó được trỏ lại vào project nào."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lan_tro = []
|
||||||
|
|
||||||
|
def set_workspace_project(self, project_id):
|
||||||
|
self.lan_tro.append(project_id)
|
||||||
|
|
||||||
|
|
||||||
|
class _FolderGhi:
|
||||||
|
"""Thay tab Thư mục — ghi lại nó được trỏ vào đường dẫn nào."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lan_tro = []
|
||||||
|
|
||||||
|
def set_project_root(self, path):
|
||||||
|
self.lan_tro.append(path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def ws(qapp, tmp_path_factory):
|
||||||
|
"""Màn Workspace thật, nhưng KHÔNG mở tab GraphRAG (xem docstring đầu file)."""
|
||||||
|
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||||
|
from cowork_local.presentation.shell.main_window import MainWindow
|
||||||
|
|
||||||
|
config_path = tmp_path_factory.mktemp("cfg") / "config.json"
|
||||||
|
build_config(config_path)
|
||||||
|
window = MainWindow(build_context(config_path))
|
||||||
|
yield window.workspace
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_bam_doi_thu_muc_thi_graphrag_duoc_tro_lai(qapp, ws, monkeypatch, tmp_path):
|
||||||
|
"""Đúng thao tác người dùng báo: bấm "Đổi" ở tab Project."""
|
||||||
|
from PySide6.QtWidgets import QFileDialog
|
||||||
|
|
||||||
|
from cowork_local.ui import workspace_tab as wt
|
||||||
|
|
||||||
|
cu, moi = tmp_path / "thu-muc-cu", tmp_path / "thu-muc-moi"
|
||||||
|
cu.mkdir()
|
||||||
|
moi.mkdir()
|
||||||
|
du_an = _kho_mot_project(monkeypatch, cu)
|
||||||
|
structure, folder = _StructureGhi(), _FolderGhi()
|
||||||
|
monkeypatch.setattr(ws, "_structure", structure, raising=False)
|
||||||
|
monkeypatch.setattr(ws, "_folder", folder, raising=False)
|
||||||
|
ws._current_id = "p1"
|
||||||
|
|
||||||
|
monkeypatch.setattr(QFileDialog, "getExistingDirectory",
|
||||||
|
staticmethod(lambda *a, **k: str(moi)))
|
||||||
|
wt.WorkspaceTab._pick_folder(ws)
|
||||||
|
|
||||||
|
assert du_an.output_dir == str(moi), "chưa ghi thư mục mới"
|
||||||
|
assert ws.folder_lbl.text() == str(moi)
|
||||||
|
assert structure.lan_tro == ["p1"], (
|
||||||
|
"GraphRAG không được trỏ lại — sẽ giữ đường dẫn cũ và quét nhầm")
|
||||||
|
assert folder.lan_tro == [str(moi)], "tab Thư mục cũng phải trỏ theo"
|
||||||
@@ -31,6 +31,7 @@ def view(qapp, monkeypatch):
|
|||||||
|
|
||||||
v = StructureGraphView.__new__(StructureGraphView)
|
v = StructureGraphView.__new__(StructureGraphView)
|
||||||
v._workspace_project = None
|
v._workspace_project = None
|
||||||
|
v._workspace_dir = None # chốt còn theo cả đường dẫn, không chỉ id
|
||||||
v.renderer = _Ghi()
|
v.renderer = _Ghi()
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|||||||
@@ -947,6 +947,7 @@ class WorkspaceTab(ProjectEditingMixin, ProjectFolderRuleMixin, QWidget):
|
|||||||
project.output_dir = chosen
|
project.output_dir = chosen
|
||||||
save_project(project)
|
save_project(project)
|
||||||
self.folder_lbl.setText(chosen)
|
self.folder_lbl.setText(chosen)
|
||||||
|
self.rebind_workspace_folder()
|
||||||
self.status_message.emit(tr("workspace.saved", name=project.name))
|
self.status_message.emit(tr("workspace.saved", name=project.name))
|
||||||
|
|
||||||
def _open_workspace(self) -> None:
|
def _open_workspace(self) -> None:
|
||||||
@@ -1002,6 +1003,7 @@ class WorkspaceTab(ProjectEditingMixin, ProjectFolderRuleMixin, QWidget):
|
|||||||
project.cloud_source = cloud_source
|
project.cloud_source = cloud_source
|
||||||
save_project(project)
|
save_project(project)
|
||||||
self.folder_lbl.setText(str(local_dir))
|
self.folder_lbl.setText(str(local_dir))
|
||||||
|
self.rebind_workspace_folder()
|
||||||
self._refresh_cloud_badge(project)
|
self._refresh_cloud_badge(project)
|
||||||
self.projects_changed.emit()
|
self.projects_changed.emit()
|
||||||
if report.errors:
|
if report.errors:
|
||||||
|
|||||||
Reference in New Issue
Block a user