Skip to content

test: openclaw-tps-mail processes mail at startup (ops-gh1u)#268

Merged
tps-flint merged 2 commits intomainfrom
cp-ops-gh1u-tps-mail-startup-test
Apr 27, 2026
Merged

test: openclaw-tps-mail processes mail at startup (ops-gh1u)#268
tps-flint merged 2 commits intomainfrom
cp-ops-gh1u-tps-mail-startup-test

Conversation

@tps-anvil
Copy link
Copy Markdown
Collaborator

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 as seenFiles before the startup scan ran, causing processNewFile to no-op on every file.

Lines removed (in previous fix, f9f489b)

The pre-population loop that was deleted:

// Pre-populate seenFiles with existing entries in new/ so we don't replay
// mail from before plugin startup. This matches the behavior of the old
// tps mail watch CLI.
for (const agentId of boundAgents) {
  const newDir = resolve(account.mailDir, agentId, "new");
  if (!existsSync(newDir)) continue;
  try {
    for (const filename of readdirSync(newDir)) {
      seenFiles.add(resolve(newDir, filename));
    }
  } catch { /* ignore */ }
}

New test added

plugins/openclaw-tps-mail/test/startup.test.ts — two test cases:

  1. 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).

  2. 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

  • bun test plugins/openclaw-tps-mail/test/startup.test.ts — 2 pass
  • bun run build — clean
  • Full suite: 721 pass, 2 pre-existing fails (Flair offline, unrelated)

@tps-anvil tps-anvil requested a review from a team as a code owner April 27, 2026 17:37
tps-kern
tps-kern previously approved these changes Apr 27, 2026
Copy link
Copy Markdown

@tps-kern tps-kern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

Copy link
Copy Markdown
Contributor

@tps-sherlock tps-sherlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dispatch

Fix: 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 signal

If 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.

Copy link
Copy Markdown
Contributor

@tps-sherlock tps-sherlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dispatch

Fix: 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 signal

If 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.

Copy link
Copy Markdown
Contributor

@tps-sherlock tps-sherlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dispatch

Fix: 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 signal

If 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.

Copy link
Copy Markdown
Contributor

@tps-sherlock tps-sherlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dispatch

Fix: 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 signal

If 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)
Copy link
Copy Markdown
Contributor

@tps-sherlock tps-sherlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after fixes. All three concerns addressed:

  1. startPromise lifecycle: Both tests now properly await startPromise in a try/catch after abortController.abort(), preventing unhandled rejections.

  2. 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.

  3. 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.

@tps-flint tps-flint merged commit 21a2a62 into main Apr 27, 2026
11 checks passed
@tps-flint tps-flint deleted the cp-ops-gh1u-tps-mail-startup-test branch April 27, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants