summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_mainwindow.cpp70
-rw-r--r--tests/test_notmuchworker.cpp75
-rw-r--r--tests/test_tagdialog.cpp4
3 files changed, 108 insertions, 41 deletions
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 9a38a95..a92b3d1 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -11981,7 +11981,7 @@ void TestMainWindow::deleteRecordsWhereTheMessageCameFrom()
// would report the tag whether or not the write ever landed.
// Re-queried by id and asserted on the TAG LIST the database returns.
//
- // Not with `tag:"deleted-from:inbox"` in the query: notmuch's parser does
+ // Not with `tag:"moved-from:inbox"` in the query: notmuch's parser does
// not match a quoted tag containing a colon that way, so such a query
// returns nothing against a perfectly tagged message and reads as the
// feature being broken. Asking for the message and inspecting its tags
@@ -11998,7 +11998,7 @@ void TestMainWindow::deleteRecordsWhereTheMessageCameFrom()
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
const QStringList tags = model->threadAt(0).tags;
tagged = tags.contains(QStringLiteral("deleted"))
- && tags.contains(QStringLiteral("deleted-from:inbox"));
+ && tags.contains(QStringLiteral("moved-from:inbox"));
if (!tagged)
QTest::qWait(200);
}
@@ -12017,8 +12017,8 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind()
// onMessagesMoved() resolved the origin placeholder from the folder the
// WORKER reported, which is where the message came FROM. On a delete that
// is the inbox and correct. On a restore it is the TRASH, so the restore
- // asked to remove `deleted-from:Trash`, a tag that had never been written,
- // while the real `deleted-from:inbox` was never named and stayed on the
+ // asked to remove `moved-from:Trash`, a tag that had never been written,
+ // while the real `moved-from:inbox` was never named and stayed on the
// message. It came home still claiming to have been deleted from
// somewhere, which makes Restore offer to move a message already at home.
//
@@ -12066,7 +12066,7 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind()
QTRY_VERIFY_WITH_TIMEOUT(
notmuchCount(backed.fixture().configPath(),
QStringLiteral("id:twice@example.org and "
- "tag:\"deleted-from:inbox\"")) == 1,
+ "tag:\"moved-from:inbox\"")) == 1,
15000);
// Second press on the same message, which restores it.
@@ -12096,11 +12096,11 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind()
== 0,
15000);
- // BOTH tags gone, asked of the database. `deleted-from:` left behind is
+ // BOTH tags gone, asked of the database. `moved-from:` left behind is
// the defect this covers, and it survived a green suite before.
// The origin tag specifically, asserted on its OWN query.
//
- // A combined `tag:deleted or tag:"deleted-from:inbox"` query is NOT
+ // A combined `tag:deleted or tag:"moved-from:inbox"` query is NOT
// equivalent and passed against the bug: `deleted` is removed correctly
// and promptly, so the disjunction went to zero on that term alone while
// the origin tag was still on the message. Split, so the assertion can
@@ -12120,18 +12120,18 @@ void TestMainWindow::deletingTwiceLeavesNoOriginTagBehind()
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:twice@example.org")), 1);
// The origin tag is gone. This is the defect: it used to survive the
- // restore, because the placeholder resolved to `deleted-from:Trash`, the
+ // restore, because the placeholder resolved to `moved-from:Trash`, the
// folder the message was coming FROM, and stripped a tag that had never
// been written.
QCOMPARE(notmuchCount(cfg,
QStringLiteral("id:twice@example.org and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
0);
// And no tag naming the trash was invented in its place.
QCOMPARE(notmuchCount(cfg,
QStringLiteral("id:twice@example.org and "
- "tag:\"deleted-from:Trash\"")),
+ "tag:\"moved-from:Trash\"")),
0);
// `deleted` itself, so a fix that dropped this one instead cannot hide.
@@ -12151,7 +12151,7 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo()
// placeholder for the tags it wrote to the database, but handed the undo
// command the raw list. Undo then asked to remove a tag by the
// placeholder's literal name, which no message carries, so the removal
- // was a silent no-op and `deleted-from:inbox` survived. The message came
+ // was a silent no-op and `moved-from:inbox` survived. The message came
// home still claiming to have been deleted from somewhere, which makes
// Restore offer to move a message that is already at home.
//
@@ -12193,7 +12193,7 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo()
// about it being REMOVED rather than never having existed.
QTRY_VERIFY_WITH_TIMEOUT(
notmuchCount(cfg, QStringLiteral("id:undotag@example.org and "
- "tag:\"deleted-from:inbox\"")) == 1,
+ "tag:\"moved-from:inbox\"")) == 1,
15000);
window.findChild<QAction *>(QStringLiteral("undo"))->trigger();
@@ -12217,11 +12217,11 @@ void TestMainWindow::undoOfADeleteRemovesTheOriginTagToo()
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:undotag@example.org")), 1);
QCOMPARE(notmuchCount(cfg,
QStringLiteral("id:undotag@example.org and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
0);
QCOMPARE(notmuchCount(cfg,
QStringLiteral("id:undotag@example.org and "
- "tag:\"deleted-from:Trash\"")),
+ "tag:\"moved-from:Trash\"")),
0);
}
@@ -12237,8 +12237,8 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt()
// from the mismatch: the toggle asked a thread ROW about its thread's
// tags, which notmuch gives as a UNION, so deleting the root left the
// union carrying no `deleted` and a second press ran Delete AGAIN,
- // trash-to-trash, producing `deleted-from:inbox` and
- // `deleted-from:Trash` at once with no way back. Item 177 dissolves the
+ // trash-to-trash, producing `moved-from:inbox` and
+ // `moved-from:Trash` at once with no way back. Item 177 dissolves the
// mismatch rather than patching it: the row and the write now agree about
// what they are for. The trash-to-trash assertions stay, because they are
// what proves a delete cannot run twice on one message.
@@ -12286,7 +12286,7 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt()
QTRY_VERIFY_WITH_TIMEOUT(folderHasMessageFile(trash, stem), 15000);
QTRY_VERIFY_WITH_TIMEOUT(
notmuchCount(cfg, QStringLiteral("id:tlone@example.org and "
- "tag:\"deleted-from:inbox\"")) == 1,
+ "tag:\"moved-from:inbox\"")) == 1,
15000);
// There is no second press to make any more, and that is the point.
@@ -12330,12 +12330,12 @@ void TestMainWindow::deletingALoneMessageRemovesItFromTheInboxAndUndoReturnsIt()
// query bar passes against any state of the database.
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org")), 1);
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
0);
// The tag a re-delete would invent. Its presence is the signature of a
// trash-to-trash move rather than a variation on the origin-tag defects.
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:tlone@example.org and "
- "tag:\"deleted-from:Trash\"")),
+ "tag:\"moved-from:Trash\"")),
0);
QVERIFY2(!folderHasMessageFile(trash, stem),
"the message was left in the trash");
@@ -12349,7 +12349,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard()
// when Delete became a move, so a whole conversation stayed in the inbox
// wearing a `deleted` chip, which is the half-deleted state item 103
// existed to remove. It moves every message now, each carrying its own
- // `deleted-from:` origin so a thread spanning folders reassembles.
+ // `moved-from:` origin so a thread spanning folders reassembles.
//
// And the ROOT card did not repaint until it was clicked, while its
// replies did. A thread-scoped move updated each message's node;
@@ -12418,7 +12418,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard()
// Each with its own origin, which is what makes the move reversible.
QCOMPARE(notmuchCount(cfg, thread
+ QStringLiteral(" and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
3);
// The ROOT CARD's own state, which is what the user watches. Read from the
@@ -12441,7 +12441,7 @@ void TestMainWindow::deleteThreadMovesEveryMessageAndRepaintsTheRootCard()
QCOMPARE(notmuchCount(cfg, thread), 3);
QCOMPARE(notmuchCount(cfg, thread
+ QStringLiteral(" and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
0);
QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt0.example.org")));
QVERIFY(!folderHasMessageFile(trash, QStringLiteral("dt1.example.org")));
@@ -12452,8 +12452,8 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip()
{
// A notmuch tag MAY contain a space, and a Maildir folder name may too.
// The worker reported each message's tags as one space-joined string, so
- // `deleted-from:Inbox/SlackBuilds users` was split back into
- // "deleted-from:Inbox/SlackBuilds" and "users", and Restore moved the
+ // `moved-from:Inbox/SlackBuilds users` was split back into
+ // "moved-from:Inbox/SlackBuilds" and "users", and Restore moved the
// messages to the truncated folder, CREATING it. On the user's real
// Maildir that put four messages into a directory mbsync does not sync,
// beside the real folder of 808, and they read as missing.
@@ -12505,7 +12505,7 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip()
// The origin tag carries the WHOLE folder name, space included.
QCOMPARE(notmuchCount(cfg,
thread
- + QStringLiteral(" and tag:\"deleted-from:"
+ + QStringLiteral(" and tag:\"moved-from:"
"Inbox/SlackBuilds users\"")),
2);
@@ -12521,13 +12521,13 @@ void TestMainWindow::aFolderNameWithASpaceSurvivesTheRoundTrip()
// could see, could not type, and could not remove.
QCOMPARE(notmuchCount(cfg,
thread
- + QStringLiteral(" and tag:\"deleted-from:"
+ + QStringLiteral(" and tag:\"moved-from:"
"Inbox/SlackBuilds users\"")),
0);
// Nor a truncated one, which is what a space-split would have written.
QCOMPARE(notmuchCount(cfg,
thread
- + QStringLiteral(" and tag:\"deleted-from:"
+ + QStringLiteral(" and tag:\"moved-from:"
"Inbox/SlackBuilds\"")),
0);
@@ -12754,7 +12754,7 @@ void TestMainWindow::restoreReturnsAMessageToItsOriginFolder()
// removes the race rather than papering over it with a longer timeout.
QTRY_VERIFY_WITH_TIMEOUT(
notmuchCount(cfg, QStringLiteral("id:ro1@example.org and "
- "tag:\"deleted-from:inbox\"")) == 0,
+ "tag:\"moved-from:inbox\"")) == 0,
15000);
QTRY_VERIFY_WITH_TIMEOUT(
notmuchCount(cfg,
@@ -12784,7 +12784,7 @@ void TestMainWindow::restoreReturnsAMessageToItsOriginFolder()
void TestMainWindow::restoreFallsBackToInboxWithoutAnOriginTag()
{
// A message trashed by ANOTHER client: it sits in the trash folder and
- // carries no `deleted-from:` tag, because nothing here put it there. The
+ // carries no `moved-from:` tag, because nothing here put it there. The
// real Maildir has such messages, which is why the trash view is path
// based rather than tag based.
//
@@ -12814,7 +12814,7 @@ void TestMainWindow::restoreFallsBackToInboxWithoutAnOriginTag()
// The guard this test needs: no origin tag, so the fallback is what is
// under test rather than an ordinary restore.
QCOMPARE(notmuchCount(cfg, QStringLiteral("id:foreign@example.org and "
- "tag:\"deleted-from:inbox\"")),
+ "tag:\"moved-from:inbox\"")),
0);
queryEdit->setText(QStringLiteral("path:\"acct/Trash/**\""));
@@ -12882,10 +12882,10 @@ void TestMainWindow::undoMovesTheMessageBack()
QVERIFY2(!folderHasMessageFile(trash, stem),
"undo restored the file and left a copy in the trash");
- // Both tags gone, asked of the database. `deleted-from:` left behind would
+ // Both tags gone, asked of the database. `moved-from:` left behind would
// make Restore offer to move a message that is already home.
queryEdit->setText(QStringLiteral(
- "id:del3@example.org and (tag:deleted or tag:\"deleted-from:inbox\")"));
+ "id:del3@example.org and (tag:deleted or tag:\"moved-from:inbox\")"));
queryEdit->returnPressed();
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 0, 15000);
// The guard the assertion above needs: a query that matches nothing
@@ -13101,7 +13101,7 @@ void TestMainWindow::twoDeletesToOneTrashBothGetTheirTags()
// Deletes in one account before the first confirmation arrived both named
// `acct/Trash`: the second insert overwrote the first and the second
// confirmation took an empty entry. That file reached the trash carrying
- // neither `deleted` nor `deleted-from:`, which makes it unrestorable by
+ // neither `deleted` nor `moved-from:`, which makes it unrestorable by
// Restore and invisible to a `tag:deleted` query.
WorkerBackedWindow backed;
QVERIFY(backed.fixture().addMessage(
@@ -13156,7 +13156,7 @@ void TestMainWindow::twoDeletesToOneTrashBothGetTheirTags()
// the defect was a write that never happened, and the model would have
// shown the optimistic state either way.
queryEdit->setText(QStringLiteral(
- "tag:deleted and tag:\"deleted-from:inbox\" and "
+ "tag:deleted and tag:\"moved-from:inbox\" and "
"(id:two1@example.org or id:two2@example.org)"));
queryEdit->returnPressed();
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 2, 15000);
@@ -13385,7 +13385,7 @@ void TestMainWindow::aMoveThatRelocatesNothingWritesNoTag()
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);
+ "tag:\"moved-from:inbox\"")), 0);
// And the file never left.
QVERIFY(folderHasMessageFile(root + QStringLiteral("/acct/inbox/new"),
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index 970f51d..2877894 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -54,6 +54,7 @@ private slots:
void applyTagsWithNoIdsDoesNothing();
void applyTagsReportsOnlyTheMessagesItChanged();
void applyTagsThatChangeNothingEmitNothing();
+ void applyTagsKeepsOnlyTheNewestOriginTag();
void queryStillWorksAfterWrite();
void aThreadCarriesItsCardMessagesOwnTags();
@@ -142,8 +143,10 @@ private:
QString fileOf(const QString &messageId,
const QString &configPath = QString());
- /// Tags of one message, read back through a fresh worker query.
- QStringList tagsOf(const QString &messageId);
+ /// Tags of one message, read back through a fresh worker query. Defaults to
+ /// the shared fixture; a test with its own fixture passes its own path.
+ QStringList tagsOf(const QString &messageId,
+ const QString &configPath = QString());
QVector<MessageRef> messagesOfThread(const QString &threadId,
const QString &matchQuery = QString(),
bool matchedOnly = false);
@@ -401,9 +404,11 @@ QVector<MessageRef> TestNotmuchWorker::messagesOfThread(const QString &threadId,
return loaded.first().at(0).value<QVector<MessageRef>>();
}
-QStringList TestNotmuchWorker::tagsOf(const QString &messageId)
+QStringList TestNotmuchWorker::tagsOf(const QString &messageId,
+ const QString &configPath)
{
- NotmuchWorker worker(m_fixture.configPath());
+ NotmuchWorker worker(configPath.isEmpty() ? m_fixture.configPath()
+ : configPath);
QSignalSpy loaded(&worker, &NotmuchWorker::threadLoaded);
worker.loadThread(QStringLiteral("{id:%1}").arg(messageId), QString(), 1);
if (loaded.isEmpty())
@@ -1117,6 +1122,68 @@ void TestNotmuchWorker::applyTagsThatChangeNothingEmitNothing()
QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
}
+void TestNotmuchWorker::applyTagsKeepsOnlyTheNewestOriginTag()
+{
+ // A message that travelled inbox -> spam -> trash must hold exactly ONE
+ // `moved-from:` tag, the newest, or Restore's first-match scan picks an
+ // origin arbitrarily. Its own fixture, so the tags this test leaves behind
+ // cannot move the shared one's thread counts.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("origin1@example.org"),
+ QStringLiteral("Travelled"),
+ QStringLiteral("Erin <erin@example.org>"),
+ QStringLiteral("Sun, 7 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body"), false));
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("origin2@example.org"),
+ QStringLiteral("Fresh"),
+ QStringLiteral("Erin <erin@example.org>"),
+ QStringLiteral("Sun, 7 Jun 2026 11:00:00 +0000"),
+ QStringLiteral("body"), false));
+ QVERIFY(fixture.index());
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred);
+
+ // An earlier move left an origin behind.
+ worker.applyTags(TagChange{ { QStringLiteral("origin1@example.org") },
+ { QStringLiteral("moved-from:inbox") },
+ {},
+ QStringLiteral("Earlier move") });
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+ QVERIFY(tagsOf(QStringLiteral("origin1@example.org"), fixture.configPath())
+ .contains(QStringLiteral("moved-from:inbox")));
+
+ // The move under test: a new origin lands while the old one is still there.
+ worker.applyTags(TagChange{
+ { QStringLiteral("origin1@example.org"),
+ QStringLiteral("origin2@example.org") },
+ { QStringLiteral("moved-from:Spam"), QStringLiteral("spam") },
+ { QStringLiteral("inbox"), QStringLiteral("unread") },
+ QStringLiteral("Mark spam") });
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+
+ const QStringList travelled =
+ tagsOf(QStringLiteral("origin1@example.org"), fixture.configPath());
+ QVERIFY(!travelled.contains(QStringLiteral("moved-from:inbox")));
+ QVERIFY(travelled.contains(QStringLiteral("moved-from:Spam")));
+ int origins = 0;
+ for (const QString &tag : travelled) {
+ if (tag.startsWith(QStringLiteral("moved-from:")))
+ ++origins;
+ }
+ QCOMPARE(origins, 1);
+
+ // The message that never carried an origin keeps exactly the new one, and
+ // the move still removed what a move removes.
+ const QStringList fresh =
+ tagsOf(QStringLiteral("origin2@example.org"), fixture.configPath());
+ QVERIFY(fresh.contains(QStringLiteral("moved-from:Spam")));
+ QVERIFY(fresh.contains(QStringLiteral("spam")));
+ QVERIFY(!fresh.contains(QStringLiteral("inbox")));
+}
+
void TestNotmuchWorker::queryStillWorksAfterWrite()
{
// applyTags closes the read-only handle to take the write lock. The same
diff --git a/tests/test_tagdialog.cpp b/tests/test_tagdialog.cpp
index 9b12109..93a7229 100644
--- a/tests/test_tagdialog.cpp
+++ b/tests/test_tagdialog.cpp
@@ -176,14 +176,14 @@ void TestTagDialog::aTagWithASpaceCanStillBeRemoved()
// they can see and cannot get rid of.
//
// Reached by a real Maildir: a folder named "Inbox/SlackBuilds users"
- // produced `deleted-from:Inbox/SlackBuilds users`, and the one dialog that
+ // produced `moved-from:Inbox/SlackBuilds users`, and the one dialog that
// could have cleared it refused the only text that names it.
//
// Only the TYPED route was blocked. Unchecking appends to the removal list
// after validation has run, so it worked throughout; that asymmetry is why
// both routes are asserted here rather than just the one that failed.
const QString spaced =
- QStringLiteral("deleted-from:Inbox/SlackBuilds users");
+ QStringLiteral("moved-from:Inbox/SlackBuilds users");
QHash<QString, int> current;
current.insert(spaced, 1);