179 lines
6.3 KiB
JavaScript
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/);
|
|
});
|