Files
nodeagent/tests/unit/mcp-tools.test.mjs
T
jkooe 5a2c5f9c55 feat: 三端暴露 L2 系统级审计(MCP na_audit_l2 + 控制台 L2 分区 + agent.l2 类型)
决策「L2 同时暴露 MCP+控制台」。此前 system.audit.l2 只存在于
被控端实现里:MCP 无工具(模型看不到)、控制台无入口无类型
(用户点不到)、审计事件写成 agent.start(与进程生命周期混类筛不出)。

MCP(41 → 42 个工具):
- defs.ts 在 na_audit 后新增 na_audit_l2(action: status/enable/
  collect/verify + since/limit,additionalProperties: false)
- index.ts TOOL_TO_CAPABILITY 映射 CapabilityNames.AuditL2,
  分发走 default 直通(参数名与被控端一致,无需改写)
- mcp-tools 守卫同步:41→42、BASELINE_SEQ_SHA 重算、
  新增「na_audit_l2 必须紧随 na_audit」顺序断言

控制台:
- conn.ts WINDOWS_ONLY_CAPS += system.audit.l2(非 Windows 被控端
  上按钮禁用并明示原因,不搞「点了才报错」)
- types/index.ts 新增 AuditL2Result(四 action 属性并集,对齐
  manifest returns_schema)+ AuditType += agent.l2;
  AuditEntry 同步补 args_bytes/args_truncated(A-P0-4 新字段)
- AuditView.vue 新增 L2 卡片:查状态/拉取采集/校验 L2 链/启用策略
  (NPopconfirm 二次确认 + 管理员权限提示);信任边界如实说明
  (采集器与 agent 同权限,防篡改依赖转发出机,不夸大本地副本)

agent.l2 审计类型四端同源:
- protocol audit-types.ts AUDIT_TYPES(真源)+= agent.l2
- agent audit.ts AuditType 联合同步(AssertExact 编译期守卫兜底)
- audit-l2.ts 两处写入点 agent.start → agent.l2(L2 策略操作与
  进程生命周期无关,混类会污染两边统计与筛选)

守卫加强(console-contract 平台门控):
- audit-l2.ts 平台门控由裸 throw 改用统一 assertWindows
  (错误信息格式统一 + 可被守卫扫到)
- 守卫扫描范围 +audit-l2.ts、模式 +assertWindows —— 覆盖面扩大;
  否则把 system.audit.l2 加进 WINDOWS_ONLY_CAPS 反而报红
- mcp-tools 正则盲区修复:原 na_[a-z_]+ 匹配不了带数字的工具名,
  na_audit_l2 会整个漏扫(新增了工具两个守卫却无觉)→ 放至 [a-z0-9_]+

新增 tests/unit/console-l2.test.mjs 5 例:前端字段 ⊆ returns_schema /
契约关键字段 ⊇ 前端渲染 / MCP 工具存在且映射 / agent.l2 四端同源 /
界面纪律(信任边界文案 + enable 二次确认 + 平台门控)
故障注入实测:审计类型退回 agent.start、WINDOWS_ONLY_CAPS 删项 → 3 处红;恢复 → 绿

单测 451/451、e2e 33/33、typecheck 全绿
2026-10-26 00:28:44 +08:00

69 lines
3.2 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 = '68c7715eece68c6ba077b9c21c83a02e013c19b6aec9e57079b81e34ad9dad4d';
/**
* 工具名序列。
*
* ⚠️ 2026-10-25(v2.1.0):正则原为 `na_[a-z_]+`,**匹配不了带数字的工具名**——
* `na_audit_l2` 里的 `2` 直接漏扫(新增了工具但两个数量/序列守卫毫无知觉,
* 等于没守)。已放宽松到 `[a-z0-9_]+`。宁可放宽字符集,不可再漏工具。
*/
function toolNames() {
return [...src.matchAll(/^\s*name: '(na_[a-z0-9_]+)',/gm)].map((m) => m[1]);
}
test('tools/defs.ts:工具数 42 且名称唯一', () => {
const names = toolNames();
assert.equal(names.length, 42, '工具数变了 —— 若有意增删,请同步更新 BASELINE_SEQ_SHA');
assert.equal(new Set(names).size, names.length, '存在重复工具名(MCP 客户端会不认)');
// v2.1.0 新增 na_audit_l2(L2 系统级审计)必须紧随 na_audit —— 语义相邻才便于模型选用
assert.equal(names[names.indexOf('na_audit') + 1], 'na_audit_l2', 'na_audit_l2 应紧跟 na_audit');
});
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}(可能漏了约束)`,
);
});