openvibers/openvibe.chat Commits

Chat: the message-deleted e2e replays from an Events cursor, never a number (#64)

OP OpenVibers committed 10 Oct 2026 · 9bb3233e43 · 1 file, +5 −3

Plan T7, ADR-042 decision 7: Events is about to stop accepting numeric positions (`after_seq`, a bare-number `Last-Event-ID`). This end-to-end test was the last caller in Chat to pass one: it replays from the position before the message, both over SSE and through a pull.

- It now builds that position as Events' own opaque cursor: `server/cursor.js` from the sibling Events checkout, with the store's epoch. It passes it as `last_event_id` and as `after=`, so it holds against the current Events and against the cursor-only one.

Test-only. Checked with the sibling Events checkout present:
- `ov test test/message-deleted.test.js` is green;
- the same run with a deliberately broken cursor fails, so the e2e branch really runs and is not skipped.

Changes

ddiff --git a/test/message-deleted.test.js b/test/message-deleted.test.js
iindex 7f9ef8a..67f52c2 100644
---- a/test/message-deleted.test.js
++++ b/test/message-deleted.test.js
@@@ -205,13 +205,15 @@ const userJwt = (subjectId) => serviceAuth.signServiceToken({
}, h.keys.privateKey);
let firstSeq, msgId;
++/** Events' opaque cursor for a position (its own encoder): a position goes over the wire only as a cursor. */
++const cursorAt = async (seq) => require(path.join(EVENTS_DIR, 'server', 'cursor')).encode(seq, await events.store.epoch());
t('e2e: the relay publishes the message, then its deletion; replay serves only the tombstone', async () => {
if (!haveEvents) return;
await h.db.run('UPDATE service_outbox SET sent_at = ? WHERE sent_at IS NULL', [Date.now()]); // earlier tests' rows
msgId = await say(SECRET, { user_id: alice.id, anon_id: null, username: 'alice' });
assert.strictEqual((await h.eventsRelay.outbox.flush()).sent, 1);
firstSeq = await events.store.lastSeq();
-- const before = await sse(`/realtime/stream?topics=chat.message.*&last_event_id=${firstSeq - 1}`);
++ const before = await sse(`/realtime/stream?topics=chat.message.*&last_event_id=${await cursorAt(firstSeq - 1)}`);
await before.waitFor((c) => c.events().length === 1);
assert.ok(before.body().includes('555-0199'), 'before the deletion the text is there (the leak)');
before.close();
@@@ -225,7 +227,7 @@ t('e2e: the relay publishes the message, then its deletion; replay serves only t
['service', { Authorization: `Bearer ${h.serviceToken(['events.event.read'], { aud: 'openvibe.events', sub: 'svc:search' })}` }],
];
for (const [who, headers] of views) {
-- const c = await sse(`/realtime/stream?topics=chat.message.*&last_event_id=${firstSeq - 1}`, headers);
++ const c = await sse(`/realtime/stream?topics=chat.message.*&last_event_id=${await cursorAt(firstSeq - 1)}`, headers);
await c.waitFor((x) => x.events().length === 2);
const [created, deleted] = c.events();
assert.deepStrictEqual([created.seq, deleted.seq], [firstSeq, firstSeq + 1], `${who}: sequence intact`);
@@@ -236,7 +238,7 @@ t('e2e: the relay publishes the message, then its deletion; replay serves only t
assert.ok(!c.body().includes('555-0199') && !c.body().includes('usr_01J9AAAAAAAAAAAAAAAAAAAAAA'), `${who}: no text, no author`);
c.close();
}
-- const pull = await fetch(`http://127.0.0.1:${eventsPort}/api/v1/events?topic=chat.message.*&after_seq=${firstSeq - 1}`, {
++ const pull = await fetch(`http://127.0.0.1:${eventsPort}/api/v1/events?topic=chat.message.*&after=${await cursorAt(firstSeq - 1)}`, {
headers: { Authorization: `Bearer ${h.serviceToken(['events.event.read'], { aud: 'openvibe.events', sub: 'svc:search' })}` },
}).then((r) => r.json());
assert.deepStrictEqual(pull.events.map((e) => e.seq), [firstSeq, firstSeq + 1]);