summaryrefslogtreecommitdiffstats
path: root/tests/test_mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-29 10:11:59 +0200
committerDanilo M. <danix@danix.xyz>2026-08-29 10:11:59 +0200
commite2bd68a863513209a5881696164fd463ddc83503 (patch)
treefe9fc90462030f9f99d5298eb09785018cc60f40 /tests/test_mainwindow.cpp
parentfeb15bcec261501eb49ad0de1fdc05a9992227c2 (diff)
downloadqtmaildir-e2bd68a863513209a5881696164fd463ddc83503.tar.gz
qtmaildir-e2bd68a863513209a5881696164fd463ddc83503.zip
fix: say an edit is waiting for the sync instead of claiming it landed
Item 182, found by hand: a thread of 9 messages with 5 unread, marked read while a sync was running, reported "<subject>: mark as read" and then reported the same work again when the sync finished. The user read it as double reporting. Not a double write, and the mail was correct. It is one action reported twice because the FIRST report was the wrong one. A sync holds notmuch's exclusive write lock and the worker's read-write open blocks on it rather than failing, so an edit made during a sync is held and sent when the lock frees. All three hold branches say exactly that, in a label chosen deliberately: NOT transient, because it describes state lasting until the sync ends, and a message that expired would leave rows showing a tag the database has not got and no explanation of why. That label never survived. Every caller announced the action itself a line later through showTransientStatus(), which overwrote it, so the user was told the write had happened and the hold was never mentioned. The flush at the end of the sync then reported the same work again and read as a duplicate rather than as its completion. announceAction() asks whether a sync holds the lock and, when one does, sets a non-transient label naming the action AND the wait. The action is still named because that announcement is what stands in for the confirmation dialog this project rules out: it is how a user tells that something larger than they meant has just happened, so the hold is added to it rather than replacing it. The flush message is untouched and is the only signal that held work actually landed, whose absence was item 106. The test drives toggle_unread, the route the user took, and asserts both halves: the text mentions the sync, and it still says what is waiting. Asserting only the first would pass against an announcement that dropped the action entirely. Mutation-checked by forcing the non-held branch, which fails with the exact text the user reported. The new string is translated, since one that misses the Italian ships as English inside an otherwise Italian UI: lupdate found it with no context warnings, lrelease reports 552 finished and 0 unfinished. Suite: 42 of 43, with undoMovesTheMessageBack failing as it does on master (item 136).
Diffstat (limited to 'tests/test_mainwindow.cpp')
-rw-r--r--tests/test_mainwindow.cpp65
1 files changed, 65 insertions, 0 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index a8e6284..bbdad19 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -270,6 +270,7 @@ private slots:
void aWorkerBackedWindowReturnsRealThreads();
void aPurgeTakesTheRowsOutOfTheViewWithoutARefresh();
void theDashboardFollowsAWriteToTheConversationItShows();
+ void anEditHeldByASyncSaysSoInsteadOfClaimingItLanded();
// Compose and send, item 123 task 12.
void theMailRootComesFromTheConfigNotTheIndex();
@@ -9226,6 +9227,70 @@ void TestMainWindow::aWorkerBackedWindowReturnsRealThreads()
/// Reachable from the dashboard's own Mark all read button, which is the worst
/// version of it: the user presses a button and the number above it does not
/// move.
+/// Item 182, found by hand: a thread marked read DURING a sync reported
+/// "<subject>: mark as read", and then reported the same work again when the
+/// sync finished, never once saying it was waiting.
+///
+/// The write is held, correctly: a sync holds notmuch's exclusive lock and the
+/// worker's read-write open BLOCKS on it, so sending would freeze the worker
+/// for the rest of the run. Every hold branch sets a deliberately NON-transient
+/// label explaining that, and every caller then overwrote it a line later with
+/// the generic announcement, which claims the write happened.
+///
+/// Asserted on the LABEL rather than on the write: what the mail does was
+/// already right, and what the user was told was not.
+void TestMainWindow::anEditHeldByASyncSaysSoInsteadOfClaimingItLanded()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const Config config = configWithTrash(dir);
+ MainWindow window(config);
+
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+ auto *view = window.findChild<QTreeView *>();
+ QVERIFY(view);
+ auto *status = window.findChild<QLabel *>(QStringLiteral("statusMessage"));
+ QVERIFY(status);
+
+ ThreadSummary thread = threadAtPath(QStringLiteral("t1"),
+ QStringLiteral("acct/inbox/cur/1:2,S"),
+ { QStringLiteral("unread") });
+ thread.totalCount = 9;
+ model->appendBatch({ thread });
+ view->setCurrentIndex(model->index(0, 0, {}));
+
+ // The sync starts. This is the same slot the sync monitor calls, so the
+ // window reaches the state a real background sync puts it in.
+ QMetaObject::invokeMethod(&window, "onExternalSyncStateChanged",
+ Q_ARG(SyncMonitor::State,
+ SyncMonitor::State::Running));
+
+ // The toggle, which is the route the user took: the thread has unread
+ // messages, so this reads "Mark thread as read" and sends the write.
+ auto *toggle = window.findChild<QAction *>(QStringLiteral("toggle_unread"));
+ QVERIFY(toggle);
+ toggle->trigger();
+
+ // The hold has to be what the user is told about. Without the fix the
+ // label reads "Mark as read: 1 thread(s)", which claims a write that has
+ // not happened and cannot happen until the sync ends.
+ QVERIFY2(status->text().contains(QStringLiteral("sync")),
+ qPrintable(QStringLiteral("the status bar never mentions the "
+ "sync that is holding the edit: %1")
+ .arg(status->text())));
+
+ // And it still names the action, which is what stands in for the
+ // confirmation dialog this project rules out: the user has to be able to
+ // tell that something larger than they meant has just happened.
+ QVERIFY2(status->text().contains(QStringLiteral("read"), Qt::CaseInsensitive),
+ qPrintable(QStringLiteral("the status bar no longer says WHAT is "
+ "waiting: %1").arg(status->text())));
+
+ // No restore to "/proc/locks": init() points every test at its own
+ // table, and handing the real one back would re-expose the next test.
+}
+
void TestMainWindow::theDashboardFollowsAWriteToTheConversationItShows()
{
WorkerBackedWindow backed;