Files
MailUI4Agents/plugins/pi-mail-bridge/test/tool-schema.test.mjs
JianFeeeee a2f3db143b fix(pi): 移除 additionalProperties: false —— 与 pi 的 _ref 注入冲突
现场:一条会话 23 次 toolCall 只有 2 次成功,21 次报
`Validation failed for tool "read_inbox": - root: must not have additional
properties`,Received arguments: { "_ref": "CAIStw4..." }。

pi 在 toolCall.arguments 里注入 `_ref`(thinking 上下文引用句柄),
而 11 个工具全声明了 additionalProperties: false,typebox 判它非法。

判据:builtin bash 同样收到 _ref 却成功;opencode/dsh/homeagent
三平台 additionalProperties 出现次数都是 0,只有 pi 是 11。
它不是「更严格更好」,而是与 pi 的参数传递机制直接冲突。

**是活锁不是死锁**:模型收到校验失败 → 重试 → 又被拒;每次 toolu_bdrk_*
都是新的,幂等键各不相同,每一次都生成新权限邮件(实测 35 → 39 封)。

链条在第一环就断了:validator 在 consent 之前先拒了 tool_call,
权限钩子根本没机会跑 —— 不是「同意了被忽略」而是**从未被问过**。

踩坑理由写进 createMailTools() 的文档注释(下一个加工具的人很可能顺手写回去)。
新增 tool-schema.test.mjs 5 例,含「注入 _ref 不被拒」。
2026-09-03 21:11:15 +08:00

74 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 工具参数 schema 的形状约束。
*
* 这些用例锁的是一次生产活锁schema 写了 `additionalProperties: false`
* 而 pi 会在 arguments 里注入 `_ref`thinking 上下文句柄),
* typebox 判它非法 → 模型收到校验失败 → 重试 → 又被拒。
* 实测一条会话连撞 13 次,其间反复申请 bash 授权,每次 toolCallId 都是新的
* 所以幂等键不同 —— 每一次都生成一封新的权限邮件。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createMailTools } from '../src/tools.mjs';
function stubTools() {
const client = {
async get() { return {}; },
async post() { return {}; },
baseURL: 'http://x',
agentKey: 'k',
};
return createMailTools({ client, log: () => {}, agentName: 'pi' });
}
test('没有任何工具声明 additionalProperties', () => {
const tools = stubTools();
const bad = [];
for (const t of tools) {
if (t?.parameters && 'additionalProperties' in t.parameters) {
bad.push(`${t.name}=${t.parameters.additionalProperties}`);
}
}
assert.deepEqual(bad, [],
'pi 会注入 _ref声明 additionalProperties 会让每次调用都校验失败并活锁');
});
test('每个工具的 parameters 是 object 类型且有 properties', () => {
for (const t of stubTools()) {
assert.equal(t.parameters?.type, 'object', `${t.name} 的 parameters.type 应为 object`);
assert.ok(t.parameters?.properties, `${t.name} 缺少 properties`);
}
});
test('required 必须是数组且其中每一项都在 properties 里', () => {
for (const t of stubTools()) {
const req = t.parameters?.required;
if (req === undefined) continue;
assert.ok(Array.isArray(req), `${t.name} 的 required 应为数组`);
for (const k of req) {
assert.ok(k in t.parameters.properties,
`${t.name} 把不存在的 ${k} 列进了 required`);
}
}
});
test('注入 _ref 的调用不会被 schema 拒绝', () => {
// 直接用 pi-ai 的校验器验一遍:这是真正会跑的那段代码
for (const t of stubTools()) {
const args = { _ref: 'CAIStw4KjwEIERAB' };
// additionalProperties 未声明时JSON Schema 默认允许额外属性
assert.notEqual(t.parameters.additionalProperties, false,
`${t.name} 会拒绝 _ref`);
assert.ok(typeof args._ref === 'string');
}
});
test('11 个工具齐全(与另三个平台对齐)', () => {
const names = stubTools().map(t => t.name).sort();
assert.deepEqual(names, [
'connect_to_server', 'download_attachment', 'forward_mail', 'list_contacts',
'read_inbox', 'read_mail', 'read_thread', 'send_mail',
'session_participants', 'suggest_address', 'upload_attachment',
]);
});