From a346bef6c08e9725f3ab41f8237f01e9eb69eea7 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 28 Aug 2026 17:56:25 +0200 Subject: feat: judge a row's membership on the thread's union Closes item 170 under item 177. A conversation belongs to a view while any of its messages match, so reading one message of a thread no longer takes the conversation out of the Unread view. The current row is never evicted, and an automatic write defers its eviction until the selection moves. --- tests/test_mainwindow.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) (limited to 'tests') diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 26ec485..2d15fe8 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -554,6 +554,10 @@ private slots: void replyToAConversationAnswersItsNewestMessage(); void replyIsUntouchedOnAMessageRow(); + // Item 177, task 6: membership is the union over the conversation. + void aConversationStaysWhileAnyMessageMatches(); + void aConversationLeavesWhenItsUnionEmpties(); + private: /// Owns the throwaway lock table init() points every test at. A pointer /// rather than a value because it is rebuilt per test, and QTemporaryDir @@ -5561,6 +5565,17 @@ void TestMainWindow::aMixedThreadIsMarkedReadAndTheSecondPressIsTheWayBack() auto *view = window.findChild(); QVERIFY(view); + // A query with no filter tag, so viewFilterTag() answers empty and the + // membership sync (item 177, task 6) stays out of this. Under the default + // startup query the first press correctly EVICTS the row it just marked + // read, and the assertions below would then be reading a list that no + // longer holds the thread. What is under test here is the toggle's + // direction, which membership neither helps nor hinders. + auto *queryEdit = + window.findChild(QStringLiteral("queryEdit")); + QVERIFY(queryEdit); + queryEdit->setText(QStringLiteral("thread:T1")); + // MIXED: the union carries `unread` because some message is unread, while // others are not. A thread whose messages are all in one state answers // identically whichever way the direction is computed, so a uniform @@ -15293,4 +15308,101 @@ void TestMainWindow::replyIsUntouchedOnAMessageRow() .arg(replyAction->text()))); } +void TestMainWindow::aConversationStaysWhileAnyMessageMatches() +{ + // The reported case: a 44-message thread with two replies still unread. + // Reading one message must not evict the conversation. + const Config config; + MainWindow window(config); + + auto *model = window.findChild(); + auto *queryEdit = + window.findChild(QStringLiteral("queryEdit")); + QVERIFY(model && queryEdit); + + queryEdit->setText( + config.resolvedQuery(Config::builtinFilter(QStringLiteral("unread")), + QString())); + + ThreadSummary other = makeThread(QStringLiteral("t1"), + QStringList{ QStringLiteral("unread") }); + other.totalCount = 1; + ThreadSummary big = makeThread(QStringLiteral("t2"), + QStringList{ QStringLiteral("unread") }); + big.totalCount = 44; + model->appendBatch({ other, big }); + + // One message of the conversation is read, and specifically the one its + // ROW displays: naming any other id makes the model answer "no thread" and + // the test measures nothing, passing against an eviction that judges on + // the card's own tags. The union still says unread, so the row stays. + window.sendMessageTagChangeForTesting({ big.firstMessageId }, {}, + { QStringLiteral("unread") }, + QStringLiteral("Mark read")); + + QCOMPARE(model->rowCount(QModelIndex()), 2); + QCOMPARE(model->threadAt(1).threadId, QStringLiteral("t2")); + + // The same question put to the judgement ITSELF, which is where the union + // rule lives. The send path above reaches it only for a thread of one, so + // asserting through that path alone would let an eviction judging the + // card's own tags pass: the long thread is filtered out before the model + // is ever asked. Here the row is named directly, and the only thing + // keeping it is that its union still carries `unread` while the message + // its card draws no longer does. + QVERIFY2(!model->messageById(big.firstMessageId) + .tags.contains(QStringLiteral("unread")), + "the fixture's card message is still unread, so this assertion " + "cannot tell the union apart from the card's own tags"); + model->removeThreadsWithoutTag(QStringList{ QStringLiteral("t2") }, + QStringLiteral("unread")); + QCOMPARE(model->rowCount(QModelIndex()), 2); + + // And the counterpart, so the test is about the union and not about + // nothing ever being evicted: a one-message row's union IS its message, so + // reading it takes the row out at once. + window.sendMessageTagChangeForTesting({ other.firstMessageId }, {}, + { QStringLiteral("unread") }, + QStringLiteral("Mark read")); + + QCOMPARE(model->rowCount(QModelIndex()), 1); + QCOMPARE(model->threadAt(0).threadId, QStringLiteral("t2")); +} + +void TestMainWindow::aConversationLeavesWhenItsUnionEmpties() +{ + const Config config; + MainWindow window(config); + + auto *model = window.findChild(); + auto *view = window.findChild(); + auto *queryEdit = + window.findChild(QStringLiteral("queryEdit")); + QVERIFY(model && view && queryEdit); + + queryEdit->setText( + config.resolvedQuery(Config::builtinFilter(QStringLiteral("unread")), + QString())); + + ThreadSummary keep = makeThread(QStringLiteral("t1"), + QStringList{ QStringLiteral("unread") }); + keep.totalCount = 1; + ThreadSummary go = makeThread(QStringLiteral("t2"), + QStringList{ QStringLiteral("unread") }); + go.totalCount = 4; + model->appendBatch({ keep, go }); + + // Selected elsewhere, so the never-evict-the-current-row rule is not what + // is being measured here. + selectThreadRow(view, 0); + QApplication::processEvents(); + + window.sendThreadTagChangeForTesting({ QStringLiteral("t2") }, {}, + { QStringLiteral("unread") }, + QStringLiteral("Mark thread read")); + + QCOMPARE(model->rowCount(QModelIndex()), 1); + QCOMPARE(model->threadAt(0).threadId, QStringLiteral("t1")); +} + #include "test_mainwindow.moc" -- cgit v1.2.3