summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md38
-rw-r--r--src/mainwindow.cpp17
-rw-r--r--src/mainwindow.h12
-rw-r--r--tests/test_mainwindow.cpp22
4 files changed, 85 insertions, 4 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
index 0186bfa..dd6a8ca 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
@@ -85,7 +85,7 @@ taking that too literally.
| 35 | No refresh of the thread list after a sync | workflow | M | open |
| 36 | `test_mainwindow` cannot reach the worker | testing | S | open, on demand |
| 37 | The worker stalls on a tag edit made during a background sync | correctness | S | **done** |
-| 38 | `test_mainwindow` fails when a real sync holds the lock | testing | XS | open |
+| 38 | `test_mainwindow` fails when a real sync holds the lock | testing | XS | **done** |
| 39 | Thread list cannot be sorted by clicking a column header | workflow | S | open |
| 40 | No live filter over the current view | workflow | M | open |
| 41 | A message whose HTML body carries a `Content-Id` renders blank | correctness | S | open |
@@ -93,7 +93,7 @@ taking that too literally.
| 43 | No "Mark all read" for the current view | workflow | S | open |
| 44 | No way to manage the filters applied at sync time | workflow | ? | open, unspecified |
| 45 | Two Sync buttons on the main window | discoverability | XS | open |
-| 46 | `uiStateSurvivesARestart` fails under the offscreen platform | testing | XS | open |
+| 46 | `uiStateSurvivesARestart` fails under the offscreen platform | testing | XS | **done** |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1917,6 +1917,30 @@ are unaffected either way; it is only the construction-time state that leaks in.
construction-time state IS the behaviour under test for this case, and a window
that never polls would pass the assertion for the wrong reason.
+### Outcome (done)
+
+`MainWindow::setLocksPathForTesting()` / `locksPath()` give the window the seam
+`SyncMonitor` already had, and the test points it at an empty file in its own
+`QTemporaryDir` so construction observes no sync. The constraint above is
+respected: the monitor is still constructed and still started, it simply reads a
+lock table the test controls.
+
+**A test seam, deliberately not a config key.** `/proc/locks` is not something a
+user would ever set, and a wrong value fails silently by disabling background
+sync detection rather than loudly. A `[general]` key was considered and rejected
+for that reason.
+
+**The override is process-wide and is reset at the end of the test**, since the
+`QTemporaryDir` holding the file is destroyed with it; leaving it set would
+point every later window at a path that no longer exists.
+
+**Verified by reproducing the original failure rather than waiting for cron.**
+Running the suite under `flock -n /tmp/mbsync.lock` fails the assertion exactly
+as reported when the seam is bypassed, and passes with it in place. The first
+mutation attempted was a dud worth recording: writing `MUTANT` into the injected
+lock table does not fail the test, because it is not a parseable `/proc/locks`
+line and `lockHeldIn()` correctly finds no lock in it.
+
## 39. Thread list cannot be sorted by clicking a column header
**Observed (user, 2026-08-05):** "left pane columns order by clicking on the
@@ -2207,6 +2231,16 @@ the property under test (the size that went in comes back out) is exact.
the machine they run on, and both were found by running the suite in a context
its author had not tried rather than by reading it.
+### Outcome (done)
+
+The asserted size is now 640x560, which fits the offscreen platform's 800x800
+screen. Nothing in `MainWindow` changed: the persistence was never broken, only
+the test's choice of a window wider than the smallest screen it runs against.
+
+Verified both ways round, since this one passed on Wayland throughout: 45 of 45
+under offscreen where it previously failed, and still green on the real
+platform.
+
## Deferred, unsized, or split out
Items noted while triaging but not part of the original list. Same numbering
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 7c7b801..75549d3 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -83,6 +83,21 @@ QString MainWindow::uiStatePath()
return base + QStringLiteral("/qtmaildir/uistate.conf");
}
+namespace {
+/// Overridden only by setLocksPathForTesting(); "/proc/locks" in every real run.
+QString g_locksPath = QStringLiteral("/proc/locks");
+} // namespace
+
+void MainWindow::setLocksPathForTesting(const QString &path)
+{
+ g_locksPath = path;
+}
+
+QString MainWindow::locksPath()
+{
+ return g_locksPath;
+}
+
void MainWindow::restoreUiState()
{
QSettings state(uiStatePath(), QSettings::IniFormat);
@@ -465,7 +480,7 @@ void MainWindow::buildUi()
// every ten minutes, so mail arrives and tags change while the window sits
// idle, and until now nothing here noticed.
m_syncMonitor = new SyncMonitor(SyncMonitor::defaultLockPath(),
- QStringLiteral("/proc/locks"), this);
+ locksPath(), this);
connect(m_syncMonitor, &SyncMonitor::stateChanged,
this, &MainWindow::onExternalSyncStateChanged);
m_syncMonitor->start();
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 22c8187..96b27f4 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -104,6 +104,18 @@ public:
/// preserve comments or key order).
static QString uiStatePath();
+ /// Kernel lock table every MainWindow's SyncMonitor watches, "/proc/locks"
+ /// unless a test overrides it.
+ ///
+ /// A test seam, deliberately NOT a config key: /proc/locks is not something
+ /// a user would ever set, and a wrong value silently disables background
+ /// sync detection rather than failing loudly. Without this every window a
+ /// test builds observes the machine's real sync state, so a test asserting
+ /// on the sync button fails whenever the user's cron sync happens to be
+ /// running (item 38).
+ static void setLocksPathForTesting(const QString &path);
+ static QString locksPath();
+
protected:
void closeEvent(QCloseEvent *event) override;
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index c409d1a..6a4f903 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -233,7 +233,11 @@ void TestMainWindow::uiStateSurvivesARestart()
QStandardPaths::setTestModeEnabled(true);
QFile::remove(MainWindow::uiStatePath());
- const QSize resized(940, 620);
+ // Must fit the smallest screen this ever runs against: the offscreen
+ // platform reports 800x800, and restoreGeometry() clamps to the available
+ // area, so a 940px width came back as 798 and failed only under offscreen.
+ // The number carries no meaning beyond differing from the default size.
+ const QSize resized(640, 560);
{
const Config config;
MainWindow window(config);
@@ -995,6 +999,17 @@ void TestMainWindow::theSyncButtonIsDisabledWhileABackgroundSyncHoldsTheLock()
s.setValue(QStringLiteral("sync/command"), QStringLiteral("/bin/true"));
}
+ // An empty lock table, so construction observes no sync. Against the real
+ // /proc/locks this assertion fails whenever the user's cron sync happens to
+ // be running: cron fires every ten minutes and a run lasts ~35s, so roughly
+ // 6% of runs landed inside one and the failure looked like flakiness.
+ const QString locks = dir.filePath(QStringLiteral("locks"));
+ {
+ QFile f(locks);
+ QVERIFY(f.open(QIODevice::WriteOnly));
+ }
+ MainWindow::setLocksPathForTesting(locks);
+
Config config;
config.load(conf);
MainWindow window(config);
@@ -1014,6 +1029,11 @@ void TestMainWindow::theSyncButtonIsDisabledWhileABackgroundSyncHoldsTheLock()
SyncMonitor::State::Idle));
QVERIFY2(button->isEnabled(),
"the sync button was not re-enabled after the background sync");
+
+ // The override is process-wide, and QTemporaryDir takes the file with it at
+ // the end of this scope: leaving it set would point every later window at a
+ // path that no longer exists.
+ MainWindow::setLocksPathForTesting(QStringLiteral("/proc/locks"));
}
void TestMainWindow::anUnobservableLockTableLeavesTheSyncButtonUsable()