From 2b6ecd2a1f624c12c071320ff1ed96404d75f839 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 31 Oct 2026 12:43:21 +0100 Subject: fix: handle every rule answer, and keep its failure out of errorOccurred A second Apply overwrote the pending token, so the first rule's answer was dropped as stale although its writes had landed: no undo, no pending count, no accounts. A write's answer cannot be superseded, so the token is gone and every ruleApplied() is handled. Pending counts are keyed by generation for the same reason, since a second Apply before the first count returned dropped the first Apply entirely. applyRule() no longer emits errorOccurred(). That path reverted an unrelated in-flight user write and its message was then overwritten by "nothing to change". The failure travels in the reply instead: ruleApplied(description, changes, error). openReadOnly() and writeTags() take an optional error out-parameter so the rule's path stays quiet. The window shows the error, still records and offers undo for whatever landed first, and names the rule from the reply's description. Item 212. Co-Authored-By: Claude Opus 5.5 --- tests/test_notmuchworker.cpp | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) (limited to 'tests/test_notmuchworker.cpp') diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index c4da146..68734ca 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -63,6 +63,7 @@ private slots: void applyRuleResolvesItsQueryOnceForEveryTag(); void applyRuleThatChangesNothingStillAnswers(); void applyRuleAnswersAcrossAQueuedConnection(); + void applyRuleCarriesItsFailureInTheReply(); void applyTagsToThreadsSpansMultipleThreads(); void applyTagsToThreadsWithNoThreadsDoesNothing(); @@ -1318,11 +1319,12 @@ void TestNotmuchWorker::applyRuleWritesEachTagOnlyWhereItMoves() worker.applyRule(QStringLiteral("thread:%1").arg(threadId), { QStringLiteral("ruleadd") }, { QStringLiteral("ruledrop") }, - QStringLiteral("Apply rule"), 7); + QStringLiteral("Apply rule")); QCOMPARE(applied.size(), 0); QCOMPARE(ruled.size(), 1); - QCOMPARE(ruled.first().at(0).value(), quint64(7)); + QCOMPARE(ruled.first().at(0).toString(), QStringLiteral("Apply rule")); + QVERIFY(ruled.first().at(2).toString().isEmpty()); const auto changes = ruled.first().at(1).value>(); QCOMPARE(changes.size(), 2); QCOMPARE(changes.at(0).added, QStringList{ QStringLiteral("ruleadd") }); @@ -1364,7 +1366,7 @@ void TestNotmuchWorker::applyRuleResolvesItsQueryOnceForEveryTag() worker.applyRule( QStringLiteral("thread:%1 and tag:backfill-src").arg(threadId), {}, { QStringLiteral("backfill-src"), QStringLiteral("backfill-other") }, - QStringLiteral("Apply rule"), 1); + QStringLiteral("Apply rule")); QCOMPARE(ruled.size(), 1); const auto changes = ruled.first().at(1).value>(); @@ -1390,11 +1392,11 @@ void TestNotmuchWorker::applyRuleThatChangesNothingStillAnswers() QSignalSpy ruled(&worker, &NotmuchWorker::ruleApplied); worker.applyRule(QStringLiteral("thread:%1").arg(threadId), {}, { QStringLiteral("nobody-has-this") }, - QStringLiteral("Apply rule"), 3); + QStringLiteral("Apply rule")); QCOMPARE(applied.size(), 0); QCOMPARE(ruled.size(), 1); - QCOMPARE(ruled.first().at(0).value(), quint64(3)); + QVERIFY(ruled.first().at(2).toString().isEmpty()); const auto changes = ruled.first().at(1).value>(); for (const TagChange &change : changes) QVERIFY(change.messageIds.isEmpty()); @@ -1417,7 +1419,8 @@ void TestNotmuchWorker::applyRuleAnswersAcrossAQueuedConnection() int answers = 0; QVector received; connect(&worker, &NotmuchWorker::ruleApplied, &receiver, - [&](quint64, const QVector &changes) { + [&](const QString &, const QVector &changes, + const QString &) { ++answers; received = changes; }); @@ -1426,8 +1429,7 @@ void TestNotmuchWorker::applyRuleAnswersAcrossAQueuedConnection() Q_ARG(QString, QStringLiteral("thread:%1").arg(threadId)), Q_ARG(QStringList, QStringList{ QStringLiteral("crossed") }), Q_ARG(QStringList, QStringList{}), - Q_ARG(QString, QStringLiteral("Apply rule")), - Q_ARG(quint64, 9))); + Q_ARG(QString, QStringLiteral("Apply rule")))); QTRY_COMPARE_WITH_TIMEOUT(answers, 1, 5000); QCOMPARE(received.size(), 1); @@ -1442,6 +1444,29 @@ void TestNotmuchWorker::applyRuleAnswersAcrossAQueuedConnection() QStringLiteral("restore") }); } +void TestNotmuchWorker::applyRuleCarriesItsFailureInTheReply() +{ + // A backfill's failure travels in its own reply. Through errorOccurred() + // the window would revert whatever unrelated user write was in flight, + // and its message would be overwritten by "nothing to change". + QTemporaryDir dir; + QVERIFY(dir.isValid()); + NotmuchWorker worker(dir.filePath(QStringLiteral("no-such-config"))); + QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred); + QSignalSpy ruled(&worker, &NotmuchWorker::ruleApplied); + + worker.applyRule(QStringLiteral("tag:inbox"), { QStringLiteral("x") }, {}, + QStringLiteral("Apply rule \"x\"")); + + QCOMPARE(errors.size(), 0); + QCOMPARE(ruled.size(), 1); + QCOMPARE(ruled.first().at(0).toString(), QStringLiteral("Apply rule \"x\"")); + QVERIFY(!ruled.first().at(2).toString().isEmpty()); + const auto changes = ruled.first().at(1).value>(); + for (const TagChange &change : changes) + QVERIFY(change.messageIds.isEmpty()); +} + void TestNotmuchWorker::applyTagsToThreadsSpansMultipleThreads() { const QString threadA = threadIdOf(QStringLiteral("Release notes")); -- cgit v1.2.3