feat: v0.4.1 质量加固版 — 工程化基线 + 安全加固 + 测试补齐 + 体验升级
工程化(从零到一): - 新增 Gitea Actions CI(debian-latest):类型检查 + Lint + 单元测试 + 产物编译验证 - 新增 husky + lint-staged 预提交钩子(lint-staged + typecheck 门禁) - 移除坏脚本 test:e2e(无 Playwright 配置必失败);prebuild 改用内置 fs.rmSync - 依赖清理:移除死依赖 sql.js(2MB)/@playwright/test,@types/shell-quote 移至 devDependencies 安全加固: - PolicyEngine 频率限制按会话隔离(多会话并发不再互抢配额) - ConfirmationHook 拒绝记忆加 10 分钟 TTL + 恢复询问入口(新增 2 个 IPC 通道) - Windows run_command 白名单工具(git/node/npm/npx/pnpm/yarn/tsc)改走 cmd.exe /c + 参数数组执行,收窄 shell 注入面 - web_search 四引擎 HTML 解析迁移 node-html-parser(结构化主层 + 正则降级) 缺陷修复(测试驱动发现): - mapError 大小写缺陷:网络错误码永远落入 UNKNOWN 无法触发重试 - 搜狗解析器自我过滤:相对链接补全后又被 sogou.com 过滤导致结果全丢 - 百度复合类名重复收录:class="result c-container" 被双重匹配 测试补齐(113 → 194 用例): - 新增 5 个测试文件:sse-stream / base-adapter / confirmation-hook / ipc-agent 编排链路 / web-search 解析器 - 覆盖 sendMessage 全分支、SSE 流解析、错误映射、确认钩子竞态/超时/批量审批 体验升级: - OutputValidator 验证结果可见化(VALIDATION 流事件 → 聊天流提示卡) - SettingsModal 巨型组件拆分(1503 行 → 10 个文件,可独立维护) - MessageList 接入 react-virtuoso 真虚拟滚动(千条消息恒定开销) - MCP 新增 streamable HTTP 传输支持(SDK 内置传输 + DB 迁移 6 + UI 双模式)
This commit is contained in:
@@ -9,12 +9,34 @@ import { PolicyEngine, DEFAULT_POLICIES } from '../permissions';
|
||||
describe('PolicyEngine 默认策略', () => {
|
||||
it('所有内置工具均有策略配置', () => {
|
||||
const knownTools = [
|
||||
'read_file', 'write_file', 'list_directory', 'search_files', 'delete_file',
|
||||
'file_move', 'file_info', 'file_editor', 'code_search', 'diff_viewer',
|
||||
'web_search', 'web_fetch', 'web_browser', 'http_request',
|
||||
'memory_store', 'memory_search', 'run_command', 'task_manager',
|
||||
'delegate_task', 'git_status', 'git_diff', 'git_log', 'git_commit',
|
||||
'lint_code', 'run_tests', 'project_info', 'think', 'view_image',
|
||||
'read_file',
|
||||
'write_file',
|
||||
'list_directory',
|
||||
'search_files',
|
||||
'delete_file',
|
||||
'file_move',
|
||||
'file_info',
|
||||
'file_editor',
|
||||
'code_search',
|
||||
'diff_viewer',
|
||||
'web_search',
|
||||
'web_fetch',
|
||||
'web_browser',
|
||||
'http_request',
|
||||
'memory_store',
|
||||
'memory_search',
|
||||
'run_command',
|
||||
'task_manager',
|
||||
'delegate_task',
|
||||
'git_status',
|
||||
'git_diff',
|
||||
'git_log',
|
||||
'git_commit',
|
||||
'lint_code',
|
||||
'run_tests',
|
||||
'project_info',
|
||||
'think',
|
||||
'view_image',
|
||||
];
|
||||
for (const tool of knownTools) {
|
||||
expect(DEFAULT_POLICIES.some((p) => p.toolName === tool)).toBe(true);
|
||||
@@ -57,7 +79,9 @@ describe('deniedPatterns 深度扫描', () => {
|
||||
|
||||
it('正常路径不误判', () => {
|
||||
const engine = new PolicyEngine();
|
||||
expect(engine.checkAuthorization('read_file', { file_path: 'src/main.ts' }).authorized).toBe(true);
|
||||
expect(engine.checkAuthorization('read_file', { file_path: 'src/main.ts' }).authorized).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,3 +115,45 @@ describe('通配符策略(mcp_*)', () => {
|
||||
expect(result.requiresConfirmation).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('频率限制会话隔离(v0.4.1)', () => {
|
||||
it('不同会话各自独立配额(一个会话耗尽不影响另一个)', () => {
|
||||
const engine = new PolicyEngine();
|
||||
// web_search 默认 maxFrequency: 10
|
||||
// 会话 A 耗尽全部配额
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-A').authorized).toBe(
|
||||
true,
|
||||
);
|
||||
engine.recordCall('web_search', 'session-A');
|
||||
}
|
||||
// 会话 A 已被限流
|
||||
expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-A').authorized).toBe(
|
||||
false,
|
||||
);
|
||||
// 会话 B 配额不受影响
|
||||
expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-B').authorized).toBe(
|
||||
true,
|
||||
);
|
||||
expect(engine.recordCall('web_search', 'session-B') === undefined).toBe(true);
|
||||
});
|
||||
|
||||
it('recordCall 与 checkAuthorization 使用相同的会话 key', () => {
|
||||
const engine = new PolicyEngine();
|
||||
// 会话 A 记录 10 次
|
||||
for (let i = 0; i < 10; i++) engine.recordCall('web_search', 'session-A');
|
||||
// 会话 A 限流,会话 B 不限
|
||||
expect(engine.checkAuthorization('web_search', {}, 'session-A').authorized).toBe(false);
|
||||
expect(engine.checkAuthorization('web_search', {}, 'session-B').authorized).toBe(true);
|
||||
});
|
||||
|
||||
it('无 sessionId 时计入 global 桶(向后兼容)', () => {
|
||||
const engine = new PolicyEngine();
|
||||
// 旧式调用(无 sessionId)共享 global 桶
|
||||
for (let i = 0; i < 10; i++) {
|
||||
engine.recordCall('web_search');
|
||||
}
|
||||
expect(engine.checkAuthorization('web_search', {}).authorized).toBe(false);
|
||||
expect(engine.checkAuthorization('web_search', {}, 'any-session').authorized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user