chatMessageOrdering.test.ts3.2 KBView on GitHub import {
isOutbound,
occurredAtOf,
sortKeyOf,
type LooseMessage,
} from '@/modules/inbox/components/ChannelThreadView';
/**
* How a LinkedIn / WhatsApp thread decides ORDER and AUTHORSHIP.
*
* The ordering bug these pin was invisible in both the types and a screenshot. LinkedIn's rows
* come off a drizzle `timestamp` column, and tRPC's superjson transformer revives them as `Date`
* on the client — while `LooseMessage` declared `string`. The sort compared
* `String(occurredAt) < String(occurredAt)`, and `String(new Date())` is
* `"Sun Aug 16 2026 13:31:21 GMT-0700"`, so the thread was ordered BY WEEKDAY NAME: Fri, Mon,
* Sat, Sun, Thu, Tue, Wed. Most days that still looks plausible, which is why it survived — it
* surfaced as a day pill sitting above a message from a different day.
*/
const at = (when: string | Date, over: Partial<LooseMessage> = {}): LooseMessage => ({
id: String(when),
text: 'x',
occurredAt: when,
...over,
});
describe('sortKeyOf', () => {
it('orders Dates chronologically, not by their weekday name', () => {
// Thu Aug 13 vs Sun Aug 16. Lexically "Sun…" < "Thu…", so the old comparison put the LATER
// message first — the exact inversion that moved the "Today" pill above yesterday's message.
const thu = at(new Date('2026-08-13T11:19:47.961Z'));
const sun = at(new Date('2026-08-16T20:31:21.529Z'));
expect([sun, thu].sort((a, b) => sortKeyOf(a) - sortKeyOf(b))).toEqual([thu, sun]);
});
it('orders ISO strings and Dates against each other', () => {
// The two sources disagree on type: LinkedIn's mirror sends Dates, WhatsApp's live read
// sends ISO strings. A thread never mixes them today, but the comparison must not care.
const older = at('2026-08-13T11:19:47.961Z');
const newer = at(new Date('2026-08-16T20:31:21.529Z'));
expect([newer, older].sort((a, b) => sortKeyOf(a) - sortKeyOf(b))).toEqual([older, newer]);
});
it('sorts a message with no usable timestamp to the END', () => {
// Almost always one we just sent and have not heard back about — it belongs at the bottom,
// not at the epoch above every real message.
const real = at('2026-08-16T20:31:21.529Z');
const undated: LooseMessage = { id: 'pending', text: 'just sent' };
expect([undated, real].sort((a, b) => sortKeyOf(a) - sortKeyOf(b))).toEqual([real, undated]);
});
});
describe('occurredAtOf', () => {
it('normalizes either spelling and either type to an ISO string', () => {
expect(occurredAtOf(at(new Date('2026-08-16T20:31:21.529Z')))).toBe('2026-08-16T20:31:21.529Z');
expect(occurredAtOf({ at: '2026-08-16T20:31:21.529Z' })).toBe('2026-08-16T20:31:21.529Z');
expect(occurredAtOf({ createdAt: '2026-08-16T20:31:21.529Z' })).toBe('2026-08-16T20:31:21.529Z');
expect(occurredAtOf({})).toBeUndefined();
});
});
describe('isOutbound', () => {
it('trusts WhatsApp’s boolean and LinkedIn’s direction alike', () => {
expect(isOutbound({ fromSelf: true })).toBe(true);
expect(isOutbound({ fromSelf: false, direction: 'outbound' })).toBe(false);
expect(isOutbound({ direction: 'outbound' })).toBe(true);
expect(isOutbound({ direction: 'inbound' })).toBe(false);
expect(isOutbound({})).toBe(false);
});
});