'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { detectThinRedLines, detectClaimedPastAction, } = require('../mcp/src/core/thin-red-lines'); const { ClaudeCodeClient, claudeSessionResetReason, sessionFirstMode, sessionFirstSystemPrompt, applyThinRedLines, QiweiAgentRuntime, } = require('../mcp/src/core/agent-runtime'); const { enforceHumanReplyStyle } = require('../mcp/src/core/response-human-style'); function fakeClient(reply) { return { async complete() { return { content: JSON.stringify({ reply, confidence: 0.9, intent: 'consult', reason: 'fixture', requiresHuman: false, profileUpdates: {}, tasks: [], alerts: [], }), }; }, }; } async function runOnce(conversationMode, reply, inbound = '怎么收费') { const runtime = new QiweiAgentRuntime({ config: { conversationMode, maxToolRounds: 2, qualityPassScore: 82, }, knowledge: { search: () => [], knowledgeIndex: () => 'knowledge/faq.md(收费)' }, modelClient: fakeClient(reply), }); return runtime.run({ conversation: { id: 'c1', contact_id: 'u1' }, messages: [{ direction: 'inbound', content: inbound }], profile: {}, inboundContent: inbound, }); } async function main() { assert.equal(sessionFirstMode({ conversationMode: 'session' }), true); assert.equal(sessionFirstMode({}), false); const system = sessionFirstSystemPrompt({ knowledge: { knowledgeIndex: () => 'knowledge/faq.md' } }); assert.match(system, /行业通用客服/); assert.doesNotMatch(system, /22 问|房源|小牛看房|【本轮回复计划】/); const promised = '费用要按服务范围确认。我先帮您核实好再回复您。'; const thickRewrite = enforceHumanReplyStyle({ reply: promised, reason: 'fixture' }, '怎么收费', {}, {}); assert.notEqual(thickRewrite.reply, promised, '厚路径应当改写无凭据承诺'); const sessionOut = await runOnce('session', promised); assert.equal(sessionOut.content, promised, 'session 模式不得改稿'); assert.equal(sessionOut.rewriteCount, 0); assert.equal(sessionOut.qualityPassed, true); assert.equal(sessionOut.qualityPlan.version, 'session-thin-1.0'); assert.equal(sessionOut.toolTrace.some(item => item.tool === 'response_quality_gate'), false); assert.equal(sessionOut.toolTrace.some(item => item.tool === 'sales_move_planner'), false); const claimed = '资料已经发给您了,您先看。'; const flagged = applyThinRedLines({ reply: claimed, requiresHuman: false, reason: 'ok' }, {}); assert.equal(flagged.reply, claimed); assert.equal(flagged.requiresHuman, true); assert.match(flagged.reason, /thin_redline:claimed_past_action/); const claimedOut = await runOnce('session', claimed, '资料发了吗'); assert.equal(claimedOut.content, claimed); assert.equal(claimedOut.requiresHuman, true); const negated = '我不能空口说已经办好了,得核一下进度再回你。'; assert.equal(detectThinRedLines(negated, {}).length, 0); assert.equal(detectClaimedPastAction(negated, {}).length, 0); const questioned = '已经发货了吗?'; assert.equal(detectThinRedLines(questioned, {}).length, 0); const withTool = detectThinRedLines('系统里看了,流程在 knowledge/faq.md。', { toolTrace: [{ tool: 'search_knowledge', executed: true }], }); assert.equal(withTool.length, 0); const thickOut = await runOnce('', promised); assert.notEqual(thickOut.content, promised, '未开 session 时厚路径仍应改写'); assert.equal(thickOut.toolTrace.some(item => item.tool === 'response_quality_gate'), true); const sessionDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-session-reset-')); const sessionFile = path.join(sessionDirectory, 'sessions.json'); try { const client = new ClaudeCodeClient({ claudeExecutable: process.execPath, claudeWorkdir: process.cwd(), claudeSessionFile: sessionFile, claudeProjectId: 'session-reset-smoke', claudeSessionMaxTurns: 12, claudeSessionMaxAgeMs: 86400000, }); const key = 'session-reset-contact'; client.sessionStore.ensure(key, {}); client.sessionStore.markInitialized(key, {}); const originalId = client.sessionStore.get(key).id; const invokedSessionIds = []; client.invoke = async (_messages, _context, session) => { invokedSessionIds.push(session.id); if (invokedSessionIds.length === 1) throw new Error('Claude Code 处理超时'); return { content: '{}', claudeCode: {} }; }; await assert.rejects( client.complete([], [], { conversation: { id: key } }), /处理超时/, ); const reset = client.sessionStore.get(key); assert.equal(claudeSessionResetReason(new Error('Claude Code 处理超时')), 'session_timeout'); assert.equal(invokedSessionIds.length, 1, '超时会话应立即结束本轮,避免重复等待'); assert.equal(invokedSessionIds[0], originalId); assert.notEqual(reset.id, originalId); assert.equal(reset.initialized, false); assert.equal(reset.epochHistory.at(-1).closedReason, 'session_timeout'); await client.complete([], [], { conversation: { id: key } }); assert.equal(invokedSessionIds.length, 2, '下一条消息必须使用重建后的新会话'); assert.equal(invokedSessionIds[1], reset.id); const repaired = client.sessionStore.get(key); assert.equal(repaired.initialized, true); } finally { fs.rmSync(sessionDirectory, { recursive: true, force: true }); } process.stdout.write('session-first smoke ok\n'); } main().catch((error) => { process.stderr.write(`${error.stack || error.message}\n`); process.exit(1); });