Enforce the project gate in _goto, not only on the rail rows
Showing Cowork and GraphRAG always, greyed, instead of letting them appear and vanish, moved the gate from "the row is absent" to "the row is disabled". That guards the rail — Qt will not select a disabled row — but it guards only the rail, and _goto is also reached from a RECENTS click and from startup restore. Called with a gated sub-tab it opened the page while the workspace's own tab strip still had it hidden. _goto now asks WorkspaceTab.subtab_available() — the same isTabVisible state that greys the row — and shows "Chọn project trước" in the status bar instead of navigating. One gate, one place, whatever triggered the jump. check_project_gate runs against an EMPTY home, because with any project on disk the gate is open and the test proves nothing. It asserts the gated rows are disabled and say why, that clicking them does nothing, that _goto refuses them, and that + new chat and the project picker are off. Dropping the _goto guard fails it with "_goto mo duoc 'Cowork' trong khi cong dang dong". 17/17 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d37a0c1578
commit
16406f863f
@@ -1074,7 +1074,14 @@ class MainWindow(QMainWindow):
|
||||
self.workspace.refresh() # re-list projects + threads on entry
|
||||
widget = self._page_widgets[page]
|
||||
if sub is not None and hasattr(widget, "select_subtab"):
|
||||
widget.select_subtab(sub)
|
||||
# Enforce the project gate here rather than at each entry point. A
|
||||
# greyed rail row cannot be clicked, but _goto is also reached from
|
||||
# RECENTS and from startup restore, and it used to open a sub-tab
|
||||
# the gate was holding shut — page shown, tab strip still hiding it.
|
||||
if hasattr(widget, "subtab_available") and not widget.subtab_available(sub):
|
||||
self.statusBar().showMessage(tr("app.nav.needs_project"), 4000)
|
||||
else:
|
||||
widget.select_subtab(sub)
|
||||
# Move the highlight with the content, however navigation was triggered —
|
||||
# a programmatic _goto used to leave it on whatever was clicked last.
|
||||
if not self._nav_building:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""With no project chosen, the gated screens must stay shut on every route.
|
||||
|
||||
The redesign shows Cowork and GraphRAG at all times instead of making them
|
||||
appear and disappear. That is a presentation change only — the gate is the same
|
||||
`isTabVisible` state as before — so this asserts the gate still actually holds,
|
||||
and holds for programmatic jumps too, not just for the greyed rail rows.
|
||||
|
||||
Runs against an EMPTY home, not a copy of the real one: with any project on
|
||||
disk the gate is open and the test proves nothing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO.parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
sandbox = Path(tempfile.mkdtemp(prefix="cowork-gate-"))
|
||||
(sandbox / ".cowork_local").mkdir(parents=True, exist_ok=True)
|
||||
for _var in ("USERPROFILE", "HOME"):
|
||||
os.environ[_var] = str(sandbox)
|
||||
os.environ.pop("HOMEDRIVE", None)
|
||||
os.environ.pop("HOMEPATH", None)
|
||||
|
||||
from capture_screens import _apply_theme, _freeze_schedulers, _load_fonts # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
_load_fonts()
|
||||
_freeze_schedulers()
|
||||
_apply_theme(app)
|
||||
|
||||
from cowork_local.config import CONFIG_DIR
|
||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||
|
||||
from cowork_local.app import MainWindow
|
||||
from cowork_local.config import AppConfig
|
||||
from cowork_local.i18n import set_language
|
||||
from cowork_local.state import AppContext
|
||||
|
||||
set_language("vi")
|
||||
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||
win.resize(1400, 900)
|
||||
win.show()
|
||||
app.processEvents()
|
||||
|
||||
fails = []
|
||||
n_projects = len(win.workspace.project_choices())
|
||||
print(f"project tren dia: {n_projects}")
|
||||
if n_projects:
|
||||
print("FAIL home khong rong — phep thu vo nghia")
|
||||
sys.stdout.flush()
|
||||
os._exit(1)
|
||||
|
||||
tree = win.nav
|
||||
rows = [(i, tree.topLevelItem(i)) for i in range(tree.topLevelItemCount())]
|
||||
locked = [(i, it) for i, it in rows if it.isDisabled()]
|
||||
print("dong bi khoa :", [it.text(0) for _i, it in locked])
|
||||
print("dong mo binh thuong:", [it.text(0) for _i, it in rows if not it.isDisabled()])
|
||||
if not locked:
|
||||
fails.append("khong project nao ma khong dong nao bi khoa")
|
||||
|
||||
for _i, it in locked:
|
||||
if not it.toolTip(0):
|
||||
fails.append(f"dong khoa '{it.text(0)}' khong noi ly do")
|
||||
|
||||
for i, it in locked:
|
||||
label = it.text(0)
|
||||
data = it.data(0, Qt.UserRole) or {}
|
||||
before = win.workspace.current_subtab()
|
||||
|
||||
tree.setCurrentItem(it)
|
||||
app.processEvents()
|
||||
if win.workspace.current_subtab() != before:
|
||||
fails.append(f"bam duoc vao '{label}' du dang khoa")
|
||||
|
||||
# the route a greyed row does not guard: a jump from code
|
||||
win._goto(data.get("page", 0), data.get("sub"))
|
||||
app.processEvents()
|
||||
landed = win.workspace.current_subtab()
|
||||
if landed == data.get("sub"):
|
||||
fails.append(f"_goto mo duoc '{label}' trong khi cong dang dong")
|
||||
print(f" '{label}': bam -> {before}, _goto -> {landed} "
|
||||
f"(tab hien = {win.workspace.tabs.isTabVisible(data.get('sub'))})")
|
||||
|
||||
# controls that would act on a project must be off too
|
||||
for name, widget in (("+ chat moi", win.nav_new_chat),
|
||||
("chon project", win.nav_project_btn)):
|
||||
if widget.isEnabled():
|
||||
fails.append(f"'{name}' van bam duoc khi chua co project")
|
||||
print(f"+ chat moi bat={win.nav_new_chat.isEnabled()} "
|
||||
f"tooltip={win.nav_new_chat.toolTip()!r}")
|
||||
|
||||
print()
|
||||
for f in fails:
|
||||
print("FAIL " + f)
|
||||
print("PASS cong project van giu, ca khi bam lan khi goi tu code" if not fails
|
||||
else f"{len(fails)} problem(s)")
|
||||
sys.stdout.flush()
|
||||
os._exit(1 if fails else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -67,6 +67,14 @@ class WorkspaceTab(QWidget):
|
||||
self.tabs.isTabVisible(i))
|
||||
for i in range(self.tabs.count())]
|
||||
|
||||
def subtab_available(self, index: int) -> bool:
|
||||
"""False while the project gate is holding this sub-tab shut.
|
||||
|
||||
The rail greys those rows out, but that only guards the rail. This lets
|
||||
every other route ask the same question of the same state.
|
||||
"""
|
||||
return bool(0 <= index < self.tabs.count() and self.tabs.isTabVisible(index))
|
||||
|
||||
def select_subtab(self, index: int) -> None:
|
||||
if 0 <= index < self.tabs.count():
|
||||
self.tabs.setCurrentIndex(index)
|
||||
|
||||
Reference in New Issue
Block a user