Files
creator-hub/web/tests/private-messages.test.cjs
T
rogee d94f83cde9
douyin-release-gate / verify (push) Failing after 18m44s
fix(messages): stabilize conversation ordering by real message time
2026-10-07 13:13:06 +08:00

112 lines
7.5 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { resolve } = require('node:path');
const Module = require('node:module');
const ts = require('typescript');
const file = resolve(__dirname, '../src/utils/private-messages.ts');
const loaded = new Module(file, module);
loaded._compile(ts.transpileModule(readFileSync(file, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText, file);
const util = loaded.exports;
const contact = (account_id, peer_uid, time = '', last_text = '') => ({ account_id, peer_uid, account_name: '', peer_name: '', last_text, last_message_at: time, message_count: 0 });
const message = (id, time = '', overrides = {}) => ({ id, account_id: 'a', peer_uid: '100', direction: 'inbound', message_type: 'text', text: '', state: 'succeeded', error: '', message_at: time, created_at: '', ...overrides });
test('conversation and message API queries preserve pagination, source and encoded peers', () => {
assert.equal(util.conversationQuery({}, 2, 25), 'page=2&page_size=25');
const query = new URLSearchParams(util.conversationQuery({ accountId: 'a/b' }, 1, 25));
assert.equal(query.get('account_id'), 'a/b');
const messages = new URLSearchParams(util.messagesQuery('a/b', '100+20', 3, 20));
assert.equal(messages.get('account_id'), 'a/b'); assert.equal(messages.get('peer_uid'), '100+20');
assert.equal(messages.get('page'), '3'); assert.equal(messages.get('page_size'), '20');
});
test('conversation identity includes account and peer, never peer alone', () => {
assert.equal(util.conversationKey(contact('a', '100')), 'a:100');
assert.notEqual(util.conversationKey(contact('a', '100')), util.conversationKey(contact('b', '100')));
assert.notEqual(util.conversationKey(contact('a', '100')), util.conversationKey(contact('a', '200')));
});
test('contact pages merge by compound key, update summaries, sort newest first and retain older rows', () => {
const old = contact('a', '100', '2026-06-01T10:00:00Z');
const updated = { ...old, last_text: '最新摘要', last_message_at: '2026-06-01T12:00:00Z' };
const previous = [old, contact('b', '100', '2026-06-01T11:00:00Z')];
const result = util.mergeConversations(previous, [updated, updated]);
assert.deepEqual(result.map(util.conversationKey), ['a:100', 'b:100']);
assert.equal(result[0].last_text, '最新摘要'); assert.equal(previous[0].last_text, '');
assert.equal(util.mergeConversations(result, []).length, 2);
});
test('missing and invalid contact times stay behind known times, equal-time ordering is deterministic', () => {
const rows = [contact('b', '100'), contact('c', '100', 'invalid'), contact('a', '100', '2026-06-01T12:00:00Z')];
assert.deepEqual(util.mergeConversations([], rows).map(util.conversationKey), ['a:100', 'b:100', 'c:100']);
});
test('late conversation snapshots never roll back newer real message times or summaries', () => {
const fresh = contact('b', '100', '2026-06-01T12:00:10Z', '最新收发消息');
const old = contact('b', '100', '2026-06-01T12:00:01Z', '旧分页摘要');
const other = contact('a', '100', '2026-06-01T12:00:05Z');
const result = util.mergeConversations([fresh, other], [old]);
assert.deepEqual(result.map(util.conversationKey), ['b:100', 'a:100']);
assert.equal(result[0].last_text, '最新收发消息');
assert.equal(result[0].last_message_at, fresh.last_message_at);
const duplicates = util.mergeConversations([], [old, fresh, other]);
assert.equal(duplicates[0].last_text, '最新收发消息');
assert.equal(duplicates.length, 2);
const incomplete = util.mergeConversations([fresh], [contact('b', '100', '', '尚无真实时间')]);
assert.equal(incomplete[0].last_message_at, fresh.last_message_at);
const renamed = util.mergeConversations([fresh], [{ ...fresh, peer_name: '实时昵称' }, { ...fresh, peer_name: '过期别名' }]);
assert.equal(renamed[0].peer_name, '实时昵称');
});
test('unknown conversation times stay last even before the Unix epoch, and ties use stable compound keys', () => {
const rows = [contact('a', '2'), contact('z', '1', '1970-01-01T00:00:00Z'), contact('y', '1', '1969-12-31T23:59:59Z'), contact('a', '10'), contact('a0', '1'), contact('b', '1', 'invalid')];
const expected = ['z:1', 'y:1', 'a:10', 'a:2', 'a0:1', 'b:1'];
assert.deepEqual(util.mergeConversations([], rows).map(util.conversationKey), expected);
assert.deepEqual(util.mergeConversations([], rows.slice().reverse()).map(util.conversationKey), expected);
});
test('older message pages merge without duplicates, ascending times and updated send results', () => {
const latest = [message('3', '2026-06-01T12:00:03Z'), message('2', '2026-06-01T12:00:02Z', { state: 'sending' })];
const updated = message('2', '2026-06-01T12:00:02Z', { state: 'succeeded' });
const result = util.mergeMessages(latest, [updated, message('1', '2026-06-01T12:00:01Z'), updated]);
assert.deepEqual(result.map((m) => m.id), ['1', '2', '3']); assert.equal(result[1].state, 'succeeded');
assert.equal(latest[1].state, 'sending'); assert.equal(util.mergeMessages(result, []).length, 3);
});
test('message ordering handles real storage times and exact large numeric IDs without number rounding', () => {
const rows = [message('9007199254740993', '', { created_at: '2026-06-01T12:00:00Z' }), message('9007199254740992', '', { created_at: '2026-06-01T12:00:00Z' }), message('2'), message('10')];
assert.deepEqual(util.mergeMessages([], rows).map((m) => m.id), ['2', '10', '9007199254740992', '9007199254740993']);
});
test('draft updates isolate sessions and success only clears the exact submitted revision', () => {
let drafts = util.updateDraft({}, 'a:100', '甲');
drafts = util.updateDraft(drafts, 'b:100', '乙');
const original = drafts;
const cleared = util.clearSentDraft(drafts, 'a:100', drafts['a:100'].version);
assert.equal(cleared['a:100'].text, ''); assert.equal(cleared['b:100'].text, '乙');
assert.equal(original['a:100'].text, '甲');
const newer = util.updateDraft(drafts, 'a:100', '新输入');
assert.strictEqual(util.clearSentDraft(newer, 'a:100', drafts['a:100'].version), newer);
assert.strictEqual(util.clearSentDraft(drafts, 'missing', 1), drafts);
});
test('retyping the same value counts as new input and cannot be erased by an old success', () => {
const original = util.updateDraft({}, 'a:100', '相同文本');
const changed = util.updateDraft(util.updateDraft(original, 'a:100', '其他输入'), 'a:100', '相同文本');
assert.strictEqual(util.clearSentDraft(changed, 'a:100', original['a:100'].version), changed);
});
test('direction, state and type labels preserve unknown server values without faking delivery or reads', () => {
assert.equal(util.messageDirectionLabel('inbound'), '收到'); assert.equal(util.messageDirectionLabel('outbound'), '发出');
assert.equal(util.messageDirectionLabel('other'), 'other');
for (const [state, label, tone] of [['succeeded', '已发送', 'success'], ['sending', '发送中', 'processing'], ['failed', '发送失败', 'error'], ['unknown', '结果未确认', 'warning']]) {
assert.equal(util.messageStateLabel(state), label); assert.equal(util.messageStateTone(state), tone);
}
assert.equal(util.messageStateLabel('other'), 'other'); assert.equal(util.messageStateTone('other'), 'default');
assert.equal(util.messageTypeLabel('text'), '文字'); assert.equal(util.messageTypeLabel('image'), '图片');
assert.equal(util.messageTypeLabel('unknown'), '未知类型'); assert.equal(util.messageTypeLabel('video'), 'video');
});