Files
metona-ollama-desktop/tests/tool-registry.test.ts
T
thzxx b66945c8a7
CI / verify (push) Successful in 1m2s
v0.17.1: 退出释放显存修正 + 备份完整性 + 核心逻辑测试补课 + 上下文逻辑收敛 + 记忆日志可读性
修复:
- main.ts 退出释放模型显存改用 getSetting(serverUrl),不再硬编码 127.0.0.1:11434(避免非默认地址时释放请求打到错误端口)
- 备份导出/导入并入 localStorage 持久化状态(会话摘要、度量历史、轨迹降级缓存、主题),版本升级到 v2,实现完整备份
- 工具数量改为 getEnabledToolDefinitions().length 动态计算,删除写死"32 个"的硬编码
- 记忆日志区分操作来源:memory:write 透传 reason,标注"新增记忆/替换/删除/清空/TTL 衰减清理/访问统计写回(无新条目)",避免"写了但看不到新记忆"的困惑

可维护性:
- 上下文压力逻辑收敛到统一 calculateContextStats,删除 getContextPressureLevel / getTrendAwareCompressThreshold 的重复实现
- 消除 validateToolArgs 同名碰撞(agent-engine 本地版改名 validateToolArgsQuick)
- 子代理工具集改用 getEnabledToolDefinitions() 基线,跟随全局启用开关与 Plan 模式
- 抽取 html-utils.ts 纯函数模块(实体解码/HTML→文本/HTML→Markdown/拦截页检测/相关性评分),tool-handlers-system 净减约 190 行重复代码
- 统一静态导入(savePlanTracker/setPlanModeActive/collectDiagnostics/addWrittenFile)
- console.* 使用处补充豁免说明(启动/退出/刷盘阶段无渲染进程可推送日志)
- run_command 工具描述改为反映可配置执行模式

测试:
- 新增 7 个测试文件 + 扩展 2 个,共 273 个测试(原 34 → 273)
- 覆盖 agent-engine / agent-safety / context-manager / tool-registry / result-formatter / tool-parsing / memory-service / crypto / build-context / html-utils / utils / tool-handlers-fs
- 全部通过 npm run typecheck && npm test && npm run build
2026-08-26 15:02:47 +08:00

190 lines
6.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
validateToolArgs,
coerceToolArgs,
truncateToolResult,
suggestToolFix,
validateToolSecurity,
getRelevantToolDefinitions,
getEnabledToolDefinitions,
formatToolName,
getToolIcon,
} from '../src/renderer/services/tool-registry.js';
import type { ToolResult } from '../src/renderer/types.js';
describe('validateToolArgs', () => {
it('read_file 缺少 path 报错', () => {
const errors = validateToolArgs('read_file', {});
expect(errors.some(e => e.includes('path'))).toBe(true);
});
it('read_file 合法参数不报错', () => {
expect(validateToolArgs('read_file', { path: 'a.txt' })).toEqual([]);
});
it('web_search 缺少 query 报错', () => {
const errors = validateToolArgs('web_search', {});
expect(errors.some(e => e.includes('query'))).toBe(true);
});
it('枚举值校验:git action 非法', () => {
const errors = validateToolArgs('git', { action: 'frobnicate' });
expect(errors.some(e => e.includes('不在允许范围'))).toBe(true);
});
it('类型校验:max_results 应为整数', () => {
const errors = validateToolArgs('web_search', { query: 'x', max_results: 'not-a-number' });
expect(errors.some(e => e.includes('应为整数'))).toBe(true);
});
it('未知工具跳过校验(MCP 工具)', () => {
expect(validateToolArgs('mcp_unknown__foo', {})).toEqual([]);
});
});
describe('coerceToolArgs', () => {
it('字符串数字转整数', () => {
expect(coerceToolArgs('read_file', { start_line: '5' }).start_line).toBe(5);
});
it('字符串布尔转布尔', () => {
expect(coerceToolArgs('web_fetch', { mobile_ua: 'true' }).mobile_ua).toBe(true);
expect(coerceToolArgs('web_fetch', { mobile_ua: 'false' }).mobile_ua).toBe(false);
});
it('逗号分隔字符串转数组', () => {
expect(coerceToolArgs('search_files', { file_extensions: '.ts,.js' }).file_extensions).toEqual(['.ts', '.js']);
});
it('JSON 字符串转数组', () => {
expect(coerceToolArgs('search_files', { file_extensions: '[".ts"]' }).file_extensions).toEqual(['.ts']);
});
it('保持未知参数原样', () => {
expect(coerceToolArgs('read_file', { weird: 'value' }).weird).toBe('value');
});
});
describe('truncateToolResult', () => {
it('小结果原样返回', () => {
const r: ToolResult = { success: true, content: 'short' };
expect(truncateToolResult(r, 'read_file')).toBe(r);
});
it('大字符串字段截断保留头尾', () => {
// content 属于截断字段;需让整体 JSON 超过 100KB 才会触发截断
const big = 'a'.repeat(120000);
const out = truncateToolResult({ success: true, content: big }, 'read_file');
expect((out as Record<string, unknown>).content).toContain('已截断');
});
it('字段截断后仍超限时暴力截断为 preview', () => {
// 多个非截断字段的大值使总和远超 100KB,触发 preview 兜底
const r: ToolResult = { success: true, a: 'x'.repeat(60000), b: 'y'.repeat(60000) };
const out = truncateToolResult(r, 'read_file');
expect(typeof (out as Record<string, unknown>).preview).toBe('string');
expect((out as Record<string, unknown>)._omitted_chars).toBeGreaterThan(0);
});
});
describe('suggestToolFix', () => {
it('文件未找到建议检查路径', () => {
const s = suggestToolFix('read_file', { path: '/nope' }, 'ENOENT: no such file');
expect(s).toContain('路径');
});
it('权限拒绝建议检查权限', () => {
const s = suggestToolFix('read_file', {}, 'EACCES: permission denied');
expect(s).toContain('权限');
});
it('网络错误建议检查网络', () => {
const s = suggestToolFix('web_fetch', { url: 'http://x' }, 'ECONNREFUSED');
expect(s).toContain('网络');
});
it('通用错误返回空串', () => {
expect(suggestToolFix('read_file', {}, 'something else')).toBe('');
});
});
describe('validateToolSecurity', () => {
it('本地文件工具拒绝 URL 路径', () => {
const r = validateToolSecurity('read_file', { path: 'http://example.com/x' });
expect(r).toBeTruthy();
expect(r).toContain('web_fetch');
});
it('拒绝 file:// 协议', () => {
const r = validateToolSecurity('web_fetch', { url: 'file:///etc/passwd' });
expect(r).toContain('file://');
});
it('路径遍历检测', () => {
const r = validateToolSecurity('read_file', { path: '../../../../etc/passwd' });
expect(r).toContain('路径遍历');
});
it('命令注入检测', () => {
const r = validateToolSecurity('run_command', { command: 'echo a; rm -rf /' });
expect(r).toContain('注入');
});
it('正常参数返回 null', () => {
expect(validateToolSecurity('read_file', { path: 'a.txt' })).toBeNull();
});
it('read_multiple_files paths 数组含 URL 拒绝', () => {
const r = validateToolSecurity('read_multiple_files', { paths: ['http://x/a', '/local/b'] });
expect(r).toContain('URL');
});
});
describe('getRelevantToolDefinitions', () => {
it('短查询返回全部已启用工具', () => {
const tools = getRelevantToolDefinitions('hi');
expect(tools).toHaveLength(getEnabledToolDefinitions().length);
});
it('空查询返回全部已启用工具', () => {
expect(getRelevantToolDefinitions('')).toHaveLength(getEnabledToolDefinitions().length);
});
it('包含核心工具', () => {
const names = getRelevantToolDefinitions('请读取这个文件并搜索内容').map(t => t.function.name);
expect(names).toContain('read_file');
expect(names).toContain('search_files');
});
it('匹配到足够多时不返回全部(含 web 相关)', () => {
const names = getRelevantToolDefinitions('帮我搜索网页并抓取内容').map(t => t.function.name);
expect(names).toContain('web_search');
expect(names).toContain('web_fetch');
});
it('过滤后过少时回退到全部', () => {
// 极小匹配场景 → 保留核心 + 至少 60% 规则,回退为全部
const tools = getRelevantToolDefinitions('随便问点什么奇怪的内容呢');
expect(tools.length).toBeGreaterThanOrEqual(8);
});
});
describe('formatToolName / getToolIcon', () => {
it('已知工具返回中文名', () => {
expect(formatToolName('read_file')).toBe('读取文件');
expect(formatToolName('web_search')).toBe('联网搜索');
});
it('未知工具返回原名字', () => {
expect(formatToolName('mcp_unknown')).toBe('mcp_unknown');
});
it('已知工具返回图标', () => {
expect(getToolIcon('read_file')).toBe('📄');
});
it('未知工具返回默认图标', () => {
expect(getToolIcon('mcp_unknown')).toBe('🔧');
});
});