aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_notmuchworker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_notmuchworker.cpp')
-rw-r--r--tests/test_notmuchworker.cpp105
1 files changed, 105 insertions, 0 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index 3f75898..9a0896d 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -65,6 +65,8 @@ private slots:
void loadMessageOnAnUnknownIdReturnsNothing();
void aQueryCarriesEachThreadsFirstMessageId();
void aSentQueryCarriesTheMatchedMessageNotTheThreadsFirst();
+ void queryCarriesTheFirstMessageSender();
+ void sendersAreCountedForTheCandidateList();
void loadThreadTreeReportsReplyDepth();
void loadThreadTreeCarriesTheFactsARowNeeds();
@@ -74,6 +76,7 @@ private slots:
void recipientsAreAbsentUnlessAskedFor();
void recipientsAreFoldedWhenAskedFor();
void recipientsCrossAQueuedCall();
+ void theFirstRecipientsAddressCrossesForTheAvatar();
void requestCountsAnswersOneCountPerQuery();
void requestCountsKeepsPositionOnAnInvalidQuery();
@@ -515,6 +518,79 @@ void TestNotmuchWorker::aSentQueryCarriesTheMatchedMessageNotTheThreadsFirst()
}
}
+void TestNotmuchWorker::queryCarriesTheFirstMessageSender()
+{
+ // Item 169. The card has no address to hash: `authors` is notmuch's own
+ // summarised string and carries display names only, measured on the real
+ // index as 'Ryanair' and 'The Hacker News tramite LinkedIn', with no `@`
+ // anywhere. firstMessageSender is the BARE address of the one message the
+ // root card stands for.
+ //
+ // The From header carries a display name on purpose: a wrong
+ // implementation returning the display name or the authors string fails
+ // this test.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.isValid());
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("sender-probe@example.org"),
+ QStringLiteral("Probe subject"),
+ QStringLiteral("Probe <sender-probe@example.org>"),
+ QStringLiteral("Mon, 8 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body")));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+
+ QSignalSpy spy(&worker, &NotmuchWorker::threadsReady);
+ worker.runQuery(QStringLiteral("subject:\"Probe subject\""), 1,
+ NotmuchWorker::NewestFirst, false);
+ QVERIFY(spy.count() > 0);
+
+ const auto threads = spy.first().at(0).value<QVector<ThreadSummary>>();
+ QCOMPARE(threads.size(), 1);
+ // The bare address, not the display name and not notmuch's authors string.
+ QCOMPARE(threads.first().firstMessageSender,
+ QStringLiteral("sender-probe@example.org"));
+}
+
+void TestNotmuchWorker::sendersAreCountedForTheCandidateList()
+{
+ // The counts that BusinessSenders::appendCandidates() consumes: per-sender
+ // totals over a query, keyed by the lower-cased BARE address (a display
+ // name would defeat the bulk-sender guess). Two messages from one sender
+ // must count twice.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.isValid());
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("n1@example.org"),
+ QStringLiteral("Receipt one"),
+ QStringLiteral("noreply@shop.example"),
+ QStringLiteral("Mon, 8 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body")));
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("n2@example.org"),
+ QStringLiteral("Receipt two"),
+ QStringLiteral("noreply@shop.example"),
+ QStringLiteral("Tue, 9 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body")));
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"),
+ QStringLiteral("j1@example.org"),
+ QStringLiteral("Hello"),
+ QStringLiteral("john@example.org"),
+ QStringLiteral("Wed, 10 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body")));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy spy(&worker, &NotmuchWorker::senderCountsReady);
+ worker.countSenders(QStringLiteral("*"));
+ QVERIFY(spy.count() > 0);
+
+ const auto counts = spy.first().at(0).value<QHash<QString, int>>();
+ QCOMPARE(counts.value(QStringLiteral("noreply@shop.example")), 2);
+ QCOMPARE(counts.value(QStringLiteral("john@example.org")), 1);
+}
+
void TestNotmuchWorker::loadThreadTreeReportsReplyDepth()
{
// Thread A is a root plus one reply carrying In-Reply-To, which is what
@@ -1003,6 +1079,35 @@ void TestNotmuchWorker::recipientsAreFoldedWhenAskedFor()
"two plus one: %1").arg(summary)));
}
+void TestNotmuchWorker::theFirstRecipientsAddressCrossesForTheAvatar()
+{
+ // Item 169's flat-view avatar. `recipients` is a DISPLAY summary and
+ // carries no address at all when every recipient has a name, so the hash
+ // needs the bare one; it rides the same fold, so it costs nothing extra.
+ const QVector<ThreadSummary> one =
+ runQuery(QStringLiteral("subject:Preventivo"),
+ NotmuchWorker::NewestFirst, true);
+ QCOMPARE(one.size(), 1);
+ QCOMPARE(one.at(0).firstMessageRecipient,
+ QStringLiteral("mario@example.org"));
+
+ // A quoted display name containing a comma must not defeat the parse, for
+ // the same reason it must not defeat the summary.
+ const QVector<ThreadSummary> many =
+ runQuery(QStringLiteral("subject:Riunione"),
+ NotmuchWorker::NewestFirst, true);
+ QCOMPARE(many.size(), 1);
+ QCOMPARE(many.at(0).firstMessageRecipient,
+ QStringLiteral("mario@example.org"));
+
+ // And it stays empty when the query never asked, exactly as `recipients`
+ // does: it is behind the same performance contract.
+ const QVector<ThreadSummary> unasked =
+ runQuery(QStringLiteral("subject:Preventivo"));
+ QCOMPARE(unasked.size(), 1);
+ QVERIFY(unasked.at(0).firstMessageRecipient.isEmpty());
+}
+
void TestNotmuchWorker::recipientsCrossAQueuedCall()
{
// The trap CLAUDE.md records for SortOrder, in the shape it takes for this