import test from 'node:test'; import assert from 'node:assert/strict'; import { buildConversationFromDbRows, countNonEmptyConversationMessages, filterUserVisibleConversation, parseAgentRunUserMessage, parseStoredConversationRow, repairConversationFromDbRows, restoreConversationUserMessagesFromAgentRunRows, restoreConversationUserMessagesFromAgentRuns, restoreConversationUserMessagesFromAgentRunsFailOpen, shouldRepairConversationFromDb, } from './conversation-repair.mjs'; const gooseMsg = (id, role, text) => ({ id, role, metadata: { userVisible: true }, content: text ? [{ type: 'text', text }] : [], }); test('parseStoredConversationRow prefers raw_json', () => { const row = { message_key: 'abc', role: 'user', text: 'fallback', raw_json: JSON.stringify({ id: 'abc', role: 'user', content: [{ type: 'text', text: 'from raw' }], metadata: { userVisible: true, displayText: '可见文案' }, }), created_at: 1_700_000_000_000, }; const parsed = parseStoredConversationRow(row); assert.equal(parsed.content[0].text, 'from raw'); assert.equal(parsed.metadata.displayText, '可见文案'); }); test('shouldRepairConversationFromDb when goose has empty placeholders', () => { const goose = [ gooseMsg('u1', 'user', '编排前缀'), gooseMsg('a1', 'assistant', '好的'), gooseMsg('msg_empty1', 'assistant', ''), gooseMsg('msg_empty2', 'user', ''), ]; const dbRows = [ { message_key: 'u-real', role: 'user', text: '小朋友在上海读初中', created_at: 1, sequence_no: 0, }, { message_key: 'a-real', role: 'assistant', text: '关于中考选择的分析…', created_at: 2, sequence_no: 1, }, ]; assert.equal(shouldRepairConversationFromDb(goose, dbRows), true); const repaired = repairConversationFromDbRows(goose, dbRows); assert.equal(countNonEmptyConversationMessages(repaired), 2); assert.match(repaired[0].content[0].text, /上海读初中/); assert.match(repaired[1].content[0].text, /中考/); }); test('repairConversationFromDbRows retains the fresh Goose tail after session recovery', () => { const goose = [ gooseMsg('recovery-user', 'user', '【Memind 任务编排】继续修改页面'), gooseMsg('fresh-assistant', 'assistant', '第一轮修改已完成'), gooseMsg('followup-user', 'user', '再加一个页脚'), gooseMsg('followup-assistant', 'assistant', '页脚也已完成'), ]; const dbRows = [ { message_key: 'old-user', role: 'user', text: '写一首诗并做成页面', created_at: 1, sequence_no: 0, }, { message_key: 'old-assistant', role: 'assistant', text: '初版页面已完成', created_at: 2, sequence_no: 1, }, { message_key: 'old-followup-user', role: 'user', text: '把标题改成金色', created_at: 3, sequence_no: 2, }, { message_key: 'old-followup-assistant', role: 'assistant', text: '标题已改成金色', created_at: 4, sequence_no: 3, }, { message_key: 'recovery-user', role: 'user', text: '继续修改页面', created_at: 5, sequence_no: 4, }, ]; const repaired = repairConversationFromDbRows(goose, dbRows); assert.deepEqual( repaired.map((message) => message.id), [ 'old-user', 'old-assistant', 'old-followup-user', 'old-followup-assistant', 'recovery-user', 'fresh-assistant', 'followup-user', 'followup-assistant', ], ); assert.equal(repaired[4].content[0].text, '继续修改页面'); assert.equal(repaired[7].content[0].text, '页脚也已完成'); }); test('shouldRepairConversationFromDb returns false when goose is complete', () => { const goose = [ gooseMsg('u1', 'user', 'hello'), gooseMsg('a1', 'assistant', 'hi there'), ]; const dbRows = [ { message_key: 'u1', role: 'user', text: 'hello', created_at: 1, sequence_no: 0 }, ]; assert.equal(shouldRepairConversationFromDb(goose, dbRows), false); }); test('buildConversationFromDbRows dedupes by message_key', () => { const rows = [ { message_key: 'same', role: 'user', text: 'first', created_at: 1, sequence_no: 0 }, { message_key: 'same', role: 'user', text: 'second', created_at: 2, sequence_no: 0 }, ]; const built = buildConversationFromDbRows(rows); assert.equal(built.length, 1); assert.equal(built[0].content[0].text, 'second'); }); test('filterUserVisibleConversation keeps messages without explicit userVisible flag', () => { const messages = [ { role: 'user', content: [{ type: 'text', text: '请记住别名' }] }, { role: 'assistant', metadata: { userVisible: true }, content: [{ type: 'text', text: '已记住' }] }, { role: 'system', metadata: { userVisible: false }, content: [{ type: 'text', text: 'hidden' }] }, ]; const visible = filterUserVisibleConversation(messages); assert.equal(visible.length, 2); assert.equal(visible[0].role, 'user'); assert.equal(visible[1].role, 'assistant'); }); test('parseAgentRunUserMessage keeps the original user text for memory extraction', () => { const parsed = parseAgentRunUserMessage({ user_message_json: JSON.stringify({ role: 'user', content: [{ type: 'text', text: '请记住灰度代号 MEM-NEW' }], metadata: { userVisible: true }, }), }); assert.equal(parsed.content[0].text, '请记住灰度代号 MEM-NEW'); assert.equal(parsed.metadata.memoryInputSource, 'agent-run-original'); }); test('restoreConversationUserMessagesFromAgentRunRows removes injected memory context', () => { const messages = [ { id: 'upstream-user-1', role: 'user', content: [{ type: 'text', text: '【Memind 任务编排】\n[Memory Context]\n- 旧记忆\n用户任务:\n请记住灰度代号 MEM-NEW' }], metadata: { userVisible: true, agentVisible: true }, }, gooseMsg('assistant-1', 'assistant', '已记住'), ]; const restored = restoreConversationUserMessagesFromAgentRunRows(messages, [{ user_message_json: JSON.stringify({ role: 'user', content: [{ type: 'text', text: '请记住灰度代号 MEM-NEW' }], metadata: { userVisible: true, agentVisible: true }, }), }]); assert.equal(restored[0].id, 'upstream-user-1'); assert.equal(restored[0].content[0].text, '请记住灰度代号 MEM-NEW'); assert.doesNotMatch(restored[0].content[0].text, /旧记忆|Memory Context/); assert.equal(restored[1].content[0].text, '已记住'); }); test('restoreConversationUserMessagesFromAgentRuns scopes successful runs by session and user', async () => { const calls = []; const pool = { async query(sql, params) { calls.push({ sql, params }); return [[{ user_message_json: JSON.stringify({ role: 'user', content: [{ type: 'text', text: '原始用户消息' }], }), }]]; }, }; const restored = await restoreConversationUserMessagesFromAgentRuns( pool, [gooseMsg('user-1', 'user', '编排后的消息')], 'session-1', 'user-1', ); assert.match(calls[0].sql, /status = 'succeeded'/); assert.match(calls[0].sql, /ORDER BY created_at DESC/); assert.match(calls[0].sql, /LIMIT \?/); assert.deepEqual(calls[0].params, ['session-1', 'user-1', 1]); assert.equal(restored[0].content[0].text, '原始用户消息'); }); test('restoreConversationUserMessagesFromAgentRunsFailOpen preserves the visible transcript', async () => { const warnings = []; const messages = [gooseMsg('user-1', 'user', '可见会话原文')]; const restored = await restoreConversationUserMessagesFromAgentRunsFailOpen( { async query() { throw new Error('agent run query unavailable'); } }, messages, 'session-1', 'user-1', { logger: { warn: (...items) => warnings.push(items.join(' ')) } }, ); assert.equal(restored, messages); assert.match(warnings[0], /agent run query unavailable/); });