Files
cinny/src/app/features/room/message/forwardContent.test.ts
T
jaredandClaude Opus 5 5fc90feef9
CI / Build & Quality Checks (push) Successful in 1m31s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Successful in 2m7s
feat(forward): Discord-style provenance — 'Forwarded from <sender> in <room> · <time>' with jump to the original
Forwarded events carried no origin at all; they read as the forwarder's own
words. buildForwardContent now stamps io.lotus.forwarded (sender, ts,
room_id, event_id; re-forwards keep the original stamp) and the main and
thread timelines render a reply-style header above the message that jumps
to the original when the viewer is in the source room (sender + time only
otherwise — the source room's name is not leaked). Unit-tested; verified
end to end with Playwright (header text, event content, jump, re-forward).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-18 21:07:33 -04:00

174 lines
5.3 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
import { buildForwardContent, getForwardedMeta } from './forwardContent';
// Pure content builder buildForwardContent: refuses undecryptable events, forwards
// the latest edit (`m.new_content`), and strips reply fallbacks + `m.relates_to`.
// MatrixClient / MatrixEvent are mocked minimally. getEditedEvent reads edits off
// `timelineSet.relations.getChildEventsForEvent(...).getRelations()`, so the base
// client returns no child edits and the edit test injects one.
const SENDER = '@me:example.org';
type EventOptions = {
content?: Record<string, unknown>;
type?: string;
id?: string;
roomId?: string;
decryptionFailure?: boolean;
ts?: number;
};
const makeEvent = (options: EventOptions = {}): MatrixEvent => {
const {
content = {},
type = 'm.room.message',
id = '$evt:example.org',
roomId = '!room:example.org',
decryptionFailure = false,
ts = 0,
} = options;
return {
getContent: () => content,
getType: () => type,
getId: () => id,
getRoomId: () => roomId,
getSender: () => SENDER,
getTs: () => ts,
isDecryptionFailure: () => decryptionFailure,
} as unknown as MatrixEvent;
};
// Base client: the timeline reports no `m.replace` edits, so the original content
// is forwarded unchanged.
const makeClient = (): MatrixClient =>
({
getRoom: () => ({
getUnfilteredTimelineSet: () => ({
relations: {
getChildEventsForEvent: () => null,
},
}),
}),
}) as unknown as MatrixClient;
test('plain text forwards the body and strips m.relates_to', () => {
const mx = makeClient();
const mEvent = makeEvent({
content: {
msgtype: 'm.text',
body: 'hello world',
'm.relates_to': { rel_type: 'm.thread', event_id: '$root:example.org' },
},
});
const content = buildForwardContent(mx, mEvent);
assert.ok(content);
assert.equal(content.body, 'hello world');
assert.equal(content.msgtype, 'm.text');
assert.equal(content['m.relates_to'], undefined);
});
test('reply quote is stripped from body and formatted_body', () => {
const mx = makeClient();
const mEvent = makeEvent({
content: {
msgtype: 'm.text',
body: '> <@alice:example.org> original\n\nmy reply',
format: 'org.matrix.custom.html',
formatted_body: '<mx-reply><blockquote>original</blockquote></mx-reply>my reply',
'm.relates_to': { 'm.in_reply_to': { event_id: '$root:example.org' } },
},
});
const content = buildForwardContent(mx, mEvent);
assert.ok(content);
assert.equal(content.body, 'my reply');
assert.equal(content.formatted_body, 'my reply');
assert.equal(content['m.relates_to'], undefined);
});
test('decryption failure returns undefined', () => {
const mx = makeClient();
const mEvent = makeEvent({
content: { msgtype: 'm.bad.encrypted' },
decryptionFailure: true,
});
assert.equal(buildForwardContent(mx, mEvent), undefined);
});
test('edited message forwards m.new_content', () => {
const mEvent = makeEvent({
content: {
msgtype: 'm.text',
body: 'original body',
'm.relates_to': { rel_type: 'm.thread', event_id: '$root:example.org' },
},
});
// The latest `m.replace` edit carries the new content under `m.new_content`.
const editEvent = makeEvent({
content: { 'm.new_content': { msgtype: 'm.text', body: 'edited body' } },
ts: 100,
});
const mx = {
getRoom: () => ({
getUnfilteredTimelineSet: () => ({
relations: {
getChildEventsForEvent: () => ({
getRelations: () => [editEvent],
}),
},
}),
}),
} as unknown as MatrixClient;
const content = buildForwardContent(mx, mEvent);
assert.ok(content);
assert.equal(content.body, 'edited body');
assert.equal(content.msgtype, 'm.text');
assert.equal(content['m.new_content'], undefined);
assert.equal(content['m.relates_to'], undefined);
});
test('forward stamps io.lotus.forwarded with the original sender / time / room / event', () => {
const mx = makeClient();
const mEvent = makeEvent({ content: { msgtype: 'm.text', body: 'prov' } });
const content = buildForwardContent(mx, mEvent);
assert.ok(content);
const meta = getForwardedMeta(content);
assert.ok(meta);
assert.equal(meta.sender, mEvent.getSender());
assert.equal(meta.origin_server_ts, mEvent.getTs());
assert.equal(meta.room_id, mEvent.getRoomId());
assert.equal(meta.event_id, mEvent.getId());
});
test('forwarding a forward keeps the ORIGINAL provenance', () => {
const mx = makeClient();
const original = {
sender: '@origin:example.org',
origin_server_ts: 1000,
room_id: '!origin:example.org',
event_id: '$origin:example.org',
};
const mEvent = makeEvent({
content: { msgtype: 'm.text', body: 'again', 'io.lotus.forwarded': original },
});
const content = buildForwardContent(mx, mEvent);
assert.ok(content);
assert.deepEqual(getForwardedMeta(content), original);
});
test('getForwardedMeta rejects malformed metadata', () => {
assert.equal(getForwardedMeta({ 'io.lotus.forwarded': 'nope' }), undefined);
assert.equal(getForwardedMeta({ 'io.lotus.forwarded': { sender: 1 } }), undefined);
assert.equal(getForwardedMeta({}), undefined);
});