/** * Guard: 同一 convId 恢复上下文时(buildPrompt 场景 C)按模型上下文 token 预算动态注入历史, * 不再是固定的 slice(-20) + 单条 500 字截断 —— 预算内整段塞进去,超预算才从最早的开始丢。 * 否则 ACP 进程重启/空闲超时后,早于第 20 条的历史会静默丢失,agent 会"忘了"之前聊过什么。 */ const fs = require('fs'); const path = require('path'); const { buildPrompt, appendHistory, truncateHistoryToBudget, estimateTokens } = require('../../routes/agent-chat'); const agentId = 'master'; const agentDir = path.resolve(__dirname, `../../../system/agents/${agentId}`); describe('agent-chat buildPrompt 场景 C:同 convId 恢复上下文', () => { it('注入完整历史,不按 slice(-20) 或单条 500 字截断', () => { const convId = `test-history-guard-${Date.now()}`; const convDir = path.resolve(__dirname, `../../../system/agents/${agentId}/_global/conversations/${convId}`); try { const longFirstMessage = 'X'.repeat(600); // 超过原来单条 500 字截断阈值 appendHistory(agentId, convId, { role: 'user', content: longFirstMessage }); for (let i = 0; i < 25; i++) { appendHistory(agentId, convId, { role: 'assistant', content: `filler-${i}` }); } const prompt = buildPrompt(agentDir, agentId, convId, '新消息', true); expect(prompt).toContain(longFirstMessage); expect(prompt).not.toContain('...(截断)'); } finally { fs.rmSync(convDir, { recursive: true, force: true }); } }); }); describe('truncateHistoryToBudget:预算超支时按 token 从最早的开始丢', () => { it('预算装不下全部历史时,丢最老的,留最近的', () => { const history = [ { role: 'user', content: 'oldest' }, { role: 'assistant', content: 'middle' }, { role: 'user', content: 'newest' }, ]; const budget = estimateTokens('middle') + estimateTokens('newest'); // 刚好够后两条,装不下 oldest const kept = truncateHistoryToBudget(history, budget); expect(kept.map(m => m.content)).toEqual(['middle', 'newest']); }); it('单条消息本身就超预算时,仍然至少保留最近一条,不会返回空', () => { const history = [{ role: 'user', content: 'X'.repeat(10000) }]; const kept = truncateHistoryToBudget(history, 1); expect(kept).toHaveLength(1); }); });