/** * 这一份钉的是 Task 9 评审逮出的三条 Important 与一条设计律 —— * 它们都在拉取 / 发信 / 分发那几条**当时还没有测试**的路上。 */ import { describe, expect, it } from 'vitest'; import { mailSendInputSchema } from '@contracts'; import { createMailService } from '../internal/usecase/mail-service.ts'; import { buildInboundRecord } from '../internal/domain/inbound-record.ts'; import { createPostalMimeParser } from '../internal/adapter/postal-mime-parser.ts'; import type { OutboundRecord, ParsedMail } from '../port.ts'; import { account, fakeProvider, fixedClock, memoryAccounts, memoryAttachments, memoryInbound, memoryOutbound, plainSecrets, seqIds } from './fakes.ts'; function harness() { const clock = fixedClock(); const accounts = memoryAccounts(clock); const inbound = memoryInbound(); const outbound = memoryOutbound(); const imap = fakeProvider('imap-smtp'); const wecom = fakeProvider('wecom'); const changes: string[] = []; const service = createMailService({ accounts, inbound, outbound, attachments: memoryAttachments(), providers: { 'imap-smtp': imap, wecom }, mime: createPostalMimeParser(), secrets: plainSecrets, clock, ids: seqIds('out'), notify: { changed: (r) => void changes.push(r) }, }); return { service, accounts, inbound, outbound, imap, wecom, clock, changes }; } const sendInput = mailSendInputSchema.parse({ to: ['buyer@acme.com'], subject: '报价', text: '见附', requestedBy: { kind: 'ops' } }); function outboundRow(over: Partial = {}): OutboundRecord { return { id: 'out-9', accountId: 'acc-1', messageId: '', idempotencyKey: 'k-1', to: ['buyer@acme.com'], cc: [], bcc: [], subject: '报价', text: '见附', html: null, inReplyTo: null, references: [], attachments: [], state: 'sending', error: null, providerRef: null, requestedBy: { kind: 'ops' }, createdAt: new Date('2026-08-25T10:00:00Z'), sentAt: null, ...over, }; } describe('凭证解不开', () => { it('密文解得开但不是 JSON:错误话里没有明文,只说「未配置凭证」', async () => { const { service, accounts, imap } = harness(); // 解得开(plainSecrets 会回出 `pw-1234`),但那不是 JSON —— JSON.parse 的 SyntaxError 把原文嵌在 message 里 await accounts.save(account({ secretCipher: 'sealed:pw-1234' })); const outcome = await service.pollNow('acc-1'); expect(outcome.error).not.toContain('pw-1234'); expect(outcome.error).toContain('未配置凭证'); expect(accounts.rows.get('acc-1')?.lastPollError).not.toContain('pw-1234'); expect(accounts.rows.get('acc-1')?.lastPollError).toContain('未配置凭证'); // 凭证都没打开,一次都不该去够厂商 expect(imap.pullCalls).toEqual([]); }); }); describe('幂等键', () => { it('同键的上一封还在飞:不发第二封,如实回没发出去', async () => { const { service, accounts, outbound, imap, clock } = harness(); await accounts.save(account()); await outbound.insert(outboundRow({ createdAt: clock.now() })); const result = await service.send({ ...sendInput, idempotencyKey: 'k-1' }); expect(result.state).toBe('failed'); expect(result.error).toContain('仍在发送中'); expect(imap.sent).toHaveLength(0); expect(outbound.rows.size).toBe(1); expect(outbound.rows.get('out-9')?.state).toBe('sending'); }); it('同键的 sending 停太久:当上次崩在半路,复用那一行重发', async () => { const { service, accounts, outbound, imap, clock } = harness(); await accounts.save(account()); await outbound.insert(outboundRow({ createdAt: new Date(clock.now().getTime() - 6 * 60_000) })); const result = await service.send({ ...sendInput, idempotencyKey: 'k-1' }); expect(result.state).toBe('sent'); expect(imap.sent).toHaveLength(1); expect(outbound.rows.get('out-9')?.state).toBe('sent'); // 复用,不是新开一行 expect(outbound.rows.size).toBe(1); expect(result.outboundId).toBe('out-9'); }); it('**并发复用要抢**:同键两路同时重发,真信只出一封,另一路如实回没发出去', async () => { /* * 本仓是两个后端连同一个库:两台同时重放同一次放行,都会读到同一行 `failed`、都判「可以复用」。 * 没有那一句条件更新的话,两台都发 —— **两封真邮件**,直接破掉契约白纸黑字的「同键不发两封」。 */ const { service, accounts, outbound, imap } = harness(); await accounts.save(account()); await outbound.insert(outboundRow({ state: 'failed', error: '上一次没发出去' })); const [a, b] = await Promise.all([ service.send({ ...sendInput, idempotencyKey: 'k-1' }), service.send({ ...sendInput, idempotencyKey: 'k-1' }), ]); expect(imap.sent).toHaveLength(1); const states = [a.state, b.state].sort(); expect(states).toEqual(['failed', 'sent']); const loser = a.state === 'failed' ? a : b; expect(loser.error).toContain('本次未重复发送'); expect(loser.outboundId).toBe('out-9'); // 抢输的那一路一个字都没往外发,也没新开一行 expect(outbound.rows.size).toBe(1); expect(outbound.rows.get('out-9')?.state).toBe('sent'); }); it('**超时 sending 的复用同样要抢**:抢不到就走「未重复发送」那一支', async () => { const { service, accounts, outbound, imap, clock } = harness(); await accounts.save(account()); await outbound.insert(outboundRow({ createdAt: new Date(clock.now().getTime() - 6 * 60_000) })); const [a, b] = await Promise.all([ service.send({ ...sendInput, idempotencyKey: 'k-1' }), service.send({ ...sendInput, idempotencyKey: 'k-1' }), ]); expect(imap.sent).toHaveLength(1); expect([a.state, b.state].sort()).toEqual(['failed', 'sent']); }); it('失败重发成功之后,那一行不留上一次的错误话', async () => { const { service, accounts, outbound, imap } = harness(); await accounts.save(account()); imap.failSendWith(new Error('暂时不行')); const first = await service.send({ ...sendInput, idempotencyKey: 'k-1' }); expect(first.state).toBe('failed'); expect(outbound.rows.get(first.outboundId)?.error).toContain('暂时不行'); imap.failSendWith(undefined); const second = await service.send({ ...sendInput, idempotencyKey: 'k-1' }); expect(second.state).toBe('sent'); expect(outbound.rows.get(first.outboundId)?.error).toBeNull(); expect((await service.listOutbound({}))[0]?.error).toBeNull(); // 第三次同键走 resultOf(prior):既说成功又带错误的结果会让调用方按 error 分支判反 const third = await service.send({ ...sendInput, idempotencyKey: 'k-1' }); expect(third.state).toBe('sent'); expect(third.error).toBeUndefined(); expect(imap.sent).toHaveLength(1); }); }); describe('卡死的 dispatching', () => { const parsed: ParsedMail = { messageId: '', references: [], from: { address: 'buyer@acme.com', name: '买家' }, to: [{ address: 'sales@we.com', name: '销售' }], cc: [], subject: '询价', text: '请报价', attachments: [], }; it('自动路径不进(墙),人按的重新投递能接管(路)', async () => { const { service, accounts, inbound, clock } = harness(); await accounts.save(account()); await inbound.insert( buildInboundRecord({ id: 'in-1', accountId: 'acc-1', address: 'sales@we.com', providerRef: 'uid-1', parsed, eml: new Uint8Array(), receivedAt: clock.now(), attachments: [], now: clock.now(), }), ); let calls = 0; service.subscribe('channel', async () => { calls += 1; }); // 进程崩在写 dispatching 与写结果之间:这条留痕永远停在 dispatching const stuck = { mailId: 'in-1', subscriber: 'channel', state: 'dispatching' as const, error: null, finishedAt: null }; await inbound.setDelivery({ ...stuck, startedAt: new Date(clock.now().getTime() - 11 * 60_000) }); const reclaimed = await service.redeliver('in-1'); expect(reclaimed[0]?.state).toBe('done'); expect(calls).toBe(1); // 新鲜的那条仍是墙:人按也不抢,免得两条线同时改一件事 await inbound.setDelivery({ ...stuck, startedAt: clock.now() }); const skipped = await service.redeliver('in-1'); expect(skipped[0]?.state).toBe('dispatching'); expect(calls).toBe(1); }); });