Files
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

81 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { buildContext } from '../src/renderer/services/context-manager.js';
import type { OllamaMessage } from '../src/renderer/types.js';
function makeMsgs(n: number): OllamaMessage[] {
return Array.from({ length: n }, (_, i) => ({ role: 'user' as const, content: `消息 ${i}` }));
}
describe('buildContext — 滑动窗口构建', () => {
it('消息数不超过窗口时原样返回', () => {
const msgs = makeMsgs(5);
const out = buildContext(msgs, { windowSize: 20, maxTokens: 131072 });
expect(out.length).toBe(5);
expect(out[0].content).toBe('消息 0');
});
it('超过窗口时保留最近 windowSize 条', () => {
const msgs = makeMsgs(30);
const out = buildContext(msgs, { windowSize: 10, maxTokens: 131072 });
// 最近的 10 条(索引 20-29)保留
expect(out.some(m => m.content === '消息 25')).toBe(true);
expect(out.some(m => m.content === '消息 0')).toBe(false);
});
it('system 消息置于最前', () => {
const msgs: OllamaMessage[] = [
{ role: 'user', content: '你好' },
{ role: 'system', content: '你是助手' },
];
const out = buildContext(msgs, { windowSize: 20, maxTokens: 131072 });
expect(out[0].role).toBe('system');
expect(out[0].content).toContain('你是助手');
});
it('注入 memoryContext 与 workspaceContext 动态前缀', () => {
const out = buildContext([], {
windowSize: 20,
maxTokens: 131072,
memoryContext: '[memory 上下文]',
workspaceContext: '[workspace 目录]',
});
const sys = out.find(m => m.role === 'system');
expect(sys?.content).toContain('[memory 上下文]');
expect(sys?.content).toContain('[workspace 目录]');
});
it('合并重复 system 消息为单条', () => {
const msgs: OllamaMessage[] = [
{ role: 'system', content: '规则 A' },
{ role: 'system', content: '规则 B' },
{ role: 'user', content: '你好' },
];
const out = buildContext(msgs, { windowSize: 20, maxTokens: 131072 });
const sysCount = out.filter(m => m.role === 'system').length;
expect(sysCount).toBeLessThanOrEqual(2);
});
it('token 超限时裁剪(需消息数超过窗口才触发)', () => {
// 25 条 > windowSize 20,走滑动窗口+裁剪路径
// 每条 500 字符 ≈ 125 token;小预算触发裁剪
const msgs: OllamaMessage[] = [];
for (let i = 0; i < 25; i++) msgs.push({ role: 'user', content: 'x'.repeat(500) });
const out = buildContext(msgs, { windowSize: 20, maxTokens: 300 });
// 只保护最近 6 条,其余被裁剪
expect(out.length).toBeLessThan(25);
expect(out.length).toBeGreaterThanOrEqual(1);
});
it('大预算时不裁剪(25 条返回 windowSize+摘要)', () => {
const msgs: OllamaMessage[] = [];
for (let i = 0; i < 25; i++) msgs.push({ role: 'user', content: 'x'.repeat(500) });
const out = buildContext(msgs, { windowSize: 20, maxTokens: 100000 });
// 25 条 → 部分摘要,不丢失全部 → 至少 20 条窗口内的
expect(out.length).toBeGreaterThanOrEqual(20);
});
it('空消息列表返回空(无 system 时)', () => {
expect(buildContext([], { windowSize: 20, maxTokens: 131072 })).toEqual([]);
});
});