app 装配 mail:channel 作首个订阅方、出站经 mail.send、mail.send 工具真发信(dryRun 只校验)、轮询工作者;上传件存取抽出共用

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 02:05:50 +08:00
co-authored by Claude Fable 5
parent 58358210e2
commit 0c6e8e2c1b
10 changed files with 449 additions and 48 deletions
@@ -0,0 +1,47 @@
/** 轮询工作者:每 tick 调 pollDue;上一 tick 没跑完就跳过这一拍;出错的账号写 stderr;stop 后不再跑 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MailPollOutcome } from '@contracts';
import { pollMailOnce, startMailPoller } from '../workers/mail-poller.ts';
function fakeMail(outcomes: MailPollOutcome[], holdMs = 0) {
const calls: Date[] = [];
return {
calls,
mail: {
pollDue: async (now: Date) => {
calls.push(now);
if (holdMs > 0) await new Promise((r) => setTimeout(r, holdMs));
return outcomes;
},
},
};
}
describe('pollMailOnce', () => {
it('回用例的结果;出错的账号写一行 stderr', async () => {
const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
const { mail } = fakeMail([{ accountId: 'a', pulled: 1, stored: 1, dispatched: 1, more: false }, { accountId: 'b', pulled: 0, stored: 0, dispatched: 0, more: false, error: '连不上' }]);
const out = await pollMailOnce({ container: { mail: mail as never } });
expect(out).toHaveLength(2);
expect(write).toHaveBeenCalledWith(expect.stringContaining('[mail-poller] b:连不上'));
write.mockRestore();
});
});
describe('startMailPoller', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('按 tick 调;在跑时跳过;stop 后停', async () => {
const { mail, calls } = fakeMail([], 1000);
const poller = startMailPoller({ container: { mail: mail as never } }, 100);
await vi.advanceTimersByTimeAsync(350);
expect(calls).toHaveLength(1); // 第一拍 1000ms 没跑完,后面两拍跳过
await vi.advanceTimersByTimeAsync(1000);
expect(calls.length).toBeGreaterThanOrEqual(2);
poller.stop();
const before = calls.length;
await vi.advanceTimersByTimeAsync(2000);
expect(calls).toHaveLength(before);
});
});