test: openclaw-tps-mail processes mail at startup (ops-gh1u)#268
test: openclaw-tps-mail processes mail at startup (ops-gh1u)#268
Conversation
tps-sherlock
left a comment
There was a problem hiding this comment.
Two test-hygiene issues that will likely flake in CI. Both are in the promise lifecycle around startAccount.
Test 1 — unhandled startPromise
The promise returned by startAccount is created but never awaited or caught:
const startPromise = capturedPlugin.gateway.startAccount(ctx);
// ... assertions ...
// afterEach aborts, but startPromise is never awaited — unhandled rejection if it throws after dispatchFix: add await startPromise (or await expect(startPromise).rejects.toThrow() if abort triggers rejection) before the test exits, or wrap in a try/catch if the abort is expected to reject.
Test 2 — await startPromise after abort may throw
abortController.abort();
await startPromise; // will throw if startAccount rejects on abort signalIf startAccount rejects when the abort signal fires, this line throws an unhandled error after the assertions, causing a test failure. Should be:
abortController.abort();
try { await startPromise; } catch { /* expected on abort */ }Minor: race in Test 1 file-move assertion
await new Promise((r) => setTimeout(r, 50));
expect(readdirSync(curDir).length).toBe(1);50ms is tight for slow CI runners. Consider polling (with a timeout) rather than a fixed sleep, or increase to something like 500ms.
Otherwise the test coverage looks correct — it validates that pre-existing mail files in new/ are dispatched at startup rather than being silently skipped due to a pre-populated seenFiles set. No fixture leaks detected; each test uses its own mkdtempSync directory.
tps-sherlock
left a comment
There was a problem hiding this comment.
Two test-hygiene issues that will likely flake in CI. Both are in the promise lifecycle around startAccount.
Test 1 — unhandled startPromise
The promise returned by startAccount is created but never awaited or caught:
const startPromise = capturedPlugin.gateway.startAccount(ctx);
// ... assertions ...
// afterEach aborts, but startPromise is never awaited — unhandled rejection if it throws after dispatchFix: add await startPromise (or await expect(startPromise).rejects.toThrow() if abort triggers rejection) before the test exits, or wrap in a try/catch if the abort is expected to reject.
Test 2 — await startPromise after abort may throw
abortController.abort();
await startPromise; // will throw if startAccount rejects on abort signalIf startAccount rejects when the abort signal fires, this line throws an unhandled error after the assertions, causing a test failure. Should be:
abortController.abort();
try { await startPromise; } catch { /* expected on abort */ }Minor: race in Test 1 file-move assertion
await new Promise((r) => setTimeout(r, 50));
expect(readdirSync(curDir).length).toBe(1);50ms is tight for slow CI runners. Consider polling (with a timeout) rather than a fixed sleep, or increase to something like 500ms.
Otherwise the test coverage looks correct — it validates that pre-existing mail files in new/ are dispatched at startup rather than being silently skipped due to a pre-populated seenFiles set. No fixture leaks detected; each test uses its own mkdtempSync directory.
tps-sherlock
left a comment
There was a problem hiding this comment.
Two test-hygiene issues that will likely flake in CI. Both are in the promise lifecycle around startAccount.
Test 1 — unhandled startPromise
The promise returned by startAccount is created but never awaited or caught:
const startPromise = capturedPlugin.gateway.startAccount(ctx);
// ... assertions ...
// afterEach aborts, but startPromise is never awaited — unhandled rejection if it throws after dispatchFix: add await startPromise (or await expect(startPromise).rejects.toThrow() if abort triggers rejection) before the test exits, or wrap in a try/catch if the abort is expected to reject.
Test 2 — await startPromise after abort may throw
abortController.abort();
await startPromise; // will throw if startAccount rejects on abort signalIf startAccount rejects when the abort signal fires, this line throws an unhandled error after the assertions, causing a test failure. Should be:
abortController.abort();
try { await startPromise; } catch { /* expected on abort */ }Minor: race in Test 1 file-move assertion
await new Promise((r) => setTimeout(r, 50));
expect(readdirSync(curDir).length).toBe(1);50ms is tight for slow CI runners. Consider polling (with a timeout) rather than a fixed sleep, or increase to something like 500ms.
Otherwise the test coverage looks correct — it validates that pre-existing mail files in new/ are dispatched at startup rather than being silently skipped due to a pre-populated seenFiles set. No fixture leaks detected; each test uses its own mkdtempSync directory.
tps-sherlock
left a comment
There was a problem hiding this comment.
Two test-hygiene issues that will likely flake in CI. Both are in the promise lifecycle around startAccount.
Test 1 — unhandled startPromise
The promise returned by startAccount is created but never awaited or caught:
const startPromise = capturedPlugin.gateway.startAccount(ctx);
// ... assertions ...
// afterEach aborts, but startPromise is never awaited — unhandled rejection if it throws after dispatchFix: add await startPromise (or await expect(startPromise).rejects.toThrow() if abort triggers rejection) before the test exits, or wrap in a try/catch if the abort is expected to reject.
Test 2 — await startPromise after abort may throw
abortController.abort();
await startPromise; // will throw if startAccount rejects on abort signalIf startAccount rejects when the abort signal fires, this line throws an unhandled error after the assertions, causing a test failure. Should be:
abortController.abort();
try { await startPromise; } catch { /* expected on abort */ }Minor: race in Test 1 file-move assertion
await new Promise((r) => setTimeout(r, 50));
expect(readdirSync(curDir).length).toBe(1);50ms is tight for slow CI runners. Consider polling (with a timeout) rather than a fixed sleep, or increase to something like 500ms.
Otherwise the test coverage looks correct — it validates that pre-existing mail files in new/ are dispatched at startup rather than being silently skipped due to a pre-populated seenFiles set. No fixture leaks detected; each test uses its own mkdtempSync directory.
… moves - Test 1: abort + try/catch await startPromise before test exit - Test 1: replace 50ms sleep with pollUntil() loop (2s timeout) - Test 2: wrap await startPromise in try/catch (abort may reject)
tps-sherlock
left a comment
There was a problem hiding this comment.
Approved after fixes. All three concerns addressed:
-
startPromise lifecycle: Both tests now properly await startPromise in a try/catch after abortController.abort(), preventing unhandled rejections.
-
Race-free file-move assertion: The brittle 50ms setTimeout is replaced with pollUntil(conditionFn, 2000) — 50ms polling interval with a 2s timeout, which will not flake on slow CI runners.
-
No fixture leaks: Each test still uses its own mkdtempSync directory cleaned up in afterEach.
Clean regression test for the ops-h2zy startup race. LGTM.
Why
Regression test for ops-h2zy / cli#267 (commit f9f489b). The startup race was hit twice in 24h on rockit (which had stale plugin code) — a test would have caught a regression instantly, and the fix is one deletion away from being undone with no guard.
The behavior locked in: mail files existing in
<agent>/new/at plugin startup MUST be processed, not silently skipped. The previous bug was a pre-population loop that marked all files asseenFilesbefore the startup scan ran, causingprocessNewFileto no-op on every file.Lines removed (in previous fix, f9f489b)
The pre-population loop that was deleted:
New test added
plugins/openclaw-tps-mail/test/startup.test.ts— two test cases:processes mail file present in new/ at startup: Writes a valid mail envelope into agent/new/ before starting the plugin, then asserts that dispatchReplyWithBufferedBlockDispatcher is called with the correct message context (From, To, MessageSid, Body) and that the file moves from new/ to cur/ (acked).
does not double-process a file (dedup via seenFiles): Verifies that the seenFiles dedup prevents a file from being dispatched more than once. Asserts dispatchReplyWithBufferedBlockDispatcher is called exactly once.
Test plan