/** * 自动转发适用范围的判定(lib/relay-policy.js)。 * * 三个函数是同一件事的三个出口,必须一起看: * - autoRelayDecision 插件该不该替模型把结论发出去 * - replyInstruction 提示词里怎么跟模型说这件事 * - inboundHeadline 进来的这封是新活、是回复、还是补投 * * 分开写必然分叉,而分叉的代价是模型被骗:以为插件会替它回信,于是把话说完 * 就停手,那封信却永远不会发出去。所以这里逐条钉住它们的一致性。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { addrName, autoRelayDecision, replyInstruction, inboundHeadline, } from '../lib/relay-policy.js'; // ─── addrName ─── test('addrName 取三维地址的名字段', () => { assert.equal(addrName('pi@/home/program/agentmail.某别名'), 'pi'); assert.equal(addrName('jianf'), 'jianf'); assert.equal(addrName(' dsh@/x '), 'dsh'); assert.equal(addrName(''), ''); assert.equal(addrName(undefined), ''); }); // ─── autoRelayDecision ─── test('人类来信 → 自动转发', () => { const d = autoRelayDecision({ fromHuman: true, replyTo: 'jianf' }); assert.equal(d.relay, true); }); test('Agent 来信 → 不自动转发', () => { const d = autoRelayDecision({ fromHuman: false, replyTo: 'dsh' }); assert.equal(d.relay, false, 'Agent 间通信必须由模型主动 send_mail —— 两边都自动回会无休止互相唤醒'); assert.match(d.reason, /dsh/, '日志要说清是谁'); assert.match(d.reason, /Agent/); }); test('不知道回给谁 → 不转发,且理由与「对方是 Agent」区分得开', () => { const d = autoRelayDecision({ fromHuman: true, replyTo: '' }); assert.equal(d.relay, false); assert.match(d.reason, /不知道回给谁/, '「本轮没有回信」有三种原因,日志里必须能分辨'); }); test('replyTo 带三维地址时也能认出 Agent 名', () => { const d = autoRelayDecision({ fromHuman: false, replyTo: 'opencode@/home/x.别名' }); assert.equal(d.relay, false); assert.match(d.reason, /opencode/); }); test('缺省参数不抛错(畸形事件不该弄死投递)', () => { assert.equal(autoRelayDecision().relay, false); assert.equal(autoRelayDecision({}).relay, false); }); // ─── replyInstruction 与 autoRelayDecision 的一致性 ─── test('人类来信的提示词承诺「插件会替你发」,且这与决策一致', () => { const lines = replyInstruction({ fromHuman: true }); const text = lines.join('\n'); assert.match(text, /回信不用你自己发/); assert.equal(autoRelayDecision({ fromHuman: true, replyTo: 'jianf' }).relay, true, '承诺了就必须真的做'); }); test('Agent 来信的提示词必须明说「插件不会替你回信」', () => { const text = replyInstruction({ fromHuman: false }).join('\n'); assert.match(text, /不会替你回信/); assert.match(text, /send_mail/, '必须给出唯一可行的做法'); assert.doesNotMatch(text, /回信不用你自己发/, '这句话在 Agent → Agent 时是假的 —— 说了它模型就会把话说完然后停手'); }); test('Agent 来信的提示词要劝阻纯客套', () => { const text = replyInstruction({ fromHuman: false }).join('\n'); assert.match(text, /收到|确认/, '要点名那种没有信息量的回复'); assert.match(text, /互相客套|无休止/, '要说清后果,否则模型不知道为什么被劝阻'); }); test('Agent 来信时把回信地址带进提示词(有就带)', () => { const withAddr = replyInstruction({ fromHuman: false, replyAddress: 'dsh@/x.别名' }).join('\n'); assert.match(withAddr, /dsh@\/x\.别名/, '要它自己发信却不给地址,它会拼一个 .new 出来 —— 那会静默开新会话'); const without = replyInstruction({ fromHuman: false }).join('\n'); assert.doesNotMatch(without, /(回信地址:)/, '没有地址时不该留一个空括号'); }); // ─── inboundHeadline ─── test('回复到了 → 明说「这不是新任务」', () => { const h = inboundHeadline({ inReplyTo: 'm-1', fromHuman: false }); assert.match(h, /回复/); assert.match(h, /不是新任务/, '把回复当新任务处理正是互相客套的起点'); }); test('回复的标题优先于续谈/补投标记', () => { const h = inboundHeadline({ inReplyTo: 'm-1', fromHuman: true, reused: true, catchup: true }); assert.match(h, /回复/, 'in_reply_to 是最强信号'); }); test('Agent 来信在标题里就标出来', () => { assert.match(inboundHeadline({ fromHuman: false }), /Agent/); assert.doesNotMatch(inboundHeadline({ fromHuman: true }), /Agent/, '人类来信不该带这个括号 —— 那是噪音'); }); test('补投要说明,否则模型按「刚到的」语气回', () => { const h = inboundHeadline({ fromHuman: true, catchup: true }); assert.match(h, /积压|补投/); }); test('续谈与新会话的措辞不同', () => { assert.match(inboundHeadline({ fromHuman: true, reused: true }), /本会话/); assert.match(inboundHeadline({ fromHuman: true, reused: false }), /你收到/); }); test('缺省参数不抛错', () => { assert.equal(typeof inboundHeadline(), 'string'); assert.equal(typeof inboundHeadline({}), 'string'); });