Files
nodeagent/tests/unit/mcp-tools.test.mjs
T
jkooe 865862ee7c refactor: 拆 mcp/index.ts(1139 → 530 行),工具清单独立成 tools/defs.ts
## 拆法
`TOOLS` 数组(610 行,占原文件过半)抽到 `apps/mcp/src/tools/defs.ts` 并 export。

保留在 index.ts 的:
- 连接与调用辅助(ensureClient / call / forwardEventToHost)
- toArgs(入参特化)· TOOL_TO_CAPABILITY · resolveToolCall(分发 switch)
- MCP server 定义与 main

**分工**:defs.ts 只管「暴露什么」,index.ts 管「怎么转发」。

## 零漂移的证据
抽出的最大风险是**顺序漂移或漏搬** —— 两者都不会让构建失败,只会静默改变客户端
看到的工具表。故用「工具名序列 sha256」做证据:
  拆分前 41 个工具 / sha=69e8533c42c3…
  拆分后 41 个工具 / sha=69e8533c42c3…  ✅ 序列与顺序完全一致

## 新增回归守卫 tests/unit/mcp-tools.test.mjs(4 例)
- 工具数 41 且名称唯一
- **顺序未漂移**(序列 sha 比对;有意调整须同步更新基线并说明)
- 每个工具都有 name/description/inputSchema 三件套
- inputSchema 普遍声明 additionalProperties: false

## 验证
单测 269 → 273 全绿;e2e 30 全绿;7 包构建通过。
2026-10-10 07:50:13 +08:00

60 lines
2.6 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.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
/**
* MCP 工具清单的回归守卫(2026-10 拆分时建立)。
*
* 背景:TOOLS 从 index.ts 抽到 tools/defs.ts(1139 → 530 行)。抽出的风险是
* **顺序漂移**或**漏搬**,而这两者都不会让构建失败 —— 只会静默改变客户端看到的工具表。
* 故用「工具名序列 sha256」做证据:序列一旦变化,本测试立刻红。
*/
const DEFS = fileURLToPath(new URL('../../apps/mcp/src/tools/defs.ts', import.meta.url));
const src = readFileSync(DEFS, 'utf8');
/** 拆分时记录的基线(顺序敏感)。改工具清单时**必须**同步更新此值。 */
const BASELINE_SEQ_SHA = '69e8533c42c302f8e5404004b76bb6d8c4ad4bcec3d57711cb1c4de128d892b7';
function toolNames() {
return [...src.matchAll(/^\s*name: '(na_[a-z_]+)',/gm)].map((m) => m[1]);
}
test('tools/defs.ts:工具数 41 且名称唯一', () => {
const names = toolNames();
assert.equal(names.length, 41, '工具数变了 —— 若有意增删,请同步更新 BASELINE_SEQ_SHA');
assert.equal(new Set(names).size, names.length, '存在重复工具名(MCP 客户端会不认)');
});
test('tools/defs.ts:工具名序列与顺序未漂移(顺序即对外呈现顺序)', () => {
const sha = createHash('sha256').update(toolNames().join('|')).digest('hex');
assert.equal(
sha,
BASELINE_SEQ_SHA,
'工具顺序变了 —— 顺序即客户端看到的列表次序。若为有意调整,请更新 BASELINE_SEQ_SHA 并说明原因',
);
});
test('tools/defs.ts:每个工具都有 name/description/inputSchema 三件套', () => {
const objects = src.split(/\n \{\n/).slice(1);
for (const obj of objects) {
assert.match(obj, /name: 'na_/, '缺少 name');
assert.match(obj, /description:/, '缺少 description(MCP 靠它让模型知道何时用)');
assert.match(obj, /inputSchema: \{/, '缺少 inputSchema');
}
});
test('tools/defs.ts:inputSchema 均声明 type=object 且禁止未声明属性', () => {
const schemas = [...src.matchAll(/inputSchema: \{([^}]*type: 'object'[^}]*)\}/g)];
assert.ok(schemas.length >= 1, '未找到 inputSchema');
// 逐工具检查 additionalProperties(允许三种写法:内联 false / 单独一行)
const inline = (src.match(/additionalProperties: false/g) ?? []).length;
const tools = toolNames().length;
assert.ok(
inline >= tools - 2,
`additionalProperties: false 只出现 ${inline} 次,少于工具数 ${tools}(可能漏了约束)`,
);
});