One merge window, not fifteen
show_conflict_summary() built a fresh ConflictSummaryDialog every time and only reassigned self._conflict_dialog — the old dialog was still a child of the window, so it stayed on screen. check_for_external_changes() runs resolve_conflicts() on every Syncthing watch tick, so that was one window per merge, forever. Fifteen had piled up on trav's desktop. The dialog is now a session-long log: add_event() folds each merge in as its own timestamped entry, newest first, and the restore button names the merge it would undo. MainWindow reuses the open dialog and clears its reference on finished, so closing it lets the next merge open a fresh one instead of touching a deleted C++ object. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8Mzze7shr5pZoEgKQU5NW
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Round 38: one merge window, and play counts that can't conflict.
|
||||
|
||||
Two bugs stacked fifteen "Synced changes merged" windows on the desktop: the
|
||||
dialog was never reused, and playing a song rewrote all 15 MB of library.json —
|
||||
so Syncthing produced a conflict file roughly once per track. The dialog is now
|
||||
a session-long log, and play counts live in per-machine journal files that only
|
||||
their owner ever writes.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from lintunes.storage.conflict_resolver import ConflictSummary
|
||||
|
||||
|
||||
def _summary(file="library.json", lines=("2 track(s) reconciled.",), backup="/b/1"):
|
||||
return ConflictSummary(file, "library", list(lines), backup)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dialog_cls(qapp):
|
||||
from lintunes.gui.conflict_dialog import ConflictSummaryDialog
|
||||
return ConflictSummaryDialog
|
||||
|
||||
|
||||
class TestOneMergeWindow:
|
||||
def test_second_merge_appends_instead_of_replacing(self, dialog_cls):
|
||||
dlg = dialog_cls([_summary(lines=["first merge"])], manager=None)
|
||||
dlg.add_event([_summary(lines=["second merge"])])
|
||||
|
||||
text = dlg._body.toPlainText()
|
||||
assert "first merge" in text and "second merge" in text
|
||||
# Newest first.
|
||||
assert text.index("second merge") < text.index("first merge")
|
||||
|
||||
def test_events_are_timestamped_once_there_is_more_than_one(self, dialog_cls):
|
||||
dlg = dialog_cls([_summary()], manager=None)
|
||||
assert "──" not in dlg._body.toPlainText() # a lone merge needs no header
|
||||
|
||||
dlg.add_event([_summary()], when=datetime(2026, 8, 20, 18, 42))
|
||||
text = dlg._body.toPlainText()
|
||||
assert "6:42 PM" in text
|
||||
assert "2 merges this session." in dlg._intro.text()
|
||||
|
||||
def test_restore_targets_the_newest_backup_and_names_it(self, dialog_cls):
|
||||
dlg = dialog_cls([_summary(backup="/b/old")], manager=None)
|
||||
assert dlg._backup_dir == "/b/old"
|
||||
|
||||
dlg.add_event([_summary(backup="/b/new")], when=datetime(2026, 8, 20, 18, 42))
|
||||
assert dlg._backup_dir == "/b/new"
|
||||
assert dlg._restore_btn.text() == "Restore backup from 6:42 PM"
|
||||
|
||||
def test_empty_event_leaves_the_previous_backup_reachable(self, dialog_cls):
|
||||
dlg = dialog_cls([_summary(backup="/b/old")], manager=None)
|
||||
dlg.add_event([])
|
||||
assert dlg._backup_dir == "/b/old"
|
||||
assert dlg._restore_btn.isEnabled()
|
||||
|
||||
def test_no_backup_disables_the_backup_buttons(self, dialog_cls):
|
||||
dlg = dialog_cls([_summary(backup="")], manager=None)
|
||||
assert not dlg._open_btn.isEnabled()
|
||||
assert not dlg._restore_btn.isEnabled()
|
||||
|
||||
|
||||
class TestWindowReuse:
|
||||
"""MainWindow isn't constructible headless, so exercise the two real methods
|
||||
on a minimal QWidget host — that's where the stacking bug lived."""
|
||||
|
||||
@pytest.fixture
|
||||
def host(self, qapp):
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
from lintunes.gui.main_window import MainWindow
|
||||
|
||||
class Host(QWidget):
|
||||
_manager = None
|
||||
_conflict_dialog = None
|
||||
show_conflict_summary = MainWindow.show_conflict_summary
|
||||
_on_conflict_dialog_closed = MainWindow._on_conflict_dialog_closed
|
||||
|
||||
return Host()
|
||||
|
||||
def test_repeated_merges_make_exactly_one_dialog(self, host):
|
||||
from lintunes.gui.conflict_dialog import ConflictSummaryDialog
|
||||
|
||||
for i in range(15):
|
||||
host.show_conflict_summary([_summary(lines=[f"merge {i}"])])
|
||||
|
||||
dialogs = host.findChildren(ConflictSummaryDialog)
|
||||
assert len(dialogs) == 1
|
||||
text = dialogs[0]._body.toPlainText()
|
||||
assert "merge 0" in text and "merge 14" in text
|
||||
|
||||
def test_closing_lets_the_next_merge_open_a_fresh_window(self, host):
|
||||
host.show_conflict_summary([_summary()])
|
||||
first = host._conflict_dialog
|
||||
first.accept() # "Keep merged"
|
||||
assert host._conflict_dialog is None
|
||||
|
||||
host.show_conflict_summary([_summary()])
|
||||
assert host._conflict_dialog is not None
|
||||
assert host._conflict_dialog is not first
|
||||
Reference in New Issue
Block a user