Files
memind/conversation-repair.test.mjs
T
john c10788dfe1
Memind CI / Test, build, and release guards (push) Successful in 2m59s
fix(memory): extract from original user messages
2026-07-22 10:56:18 +08:00

179 lines
6.3 KiB
JavaScript

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('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/);
});