Skip to content

Commit 352a8a9

Browse files
committed
feat: enhance CLAUDE.md with detailed plugin routing instructions and update MainWindow tests for better resource management
1 parent 7b74c02 commit 352a8a9

3 files changed

Lines changed: 47 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44

5+
## Plugin Routing — Qt Plugins (MANDATORY)
6+
7+
This is a PySide6/Qt project. Always invoke the appropriate plugin skill before writing code:
8+
9+
| Task | Invoke |
10+
|------|--------|
11+
| Write or add tests (pytest-qt, QTest) | `qt:qtest-patterns` skill |
12+
| Run the test suite or check pass/fail | `qt:run` skill |
13+
| Measure test coverage, find gaps | `qt:coverage` skill → `qt:qt-coverage-workflow` skill |
14+
| Generate tests for uncovered lines | `qt:generate` skill (delegates to `qt:test-generator` agent) |
15+
| Visual/GUI interaction testing | `qt:visual` skill (delegates to `qt:gui-tester` agent) |
16+
| Any new feature or UI work | `qt:qtest-patterns` after implementation, `qt:visual` to verify |
17+
18+
**Rule**: Never write Qt/PySide6 tests without first consulting `qt:qtest-patterns`. Never run tests manually via raw `pytest` when `qt:run` is available — use the skill so the agent can capture results and act on failures.
19+
520
## Project Overview
621

722
TextTools is a PySide6 desktop application for text processing on Linux. Its planned features are encoding conversion (to UTF-8), text formatting/cleaning, find/replace, and file management. **The MVVM framework and UI shell are in place, but all TextTools feature logic is unimplemented — the current source files are scaffolding/template code.** `DESIGN.md` is the authoritative spec for what needs to be built (UI mockups, widget objectNames, feature acceptance criteria, data flow diagrams).

src/views/main_window.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from PySide6.QtGui import QAction, QColor, QFont, QKeySequence, QPalette, QShortcut
2121
from PySide6.QtUiTools import QUiLoader
2222
from PySide6.QtWidgets import (
23+
QAbstractItemView,
2324
QApplication,
2425
QCheckBox,
2526
QFileDialog,
@@ -247,8 +248,6 @@ def _setup_file_tree(self) -> None:
247248

248249
def _setup_merge_tab(self) -> None:
249250
"""Configure the merge list widget (drag-drop mode set programmatically)."""
250-
from PySide6.QtWidgets import QAbstractItemView
251-
252251
self._merge_file_list.setDragDropMode(
253252
QAbstractItemView.DragDropMode.InternalMove
254253
)
@@ -264,9 +263,12 @@ def _connect_signals(self) -> None:
264263

265264
# Cleaning: each checkbox triggers a fresh apply when a doc is loaded.
266265
# The checkboxes act as "apply now" toggles, not deferred configuration.
267-
self._trim_cb.stateChanged.connect(self._on_clean_requested)
268-
self._clean_cb.stateChanged.connect(self._on_clean_requested)
269-
self._remove_tabs_cb.stateChanged.connect(self._on_clean_requested)
266+
# checkStateChanged replaces the deprecated stateChanged (Qt 6.7+); both
267+
# work but stateChanged passes int while checkStateChanged passes Qt.CheckState.
268+
# _on_clean_requested ignores the argument so either signature is compatible.
269+
self._trim_cb.checkStateChanged.connect(self._on_clean_requested)
270+
self._clean_cb.checkStateChanged.connect(self._on_clean_requested)
271+
self._remove_tabs_cb.checkStateChanged.connect(self._on_clean_requested)
270272

271273
# Find / Replace
272274
self._find_button.clicked.connect(self._on_find_clicked)

tests/integration/test_main_window.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,20 @@ def mock_text_svc():
3030

3131
@pytest.fixture
3232
def window(mock_file_svc, mock_text_svc, qapp):
33+
"""Construct a MainWindow and clean up the aboutToQuit connection on teardown.
34+
35+
MainWindow.__init__ connects app.aboutToQuit → window._save_settings. Without
36+
teardown, every test that uses this fixture adds another live connection to the
37+
session-scoped QApplication, preventing GC of the window objects for the whole
38+
test session and triggering save_settings for every window at program exit.
39+
"""
3340
vm = MainViewModel(mock_file_svc, mock_text_svc)
34-
return MainWindow(vm)
41+
win = MainWindow(vm)
42+
yield win
43+
try:
44+
qapp.aboutToQuit.disconnect(win._save_settings)
45+
except RuntimeError:
46+
pass # already disconnected or never connected
3547

3648

3749
class TestWindowTitle:
@@ -607,8 +619,18 @@ def test_merge_tab_list_refreshes_on_add(self, window, mock_file_svc, tmp_path,
607619
assert list_widget.count() == 1
608620
assert list_widget.item(0).text() == "hello.txt"
609621

610-
def test_merge_empty_list_shows_error(self, window, qtbot):
611-
"""Clicking Merge with no files emits error_occurred."""
622+
def test_merge_empty_list_shows_error(self, window, monkeypatch, qtbot):
623+
"""Clicking Merge with no files emits error_occurred and shows an error dialog.
624+
625+
Must monkeypatch QMessageBox.critical: the signal is connected to _on_error
626+
which calls QMessageBox.critical() (a blocking modal). The modal creates a
627+
nested event loop that blocks waitSignal's slot from ever firing in offscreen
628+
mode — the dialog is never dismissed so the test hangs without this patch.
629+
"""
630+
monkeypatch.setattr(
631+
"src.views.main_window.QMessageBox.critical",
632+
lambda *a, **kw: None,
633+
)
612634
with qtbot.waitSignal(window._viewmodel.error_occurred, timeout=1000) as blocker:
613635
window._viewmodel.execute_merge()
614636
assert "No files in merge list" in blocker.args[0]

0 commit comments

Comments
 (0)