From 3fd999907ae8344f76ee4e5be1ac278a26f452ca Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 29 Aug 2026 11:25:02 +0200 Subject: feat: have the sync script report what it did Item 174, and half of item 125. The premise was corrected before any code. The note asks for an external `notmuch new` to clear the pending count; it must not. That count means tag mutations not yet known to have reached the MAIL STORE, which is the server: an edit is in notmuch the moment it is made, and what is outstanding is mbsync pushing the renamed Maildir files. `notmuch new` re-indexes local files and pushes nothing, so clearing on it would tell the user their work was safe to quit on while it was still local. The entry's own proposal to watch notmuch_database_get_revision() was rejected for the same reason: a revision moves when mail ARRIVES too, and in neither case does it say anything about the server. What was actually wrong was the reporting channel. The application inferred a finished run from an inode in /proc/locks and from grepping the log for its RUN END banner, which made a human-readable line into wire format and could not say WHICH channels a run carried. The local sync path has always narrowed its clear to the accounts it carried; the external path could not, and cleared everything, so an edit to an account a run never touched was reported as delivered. So the script reports instead of leaving evidence to be inferred. It writes ~/.local/state/qtmaildir/syncstatus.json atomically at the end of every run, including a skip, naming the channels, both exit statuses and a state of ok, failed or skipped. MailSync::readStatus() reads it, MainWindow prefers it over the log banner and narrows the clear through Account::syncChannel(). A skipped run clears nothing, which is item 125's first half: the application can now see that a run happened and carried nothing. The log banner and lastRunOutcome() stay as the fallback for a missing file, which is what a first run after upgrading looks like. This is the user's own framing of the scope: the script was written for another system and adapted, and is now qtmaildir's only consumer, so it serves the application rather than the reverse. Two facts made it safe to act on: their crontab runs mailsync.sh and nothing else touches mail, and ~/bin/mailsync.sh is a symlink into this repo, so an edit is live on the next tick. Two bugs found while wiring it in, both recorded in the closed item. A test read the developer's real sync state, twice: a [sync] section naming only `log` leaves syncStatus() defaulting to the real file, so two tests asserting that a FAILED run leaves the count alone read the last real cron run, found ok, and cleared. Pinning only `status` has the mirror problem. noSyncTestReadsTheRealSyncState() is the guard, modelled on noTestCanSeeTheRealLockTable(). And Qt::ISODate carries no milliseconds. The status file is preferred only when it describes THIS run, compared against when the lock appeared, so a stale success cannot outrank a fresh failure; but the script writes date -Iseconds, and a round trip of "now" comes back 329 ms behind, measured. A fast sync's own file therefore parsed as stale and fell back to the log, with nothing failing to say so. One second of slack matches the precision the format carries. Design: docs/superpowers/specs/2026-08-29-sync-status-file-design.md Suite: 43 of 44, with undoMovesTheMessageBack failing as it does on master (item 136). --- src/mainwindow.h | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'src/mainwindow.h') diff --git a/src/mainwindow.h b/src/mainwindow.h index 2b217d2..9c93f4f 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -202,6 +202,20 @@ public: /// command was pushed, which is what "this did nothing" has to assert. int undoDepthForTesting() const { return m_undoStack.count(); } + /// Item 174. The set of accounts with edits not yet known to have reached + /// the mail store, so a test can assert that an external sync cleared the + /// accounts it carried and ONLY those. + QSet editedAccountsForTesting() const { return m_editedAccounts; } + + /// Marks an account edited, standing in for the write funnels: a test + /// asserting which accounts a sync clears needs more than one of them + /// edited, and driving two real writes through a worker to arrange that + /// would test the funnels rather than the clearing. + Q_INVOKABLE void noteEditedAccountForTesting(const QString &accountKey) + { + m_editedAccounts.insert(accountKey); + } + /// Item 178. Stands in for the digest round trip, which a bare window has /// no worker to make. Sets what onThreadDigestLoaded() would have set. void setConversationPathsForTesting(const QString &threadId, @@ -1485,6 +1499,17 @@ private: /// half. Tracked here rather than read back from SyncMonitor so the state /// the UI acted on is the state it was told about. bool m_externalSyncBusy = false; + + /// When the external sync now running began, from the lock appearing. + /// + /// Item 174. The status file the script leaves is preferred over the log's + /// banner, but only when it describes THIS run: a stale file outranking a + /// fresh log would clear the pending count on an old success for a run that + /// has just failed, which is the indicator lying in the direction that + /// loses work. Invalid when no external sync has been observed, where the + /// comparison is skipped rather than failing closed on a file that may well + /// be current. + QDateTime m_externalSyncStartedAt; QUndoStack m_undoStack; QLineEdit *m_queryEdit = nullptr; -- cgit v1.2.3