diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-26 19:19:55 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-26 19:19:55 +0200 |
| commit | 1c034f6358f17c5c1d0eeaa04c42c33fac125d93 (patch) | |
| tree | 6fc7e7b604f95d87c51c19d7a8cb932fa2840af4 /tests | |
| parent | 6ee94127510862139e89e8d653cd4994e27e5ffe (diff) | |
| download | qtmaildir-1c034f6358f17c5c1d0eeaa04c42c33fac125d93.tar.gz qtmaildir-1c034f6358f17c5c1d0eeaa04c42c33fac125d93.zip | |
feat: resolve pending-change ids to subjects
Item 119, second half of the data: the step that turns the snapshot's ids
into something worth showing.
resolvePendingSubjects() takes the rows' ids in order, each flagged as a
thread id or a message id, and answers positionally: one subject per input,
plus the thread's message total for a thread id and -1 for a message id.
Positional rather than set-based, and that is load-bearing. The caller has
already decided what its rows are and in what order, and one id can
legitimately appear on several rows: a message with two outstanding actions
is two rows carrying one id. A combined query returns a set, which loses
both the order and the duplicate, so the walk is one lookup per row instead.
The cost is bounded by what the user did by hand since the last sync, which
is not a query-sized number.
A missing id answers with an EMPTY subject rather than being dropped. The
dialog still shows that row, because the count the user clicked has to equal
the list they are shown, and dropping a row breaks that agreement in exactly
the case where the user is most likely to notice. An index that cannot be
opened answers the same way, one empty subject per row, so the list still
shows the changes with only the subjects missing.
The thread count is taken at snapshot time and says so: a held thread edit
applies when the sync ends, and a reply arriving in between makes the real
number larger. The row describes what the user is looking at, not what the
write will touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P88Q3MCSCSQxKDy7pmXh9F
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_notmuchworker.cpp | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index 9a0896d..bcde45c 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -58,6 +58,8 @@ private slots: void applyTagsToThreadsSpansMultipleThreads(); void applyTagsToThreadsWithNoThreadsDoesNothing(); + void pendingSubjectsAnswerPositionally(); + void aMissingPendingIdYieldsAnEmptySubject(); void requestAllTagsReturnsSortedTags(); void requestAllTagsOnUnreadableConfigEmitsError(); @@ -962,6 +964,71 @@ void TestNotmuchWorker::applyTagsToThreadsWithNoThreadsDoesNothing() QVERIFY(errors.isEmpty()); } +void TestNotmuchWorker::pendingSubjectsAnswerPositionally() +{ + // Item 119. The dialog has already decided what its rows are and in what + // order, so the answer is positional: one subject per input id, in the + // same order, whatever those ids are. + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy spy(&worker, &NotmuchWorker::pendingSubjectsResolved); + + // The SAME message id twice, which is what a message with two outstanding + // actions produces. A combined query would return it once and the rows + // would no longer line up. + const QStringList ids { QStringLiteral("b1@example.org"), + QStringLiteral("b1@example.org") }; + worker.resolvePendingSubjects(ids, { false, false }); + + QCOMPARE(spy.size(), 1); + const QStringList subjects = spy.first().at(0).toStringList(); + const QList<int> counts = spy.first().at(1).value<QList<int>>(); + QCOMPARE(subjects.size(), 2); + QCOMPARE(counts.size(), 2); + + // Both rows carry the subject, and neither claims a message count: a + // message id is not a thread. + QVERIFY2(!subjects.at(0).isEmpty(), "the subject did not resolve"); + QCOMPARE(subjects.at(0), subjects.at(1)); + QCOMPARE(counts.at(0), -1); + QCOMPARE(counts.at(1), -1); +} + +void TestNotmuchWorker::aMissingPendingIdYieldsAnEmptySubject() +{ + // A stale row: the id is no longer in the index. It must come back EMPTY + // rather than being dropped, because the dialog still has to show it. The + // count the user clicked has to equal the list they are shown, and a + // dropped row breaks that in the one case where it matters most. + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy spy(&worker, &NotmuchWorker::pendingSubjectsResolved); + + worker.resolvePendingSubjects( + { QStringLiteral("b1@example.org"), + QStringLiteral("gone@example.org") }, + { false, false }); + + QCOMPARE(spy.size(), 1); + const QStringList subjects = spy.first().at(0).toStringList(); + QCOMPARE(subjects.size(), 2); + QVERIFY(!subjects.at(0).isEmpty()); + QVERIFY2(subjects.at(1).isEmpty(), + "a missing id must answer empty, not drop its row"); + + // A thread id resolves to its subject AND its message count, which is what + // a thread-scoped row reports. + QSignalSpy threadSpy(&worker, &NotmuchWorker::pendingSubjectsResolved); + const QVector<ThreadSummary> threads = + runQuery(QStringLiteral("subject:Preventivo")); + QCOMPARE(threads.size(), 1); + worker.resolvePendingSubjects({ threads.first().threadId }, { true }); + + QCOMPARE(threadSpy.size(), 1); + QCOMPARE(threadSpy.first().at(0).toStringList().size(), 1); + QVERIFY(!threadSpy.first().at(0).toStringList().at(0).isEmpty()); + QCOMPARE(threadSpy.first().at(1).value<QList<int>>().at(0), + threads.first().totalCount); +} + void TestNotmuchWorker::requestAllTagsReturnsSortedTags() { NotmuchWorker worker(m_fixture.configPath()); |
