mail 接线两处收紧:入站线程取根不取最近一跳(否则跑一个来回①级归属就落空);附件没给 uploadRef 当场判 invalid,不静默丢掉

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:20:08 +08:00
co-authored by Claude Fable 5
parent ab30ce56c5
commit b6ec37a231
2 changed files with 29 additions and 6 deletions
+16 -3
View File
@@ -41,9 +41,11 @@ function fakeSend(result: Partial<MailSendResult> = {}): { send: (input: MailSen
}
describe('toInboundMail', () => {
it('线程取 References 末项(没 In-Reply-To);只带存下来的附件;纯 html 剥成正文', () => {
it('线程取 References 首项(即这一串的根);只带存下来的附件;纯 html 剥成正文', () => {
const mail: InboundMail = toInboundMail(detail);
expect(mail.threadId).toBe('<q1@we.com>');
// 不是末项 <q1@we.com>:我方出站登记的恒是首封,①级归属是精确等值比对 ——
// 取最近一跳的话,跑一个来回(我方再回一封)线程就漂到库里没记过的 Message-ID 上,①级落空退③级
expect(mail.threadId).toBe('<root@we.com>');
expect(mail.from).toBe('buyer@acme.com');
expect(mail.to).toEqual(['sales@we.com']);
expect(mail.cc).toEqual(['ops@we.com']);
@@ -87,12 +89,23 @@ describe('mailSendTool', () => {
const base = { connection: { account: 'default' }, operation: undefined, idempotencyKey: 'k-1' };
it('数组 / 逗号串都认作收件人;body → text;幂等键与连接配置的账号带过去;出参保留 queued', async () => {
const mail = fakeSend();
const result = await mailSendTool(mail)({ ...base, connection: { account: 'acc-2' }, input: { to: 'a@x.com; b@x.com', subject: 's', body: '正文', inReplyTo: '<m1@a>', attachments: [{ uploadRef: 'up-1', name: 'q.pdf' }, { name: 'no-ref' }] } });
const result = await mailSendTool(mail)({ ...base, connection: { account: 'acc-2' }, input: { to: 'a@x.com; b@x.com', subject: 's', body: '正文', inReplyTo: '<m1@a>', attachments: [{ uploadRef: 'up-1', name: 'q.pdf' }] } });
expect(result).toEqual({ ok: true, output: { queued: true, outboundId: 'out-1', messageId: '<out-1@we.com>', state: 'sent' } });
expect(mail.calls[0]).toMatchObject({ accountId: 'acc-2', to: ['a@x.com', 'b@x.com'], text: '正文', inReplyTo: '<m1@a>', references: ['<m1@a>'], idempotencyKey: 'k-1', attachments: [{ uploadRef: 'up-1', name: 'q.pdf' }], requestedBy: { kind: 'tool', tool: 'mail.send' } });
await mailSendTool(mail)({ ...base, input: { to: ['a@x.com'], subject: 's', body: 'b' } });
expect(mail.calls[1]?.accountId).toBeUndefined();
});
it('没给 uploadRef 的附件不静默丢掉:整次调用判 invalid,一封都不发', async () => {
// 模块那一层本就是硬的(既没字节也没 ref 就抛 MailSendInvalidError);app 层不许比它松 ——
// 悄悄丢掉附件再回 queued:true,等于让界面撒谎
const mail = fakeSend();
const result = await mailSendTool(mail)({ ...base, input: { to: ['a@x.com'], subject: 's', body: 'b', attachments: [{ uploadRef: 'up-1', name: 'q.pdf' }, { name: 'no-ref' }] } });
expect(result.ok).toBe(false);
expect(result.error?.code).toBe('invalid');
expect(result.error?.retryable).toBe(false);
expect(result.error?.message).toContain('no-ref');
expect(mail.calls).toHaveLength(0);
});
it('dryRun 只校验不发', async () => {
const mail = fakeSend();
expect(await mailSendTool(mail)({ ...base, input: { to: ['x@y.com'], subject: '询价', body: 'b', dryRun: true } })).toEqual({ ok: true, output: { queued: false, dryRun: true } });