/** * 工具参数 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', ]); });