fix(四桥)★★: parent_from 方向判据在 dsh/opencode 上是死代码 —— 接线补齐
## 起因:修部署漂移时撞出来的
b0c8719(2026-09-30)给 `inboundHeadline` 加了 `parentFrom`/`selfName`,
用来分辨「回的是你那封」与「多方线索里的他人续谈」——单向续信链(8 封全是
对方发来的)同样满足 `in_reply_to`,不判方向就会被逐封宣称「回的是你那封」,
模型把它当新任务处理。**但只有 pi 桥接了**:
pi parentFrom: data?.parent_from ✓(turn.mjs:148)
dsh 从不传 ✗ 3 处
opencode 从不传 ✗ 1 处
判据函数是三个桥**同一份** relay-policy.js 的拷贝,看起来测过了 ——
但「函数支持」≠「调用方传了」。三个桥各自都有 relay-policy 单元测试且全绿,
因为它们只测那个**纯函数**,没有一个测「桥真的把参数传进去了」。
dsh 还多一层:它有一份**手写**的 `lib/relay-policy.d.ts`,缺这两个字段
⇒ tsc 报 TS2353 ⇒ 调用方即使想传也过不了类型检查。那行判据是被类型系统
**主动拦住**的死代码。只补 .js 不补 .d.ts 等于没补。
## 改动
dsh 3 处 + opencode 1 处调用点补 `parentFrom`/`selfName`,对齐 pi 的样板;
dsh 的 `.d.ts` 补声明(注释写明「光有声明不够,调用点也得传」)。
## 新增判据 deploy/check-parent-from.mjs(7 格)
不只是查「有没有传」,还查**三处曾经不一致的接缝**:
① 每个 inboundHeadline 调用点都传了 parentFrom(次数写死,新增调用点
忘了传要能被发现,而不是被 `>0` 静默接受)
② 传了 parentFrom 就必须传 selfName —— 只传前者时
`parentFrom === selfName` 永不成立,判据形同虚设
③ 三份 relay-policy.js 都真的有 `parentFrom === selfName`
④ dsh 的 .d.ts 声明与实现一致(否则 tsc 拦住,判据等于没有)
**变异验证**:撤一处传参 → 红 2;只撤 selfName → 红 1;撤 .d.ts 声明 → 红 1。
## ★ 判据自身也踩了两次坑(都已修)
1. 初版 `.d.ts` 那格用 `\bparentFrom\b` 全文件搜 ⇒ 撤掉字段声明后,
**注释文字里还留着这个词** ⇒ 照样匹配 ⇒ 变异不红(假绿)。
改为只认字段声明 `^\s*parentFrom\??\s*:\s*string\s*;`。
2. 初版 `.mjs` 写了 `require` ⇒ ReferenceError(ESM)。已改 import。
## 顺带修掉 6 个陈旧判据(都在 HEAD 上、此前静默红着)
`turn.test.mjs` 的「回信到达时明说不是新任务」:只造 `in_reply_to` 却断言
`/m-0/`,而 `b0c8719` 新增的「我的回复」分支文案里压根不含父邮件 ID ——
判据没跟上那次改动。已改成覆盖三种情形(parent_from 是我 / 是别人 / 缺席)。
★ 我第一版改完是**假绿**:只断言 `/续谈/` 时,把 `mine` 分支改坏也会走
「续谈」分支而照样通过。加反向断言(mine 不含「续谈」、theirs 不含
「不是新任务」)后才抓住。变异 A/B 各 35 pass / 1 fail。
`cross-bridge-permission-routing.test.mjs`:zcode 的路径与目录还指向
`d09ef39` 已删的 `hooks/`、`mcp/` ⇒ ENOENT。zcode 确实**没有** 409 分支了
(移除执行类工具所致,非缺陷)⇒ `permanent: null` 表示不适用,并加反向
守卫:若 `src/` 里出现 `isPermanentFailure` 就红(防止前提变了却继续空转)。
`isPermanentFailure` 是 `lib/relay-key.js` 的通用网关辅助,与权限放行无关 ——
踩过一次:把 `lib` 加进扫描范围后对着共享辅助函数误报。
全量:pi 533 / opencode 364 / dsh 433,零红(此前 4 红)。
## 部署与端到端
pi / opencode / dsh 三个宿主均已部署;dsh 已真实收发验证:
[dsh-mail-bridge] 本轮不自动转发:来信方 pi 是 Agent…
[dsh-mail-bridge] new_mail -> 新会话 mail-8e8bf319…
This commit is contained in:
119
deploy/check-parent-from.mjs
Normal file
119
deploy/check-parent-from.mjs
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
跨桥一致性判据:`parent_from` 方向判据必须在**每个**桥真正生效(2026-10-02)。
|
||||
|
||||
# 这个缺陷是怎么活下来的
|
||||
|
||||
b0c8719(2026-09-30)给 `inboundHeadline` 加了 `parentFrom`/`selfName` 两个参数,
|
||||
用来分辨「回的是你那封」和「多方线索里的他人续谈」——单向续信链(8 封全是
|
||||
对方发来的)同样满足 `in_reply_to`,不判方向就会被逐封宣称「回的是你那封」,
|
||||
模型于是把它当新任务处理。pi 桥接了,**另外两个桥没接**:
|
||||
|
||||
pi parentFrom: data?.parent_from ✓
|
||||
dsh (从不传) ✗ 3 处调用点
|
||||
opencode (从不传) ✗ 1 处调用点
|
||||
|
||||
判据函数在 `lib/relay-policy.js` 里对三个桥是**同一份拷贝**,看起来测过了;
|
||||
但「函数支持」≠「调用方传了」。dsh 还多一层:它有一份**手写**的
|
||||
`lib/relay-policy.d.ts`,缺这两个字段 ⇒ tsc 报 TS2353 ⇒ 调用方即使想传
|
||||
也传不过类型检查 —— 于是那行判据在 dsh 上是被类型系统**主动拦住**的死代码。
|
||||
|
||||
# 为什么这值得单独一格判据
|
||||
|
||||
三个桥各自都有 relay-policy 的单元测试,且全绿 —— 因为它们测的是
|
||||
`inboundHeadline` 这个**纯函数**,没有一个测「桥真的把参数传进去了」。
|
||||
纯函数测试与接线测试是两件事,缺后者就等于没测。
|
||||
|
||||
用法:在仓库根跑,见 go 之外的 `node deploy/check-parent-from.mjs`。
|
||||
*/
|
||||
|
||||
// 这是 .mjs —— ESM。(初版写了 require,跑起来直接 ReferenceError;
|
||||
// 与其它 deploy/check-*.mjs 保持一致用 import。)
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/** 每个桥:入口文件 + 期望出现 parentFrom 传参的次数。
|
||||
* 次数写死而不是「>0」:新增调用点时忘了传参,要能被发现而不是被静默接受。 */
|
||||
const BRIDGES = [
|
||||
{ name: 'pi', files: ['plugins/pi-mail-bridge/src/turn.mjs'], expect: 1 },
|
||||
{ name: 'dsh', files: ['plugins/dsh-mail-bridge/src/index.ts'], expect: 3 },
|
||||
{ name: 'opencode', files: ['plugins/opencode-mail-bridge/index.js'], expect: 1 },
|
||||
];
|
||||
|
||||
const PASS = (m) => console.log(` 通过 ${m}`);
|
||||
const FAIL = (m) => { console.log(` 失败 ${m}`); failures.push(m); };
|
||||
const failures = [];
|
||||
|
||||
console.log('判据:三个桥都必须把 parent_from 传给 inboundHeadline');
|
||||
|
||||
for (const b of BRIDGES) {
|
||||
for (const rel of b.files) {
|
||||
const full = path.join(REPO, rel);
|
||||
if (!fs.existsSync(full)) { FAIL(`${b.name}: 文件不存在 ${rel}`); continue; }
|
||||
const src = fs.readFileSync(full, 'utf8');
|
||||
|
||||
// ① 每个 inboundHeadline 调用点都必须带 parentFrom
|
||||
const calls = [...src.matchAll(/inboundHeadline\(\s*\{([\s\S]*?)\n\s*\}\)/g)];
|
||||
if (calls.length === 0) { FAIL(`${b.name}: 没找到 inboundHeadline 调用点(判据本身失效?)`); continue; }
|
||||
const missing = calls.filter((c) => !/parentFrom\s*:/.test(c[1]));
|
||||
if (missing.length) {
|
||||
FAIL(`${b.name}: ${missing.length}/${calls.length} 个 inboundHeadline 调用点没传 parentFrom ` +
|
||||
`⇒ 单向续信链会被误判成回信`);
|
||||
}
|
||||
const have = calls.filter((c) => /parentFrom\s*:/.test(c[1])).length;
|
||||
if (have !== b.expect) {
|
||||
FAIL(`${b.name}: 实传 ${have} 处,期望 ${b.expect} 处(新增/漏改调用点)`);
|
||||
}
|
||||
if (!missing.length && have === b.expect) {
|
||||
PASS(`${b.name}: ${calls.length} 个调用点全部传了 parent_from(${rel})`);
|
||||
}
|
||||
|
||||
// ② 传了 parentFrom 就必须传 selfName,否则判据永远不成立
|
||||
const noSelf = calls.filter((c) => /parentFrom\s*:/.test(c[1]) && !/selfName\s*:/.test(c[1]));
|
||||
if (noSelf.length) {
|
||||
FAIL(`${b.name}: ${noSelf.length} 处传了 parentFrom 却没传 selfName ` +
|
||||
`⇒ parentFrom === selfName 永不成立,判据形同虚设`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ③ 判据函数本身必须真的认这两个参数(三份拷贝各一份)
|
||||
for (const lib of [
|
||||
'plugins/pi-mail-bridge/lib/relay-policy.js',
|
||||
'plugins/dsh-mail-bridge/lib/relay-policy.js',
|
||||
'plugins/opencode-mail-bridge/lib/relay-policy.js',
|
||||
]) {
|
||||
const full = path.join(REPO, lib);
|
||||
if (!fs.existsSync(full)) { FAIL(`判据函数缺失 ${lib}`); continue; }
|
||||
const src = fs.readFileSync(full, 'utf8');
|
||||
if (!/parentFrom\s*===\s*selfName/.test(src)) {
|
||||
FAIL(`${lib}: 方向判据 parentFrom === selfName 不在(调用方传了也没用)`);
|
||||
} else {
|
||||
PASS(`${lib}: 方向判据在`);
|
||||
}
|
||||
}
|
||||
|
||||
// ④ dsh 的手写 .d.ts 必须声明这两个字段,否则 tsc 会拦住调用方
|
||||
{
|
||||
const full = path.join(REPO, 'plugins/dsh-mail-bridge/lib/relay-policy.d.ts');
|
||||
if (fs.existsSync(full)) {
|
||||
const src = fs.readFileSync(full, 'utf8');
|
||||
// ★ 只认**字段声明**,不能只搜字段名。
|
||||
// 初版用 `\bparentFrom\b` 全文件搜 —— 撤掉 `parentFrom?: string;` 后
|
||||
// 注释文字里还留着这个词 ⇒ 照样匹配 ⇒ 变异 ③ 不红(假绿,已实测)。
|
||||
// 判据本身就是这次事故的一部分,所以这里必须真的能判。
|
||||
const missing = ['parentFrom', 'selfName'].filter((f) =>
|
||||
!new RegExp(`^\\s*${f}\\??\\s*:\\s*string\\s*;`, 'm').test(src));
|
||||
if (missing.length) {
|
||||
FAIL(`dsh relay-policy.d.ts 缺声明 ${missing.join(', ')} ` +
|
||||
`⇒ TS2353 会让调用方传不了参(这正是它一度是死代码的原因)`);
|
||||
} else {
|
||||
PASS('dsh relay-policy.d.ts 声明与实现一致');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(failures.length ? `\n✗ ${failures.length} 处不通过` : '\n✓ 全部通过');
|
||||
process.exit(failures.length ? 1 : 0);
|
||||
14
plugins/dsh-mail-bridge/lib/relay-policy.d.ts
vendored
14
plugins/dsh-mail-bridge/lib/relay-policy.d.ts
vendored
@ -16,6 +16,20 @@ export function replyInstruction(ctx: ReplyInstructionCtx): string[];
|
||||
|
||||
export interface InboundHeadlineCtx {
|
||||
inReplyTo?: string;
|
||||
/**
|
||||
* parentFrom 是**父邮件的发件人**(服务端 parent_from)。
|
||||
*
|
||||
* ★ 2026-10-02 补:`.js` 早就认这两个字段(b0c8719 加的),但这份手写声明
|
||||
* 没跟上 ⇒ TypeScript 报 TS2353,调用方**不敢传**,于是那行方向判据
|
||||
* 在本桥上一直是死代码:单向续信链同样满足 in_reply_to,被逐封宣称
|
||||
* 「回的是你那封」,模型把它当新任务处理。pi 桥是唯一接了的。
|
||||
*
|
||||
* 光有声明不够 —— 调用点也得传,见 src/index.ts 的三处 inboundHeadline。
|
||||
* 两者缺一,判据都等于没有。
|
||||
*/
|
||||
parentFrom?: string;
|
||||
/** selfName 是本方名字(收件方自己);`parentFrom === selfName` 才是「我的回复到了」。 */
|
||||
selfName?: string;
|
||||
fromHuman: boolean;
|
||||
catchup?: boolean;
|
||||
reused?: boolean;
|
||||
|
||||
@ -17,8 +17,8 @@
|
||||
"test": "node --test 'test/*.test.mjs'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.0"
|
||||
"@deepseek-ai/cordis": "~4.0.4",
|
||||
"@deepseek-ai/dsh-tools": "0.2.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@ -1076,7 +1076,13 @@ export function apply(ctx: any, config: PluginConfig): void {
|
||||
}
|
||||
const fromHuman = data.from_human === true;
|
||||
return [
|
||||
inboundHeadline({ inReplyTo: data.in_reply_to, fromHuman, reused: true }),
|
||||
inboundHeadline({
|
||||
inReplyTo: data.in_reply_to,
|
||||
parentFrom: data.parent_from,
|
||||
selfName: AGENT_NAME,
|
||||
fromHuman,
|
||||
reused: true,
|
||||
}),
|
||||
``,
|
||||
`发件人:${data.from_name || 'unknown'}`,
|
||||
`主题:${data.subject || '(无主题)'}`,
|
||||
@ -1190,8 +1196,16 @@ export function apply(ctx: any, config: PluginConfig): void {
|
||||
const promptText = kind === 'permission'
|
||||
? permissionPrompt(data)
|
||||
: [
|
||||
// ★ 方向判据(2026-10-02 补):b0c8719 给 inboundHeadline 加了
|
||||
// parentFrom/selfName 用来分辨「回的是你那封」与「他人续谈」,
|
||||
// 但**本桥从不传这两个参数** ⇒ 那行判据在这里是死代码。
|
||||
// 实测:单向续信链(8 封全是对方发来的)同样满足 in_reply_to,
|
||||
// 于是被逐封宣称「回的是你那封」,模型把它当新任务处理。
|
||||
// pi 桥是唯一接了的(turn.mjs:148),此处按它的样板对齐。
|
||||
inboundHeadline({
|
||||
inReplyTo: data.in_reply_to,
|
||||
parentFrom: data.parent_from,
|
||||
selfName: AGENT_NAME,
|
||||
fromHuman: data.from_human === true,
|
||||
reused: true,
|
||||
}),
|
||||
@ -1241,6 +1255,8 @@ export function apply(ctx: any, config: PluginConfig): void {
|
||||
: [
|
||||
inboundHeadline({
|
||||
inReplyTo: data.in_reply_to,
|
||||
parentFrom: data.parent_from,
|
||||
selfName: AGENT_NAME,
|
||||
fromHuman: data.from_human === true,
|
||||
catchup: data.catchup,
|
||||
reused: false,
|
||||
|
||||
@ -1142,6 +1142,12 @@ async function deliverMail(client, directory, data, kind) {
|
||||
: [
|
||||
inboundHeadline({
|
||||
inReplyTo: data.in_reply_to,
|
||||
// ★ 方向判据(2026-10-02 补):b0c8719 给 inboundHeadline 加了
|
||||
// parentFrom/selfName,本桥却**从不传** ⇒ 那行判据是死代码。
|
||||
// 单向续信链同样满足 in_reply_to,会被逐封宣称「回的是你那封」。
|
||||
// pi 桥是唯一接了的(turn.mjs:148),此处按它的样板对齐。
|
||||
parentFrom: data.parent_from,
|
||||
selfName: AGENT_NAME,
|
||||
fromHuman,
|
||||
catchup: data.catchup,
|
||||
reused,
|
||||
|
||||
@ -42,7 +42,10 @@ const PLUGINS = join(HERE, '..', '..');
|
||||
const IMPL = {
|
||||
'dsh-mail-bridge': ['src'],
|
||||
'pi-mail-bridge': ['src'],
|
||||
'zcode-mail-bridge': ['src', 'mcp', 'hooks'],
|
||||
// ★ 2026-10-02:d09ef39 删掉了 zcode 的 mcp/ 与 hooks/(MCP 已内置网关,
|
||||
// 执行门禁随之退场)。留着这两个目录名 ⇒ stat 抛 ENOENT ⇒ 整档红,
|
||||
// 而且是「文件不存在」这种红,会盖住真正要看的判据失败。
|
||||
'zcode-mail-bridge': ['src', 'lib'],
|
||||
'opencode-mail-bridge': ['index.js'],
|
||||
};
|
||||
|
||||
@ -87,10 +90,16 @@ const BRIDGES = [
|
||||
},
|
||||
{
|
||||
plugin: 'zcode-mail-bridge',
|
||||
allow: /permission_mode \|\| ''\) === 'full'/,
|
||||
mustWriteBack: null,
|
||||
permanent: 'if (isPermanentFailure(e))',
|
||||
note: 'hooks/permission.mjs 的 409 分支(该桥没有 sandbox/approval 旋钮)',
|
||||
allow: /normalizeMode\(data\?\.permission_mode\)/,
|
||||
allow: /normalizeMode\(data\?\.permission_mode\)/,
|
||||
// ★ zcode 整条 409 分支都不存在(d09ef39 移除执行类工具后不再向人问权限),
|
||||
// 所以「放行必须排在永久失败之前」对它**不适用**:没有 permanent 分支,
|
||||
// 也就没有顺序可排。置 null = 本桥不参与该子判据,不是「跳过了检查」。
|
||||
permanent: null,
|
||||
// ★ zcode 已无 409 分支:d09ef39 移除执行类工具后,src/index.mjs:232 的
|
||||
// modeReachesPermissionHook 只是提醒(本平台没有执行面),不存在「按
|
||||
// workspace 档问人」这条路。此处只钉住它仍读 permission_mode 并归一档位。
|
||||
note: 'src/index.mjs:219 的 normalizeMode(data?.permission_mode)',
|
||||
},
|
||||
{
|
||||
plugin: 'opencode-mail-bridge',
|
||||
@ -127,6 +136,17 @@ test('★ 四桥都必须认出「409 带 full → 放行」,且排在永久
|
||||
`${b.plugin} 有会话档位旋钮,所以 409 里的权威档位还必须**写回会话**:` +
|
||||
'只放行修的是"问不问",沙箱(能不能)还是窄的。');
|
||||
}
|
||||
if (b.permanent === null) {
|
||||
// 本桥没有 409 分支 ⇒ 顺序问题不适用。但仍要确认它**真的**没有,
|
||||
// 而不是路径/正则过时了:出现任一个就说明前提变了,该改判据。
|
||||
// 只扫它自己的 src/:isPermanentFailure 是 lib/relay-key.js 里的**通用**
|
||||
// 网关辅助(所有桥都有),与「409 权限放行分支」无关。全量扫会误判 ——
|
||||
// 踩过一次:把 lib 加进 IMPL 后,这格对着共享辅助函数报红。
|
||||
assert.doesNotMatch(readImpl(b.plugin).split('lib/')[0], /isPermanentFailure/,
|
||||
`${b.plugin} 声明「无 409 分支」但 src/ 里出现了 isPermanentFailure —— ` +
|
||||
'前提变了,别让这格继续空转');
|
||||
continue;
|
||||
}
|
||||
const allowAt = src.search(b.allow);
|
||||
const permAt = src.indexOf(b.permanent);
|
||||
assert.ok(permAt >= 0,
|
||||
@ -144,6 +164,10 @@ test('★ 判据自检:把放行那一支挪到永久失败分支之后,顺
|
||||
return a >= 0 && p >= 0 && a < p;
|
||||
};
|
||||
for (const b of BRIDGES) {
|
||||
// ★ 2026-10-02:permanent === null 的桥(zcode)没有 409 分支,顺序无从谈起。
|
||||
// 原来这里直接 indexOf(null) ⇒ 把 null 当成字符串找不到 ⇒ 前置恒红 ——
|
||||
// 一条判据自己制造的红。跳过前仍要过一遍上面那格,确认「无分支」是真的。
|
||||
if (b.permanent === null) continue;
|
||||
const src = codeOnly(readImpl(b.plugin));
|
||||
assert.ok(orderHolds(src, b), `前置:${b.plugin} 当前顺序正确,才谈得上"挪动"`);
|
||||
const a = src.search(b.allow);
|
||||
@ -173,7 +197,11 @@ test('★ 配对:消费者读的档位键名,生产者必须产出(四平
|
||||
['pi', 'pi-mail-bridge/src/worker.mjs'],
|
||||
['dsh', 'dsh-mail-bridge/src/index.ts'],
|
||||
['opencode', 'opencode-mail-bridge/index.js'],
|
||||
['zcode', 'zcode-mail-bridge/hooks/permission.mjs'],
|
||||
// ★ 2026-10-02 修路径(不是删判据):d09ef39 删掉 zcode 的本地 MCP 与整套
|
||||
// 执行门禁(含 hooks/permission.mjs),zcode 改为在 src/index.mjs:219
|
||||
// 读档位。能力没丢,只是搬了家;原路径不存在 ⇒ 4 个 subtest 全是
|
||||
// ENOENT,白烧 4 个红且会掩盖真实失败。判据仍要求它登记。
|
||||
['zcode', 'zcode-mail-bridge/src/index.mjs'],
|
||||
];
|
||||
for (const [who, rel] of CONSUMERS) {
|
||||
const text = codeOnly(readFileSync(join(PLUGINS, rel), 'utf8'));
|
||||
|
||||
@ -215,17 +215,50 @@ test('不变量:from_human 缺失时按 Agent 处理(不能承诺做不到
|
||||
'宁可让它多调一次 send_mail,也不能让发件方白等一个不会发生的自动回信');
|
||||
});
|
||||
|
||||
test('不变量:回信到达时明说「不是新任务」', () => {
|
||||
// 把回复当新任务处理正是互相客套的起点。
|
||||
const p = buildMailPrompt({
|
||||
agentName: 'pi',
|
||||
data: { ...mailData, from_human: false, in_reply_to: 'm-0' },
|
||||
kind: 'mail',
|
||||
reused: true,
|
||||
});
|
||||
assert.match(p, /回复/);
|
||||
assert.match(p, /不是新任务/);
|
||||
assert.match(p, /m-0/, '要说出回的是哪封');
|
||||
test('不变量:回信到达时明说「不是新任务」', () => {
|
||||
// 把回复当新任务处理正是互相客套的起点。
|
||||
//
|
||||
// ★ 2026-10-02 修判据(不是修代码):这格原本只造 in_reply_to,于是走进
|
||||
// `parentFrom === selfName` 的 **mine** 分支,而那行文案压根不含父邮件
|
||||
// ID ⇒ 断言 /m-0/ 必然失败。它是 b0c8719 加方向判据时漏改的陈旧判据
|
||||
// (实测:HEAD 上就是红的,且 pi 当时一个字节都没动 ⇒ 不是回归)。
|
||||
//
|
||||
// 三种情形都要覆盖,因为它们走的是**不同分支、文案不同**:
|
||||
// · parent_from === 我自己 ⇒ 我的回复到了(只说不是新任务)
|
||||
// · parent_from === 别人 ⇒ 多方线索里的续谈(要说回的是谁的)
|
||||
// · 无 parent_from ⇒ 服务端未升级,退回旧行为
|
||||
const mine = buildMailPrompt({
|
||||
agentName: 'pi',
|
||||
data: { ...mailData, from_human: false, in_reply_to: 'm-0', parent_from: 'pi' },
|
||||
kind: 'mail',
|
||||
reused: true,
|
||||
});
|
||||
assert.match(mine, /回复/);
|
||||
assert.match(mine, /不是新任务/);
|
||||
// ★ 反向对照:这格原来是假绿 —— 只断言 /续谈/ 时,
|
||||
// 把 mine 分支改坏(mine=false)也会走「续谈」分支而照样通过。
|
||||
// 所以必须反向断言 mine **不是**续谈文案,否则这格没有区分力。
|
||||
assert.doesNotMatch(mine, /续谈/, '方向是我的就不该说成续谈');
|
||||
|
||||
const theirs = buildMailPrompt({
|
||||
agentName: 'pi',
|
||||
data: { ...mailData, from_human: false, in_reply_to: 'm-0', parent_from: 'opencode' },
|
||||
kind: 'mail',
|
||||
reused: true,
|
||||
});
|
||||
assert.match(theirs, /续谈/);
|
||||
assert.match(theirs, /opencode/, '★ 方向不对时必须说清回的是谁的信');
|
||||
// ★ 同理:他人续谈**不是**我的回复,不该说成「不是新任务」
|
||||
// (那会把别人的话当成我上一封信的回音,正是要避免的误判)。
|
||||
assert.doesNotMatch(theirs, /不是新任务/, '他人续谈不是我的回信');
|
||||
|
||||
const legacy = buildMailPrompt({
|
||||
agentName: 'pi',
|
||||
data: { ...mailData, from_human: false, in_reply_to: 'm-0' },
|
||||
kind: 'mail',
|
||||
reused: true,
|
||||
});
|
||||
assert.match(legacy, /不是新任务/, '服务端未升级时仍要明说不是新任务');
|
||||
});
|
||||
|
||||
test('不变量:提示词带 mail_id 与 read_inbox 指引', () => {
|
||||
|
||||
Reference in New Issue
Block a user