From e91a7b519f8172b0d140dfed5fba76ba96728b0a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 19 Aug 2026 10:08:01 +0200 Subject: test: assert a move that relocates nothing writes no tag The spec's ordering bullet had only half a test. moveMessagesReportsOnlyWhatMoved() covers the worker refusing to report a message it did not move; nothing covered the window writing tags only for what the worker reported. The failure is provoked by putting a FILE where the trash folder must go, so the Maildir subdirectories cannot be created under it. A read-only directory would be ignored by a test running as root, which this must not depend on. A tag written anyway is the exact half-done state item 103 removes: a message marked deleted, its file still in the inbox, and its origin tag lying about where it went. The mutation putting that back fails this test. Co-Authored-By: Claude Opus 5 --- tests/test_mainwindow.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) (limited to 'tests/test_mainwindow.cpp') diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 578606b..e982e37 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -387,6 +387,7 @@ private slots: void restoreFallsBackToInboxWithoutAnOriginTag(); void theCleanupQueryFindsStrandedMail(); void theCleanupQueryExcludesMailAlreadyInTrash(); + void aMoveThatRelocatesNothingWritesNoTag(); private: /// Owns the throwaway lock table init() points every test at. A pointer @@ -10322,4 +10323,70 @@ void TestMainWindow::theCleanupQueryExcludesMailAlreadyInTrash() QCOMPARE(notmuchCount(cfg, queryEdit->text()), 0); } +void TestMainWindow::aMoveThatRelocatesNothingWritesNoTag() +{ + // The spec's ordering bullet, at the UI level: a failed rename must leave + // no tag. The worker half is moveMessagesReportsOnlyWhatMoved(); this is + // the other half, that the window writes tags only for what the worker + // reported as actually moved. + // + // The failure is provoked by making the destination unwritable, which is + // the closest thing to a failed rename that a test can arrange without + // stubbing the worker. A tag written anyway would be the exact half-done + // state item 103 exists to remove: a message marked deleted, with its file + // still in the inbox and its origin tag lying about where it went. + WorkerBackedWindow backed; + QVERIFY(backed.fixture().addMessage( + QStringLiteral("acct/inbox"), QStringLiteral("nomove@example.org"), + QStringLiteral("Cannot be moved"), QStringLiteral("sender@example.org"), + QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"), + QStringLiteral("Body text."))); + QVERIFY2(backed.build(QStringLiteral("acct"), QStringLiteral("acct"), + QStringLiteral("Trash")), + qPrintable(backed.error())); + + const QString root = backed.fixture().maildirPath(); + const QString cfg = backed.fixture().configPath(); + + // The trash as a FILE where the folder must be, so creating the Maildir + // subdirectories under it cannot succeed. A read-only directory would be + // ignored by a test running as root, which this one must not depend on. + QFile blocker(root + QStringLiteral("/acct/Trash")); + QVERIFY2(blocker.open(QIODevice::WriteOnly), + "could not put a file where the trash folder would go"); + blocker.close(); + + MainWindow window(backed.config()); + auto *model = window.findChild(); + auto *view = window.findChild(); + auto *queryEdit = + window.findChild(QStringLiteral("queryEdit")); + QVERIFY(model && view && queryEdit); + + queryEdit->setText(QStringLiteral("tag:inbox")); + queryEdit->returnPressed(); + QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000); + view->setCurrentIndex(model->index(0, 0, QModelIndex())); + window.findChild(QStringLiteral("delete"))->trigger(); + + // The guard, so this cannot pass by the delete never having been + // attempted: the message is still there and still findable afterwards. + QTRY_VERIFY_WITH_TIMEOUT( + notmuchCount(cfg, QStringLiteral("id:nomove@example.org")) == 1, 15000); + + // A tag write is a round trip, so an immediate read would pass against a + // write still in flight. Given time to arrive, then asserted absent. + QTest::qWait(1500); + QCOMPARE(notmuchCount(cfg, QStringLiteral("id:nomove@example.org and " + "tag:deleted")), 0); + QCOMPARE(notmuchCount(cfg, QStringLiteral("id:nomove@example.org and " + "tag:\"deleted-from:inbox\"")), 0); + + // And the file never left. + QVERIFY(folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"), + QStringLiteral("nomove.example.org")) + || folderHasMessageFile(root + QStringLiteral("/acct/inbox/cur"), + QStringLiteral("nomove.example.org"))); +} + #include "test_mainwindow.moc" -- cgit v1.2.3