Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
134 lines
6.8 KiB
JavaScript
134 lines
6.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {createHash} from 'node:crypto';
|
|
import {readFileSync} from 'node:fs';
|
|
import {pathToFileURL} from 'node:url';
|
|
|
|
// An installed SDK directory can be supplied to keep node_modules outside the repo.
|
|
const sdkDir = process.env.FEP_MATRIX_SDK_DIR;
|
|
const sdk = await import(sdkDir ? pathToFileURL(`${sdkDir}/lib/index.js`).href : 'matrix-js-sdk');
|
|
const {createClient, MatrixScheduler, Room, MatrixEvent} = sdk;
|
|
const loggerModule = await import(sdkDir ? pathToFileURL(`${sdkDir}/lib/logger.js`).href : 'matrix-js-sdk/lib/logger.js');
|
|
loggerModule.logger.setLevel('silent');
|
|
const installedDir = sdkDir ? pathToFileURL(`${sdkDir}/`) : new URL('./node_modules/matrix-js-sdk/', import.meta.url);
|
|
const packageUrl = new URL('package.json', installedDir);
|
|
assert.equal(JSON.parse(readFileSync(packageUrl)).version, '42.3.0');
|
|
const userId = '@research:example.invalid';
|
|
const roomId = '!research:example.invalid';
|
|
const digest = data => createHash('sha256').update(data).digest('hex');
|
|
|
|
function fixture({dropFirst = false, holdFirst = false} = {}) {
|
|
const requests = [];
|
|
// Synthetic receiver: idempotency is implemented here, not proved about a homeserver.
|
|
const accepted = new Map();
|
|
let release;
|
|
let arrived;
|
|
const firstRequest = new Promise(resolve => { arrived = resolve; });
|
|
const gate = new Promise(resolve => { release = resolve; });
|
|
const client = createClient({
|
|
baseUrl: 'https://example.invalid', userId, deviceId: 'RESEARCH',
|
|
accessToken: 'synthetic-not-a-credential',
|
|
// Disable scheduler retries to expose the first failed attempt deterministically.
|
|
scheduler: new MatrixScheduler(() => -1),
|
|
fetchFn: async (url, init) => {
|
|
const path = new URL(url).pathname;
|
|
assert.equal(init.method, 'PUT');
|
|
assert.match(path, /\/send\/m.room.message\//);
|
|
const body = JSON.parse(init.body);
|
|
requests.push({path, body});
|
|
let result = accepted.get(path);
|
|
if (result) assert.deepEqual(result.body, body, 'retry payload changed');
|
|
else {
|
|
result = {body, event_id: `$accepted-${accepted.size + 1}`};
|
|
accepted.set(path, result);
|
|
}
|
|
if (requests.length === 1) {
|
|
arrived();
|
|
if (holdFirst) await gate;
|
|
if (dropFirst) throw new TypeError('synthetic response loss after acceptance');
|
|
}
|
|
return new Response(JSON.stringify({event_id: result.event_id}), {
|
|
status: 200, headers: {'Content-Type': 'application/json'},
|
|
});
|
|
},
|
|
});
|
|
const room = new Room(roomId, client, userId, {pendingEventOrdering: 'detached'});
|
|
client.store.storeRoom(room);
|
|
async function echo() {
|
|
const [path, data] = [...accepted][0];
|
|
const txn = decodeURIComponent(path.split('/').at(-1));
|
|
await room.addLiveEvents([new MatrixEvent({
|
|
event_id: data.event_id, room_id: roomId, sender: userId,
|
|
type: 'm.room.message', content: data.body, origin_server_ts: 1,
|
|
unsigned: {transaction_id: txn},
|
|
})], {});
|
|
}
|
|
return {client, room, requests, accepted, release, firstRequest, echo};
|
|
}
|
|
|
|
const results = [];
|
|
{
|
|
const f = fixture({dropFirst: true});
|
|
await assert.rejects(f.client.sendTextMessage(roomId, 'preserve this text', 'operation-a'));
|
|
const [pending] = f.room.getPendingEvents();
|
|
assert.equal(pending.status, 'not_sent');
|
|
assert.equal(f.accepted.size, 1, 'receiver accepted despite local not_sent');
|
|
assert.equal(pending.getContent().body, 'preserve this text');
|
|
const txn = pending.getTxnId();
|
|
await f.client.resendEvent(pending, f.room);
|
|
assert.equal(pending.getTxnId(), txn);
|
|
assert.equal(f.requests.length, 2);
|
|
assert.deepEqual(f.requests[0], f.requests[1]);
|
|
assert.equal(f.accepted.size, 1);
|
|
assert.equal(pending.status, 'sent');
|
|
await f.echo();
|
|
assert.equal(f.room.getPendingEvents().length, 0);
|
|
assert.equal(f.room.getLiveTimeline().getEvents().length, 1);
|
|
results.push({case: 'lost-response-same-operation-retry', passed: true,
|
|
requests: 2, synthetic_receiver_commits: 1, status_after_loss: 'not_sent',
|
|
pending_after_echo: 0, timeline_events: 1});
|
|
f.client.stopClient();
|
|
}
|
|
{
|
|
const f = fixture({dropFirst: true});
|
|
await assert.rejects(f.client.sendTextMessage(roomId, 'same intent', 'operation-b'));
|
|
await assert.rejects(f.client.sendTextMessage(roomId, 'same intent', 'operation-b-new'), /blocked/);
|
|
assert.equal(f.requests.length, 1, 'pending queue should block a fresh send');
|
|
for (const event of [...f.room.getPendingEvents()]) f.client.cancelPendingEvent(event);
|
|
await f.client.sendTextMessage(roomId, 'same intent', 'operation-b-after-cancel');
|
|
assert.equal(f.accepted.size, 2);
|
|
results.push({case: 'negative-control-new-identity', passed: true,
|
|
synthetic_receiver_commits: 2, fresh_send_initially_blocked: true,
|
|
conclusion: 'cancelling local pending records then using a new identity permits a duplicate logical send'});
|
|
f.client.stopClient();
|
|
}
|
|
for (const dropFirst of [false, true]) {
|
|
const f = fixture({holdFirst: true, dropFirst});
|
|
// Attach rejection handling immediately, before deliberately delaying the response.
|
|
const outcome = f.client.sendTextMessage(roomId, 'echo first', 'operation-c')
|
|
.then(() => 'resolved', () => 'rejected');
|
|
await f.firstRequest;
|
|
const pending = f.room.getPendingEvents()[0];
|
|
await f.echo();
|
|
assert.equal(f.room.getPendingEvents().length, 0);
|
|
assert.equal(pending.status, null);
|
|
f.release();
|
|
const promiseOutcome = await outcome;
|
|
assert.equal(promiseOutcome, dropFirst ? 'rejected' : 'resolved');
|
|
assert.equal(f.room.getPendingEvents().length, 0);
|
|
assert.equal(f.room.getLiveTimeline().getEvents().length, 1);
|
|
assert.equal(pending.status, null, 'late transport result regressed reconciled event');
|
|
results.push({case: dropFirst ? 'echo-before-transport-error' : 'echo-before-success-response',
|
|
passed: true, promise_outcome: promiseOutcome, event_status: pending.status,
|
|
pending_events: 0, timeline_events: 1});
|
|
f.client.stopClient();
|
|
}
|
|
console.log(JSON.stringify({
|
|
kind: 'real-sdk-with-synthetic-transport', sdk: 'matrix-js-sdk@42.3.0',
|
|
node: process.version, live_server: false, encryption: false,
|
|
scheduler_automatic_retries: false,
|
|
experiment_sha256: digest(readFileSync(new URL(import.meta.url))),
|
|
lock_sha256: digest(readFileSync(new URL('./package-lock.json', import.meta.url))),
|
|
sdk_file_sha256: Object.fromEntries(['package.json', 'lib/client.js', 'lib/models/room.js', 'lib/scheduler.js']
|
|
.map(file => [file, digest(readFileSync(new URL(file, installedDir)))])),
|
|
results,
|
|
}, null, 2));
|