620 lines
32 KiB
TypeScript
620 lines
32 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { createLanes, type LaneRequest, type Settled } from '../src/fixes/mutationGate/lanes.ts';
|
|
import { classifyResponse, matchMutation, synthesizedBody, type MutationMatch } from '../src/fixes/mutationGate/matchers.ts';
|
|
import { installMutationGate } from '../src/fixes/mutationGate/index.ts';
|
|
import { X_FEATURES } from '../src/areas/table.ts';
|
|
import { FEATURE_DEFAULTS } from '../src/inject/features.ts';
|
|
|
|
const BASE = 'https://x.com/home';
|
|
const post = (body: BodyInit | null | undefined): RequestInit => ({ method: 'POST', body });
|
|
const likeBody = (id: unknown) => JSON.stringify({ variables: { tweet_id: id }, queryId: 'lI07N6Otwv1PhnEgXILM7A' });
|
|
const tick = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
// ---------- matchers ----------
|
|
|
|
test('matcher: like/unlike and follow/unfollow fixtures', () => {
|
|
assert.deepEqual(matchMutation('/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet', post(likeBody('1790000000000000001')), BASE),
|
|
{ kind: 'like', id: '1790000000000000001', key: 'tweet:1790000000000000001', want: true });
|
|
assert.deepEqual(matchMutation(new URL('https://x.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet'), post(likeBody('42')), BASE),
|
|
{ kind: 'like', id: '42', key: 'tweet:42', want: false });
|
|
assert.deepEqual(matchMutation('https://x.com/i/api/1.1/friendships/create.json', post('include_profile_interstitial_type=1&user_id=783214&skip_status=1'), BASE),
|
|
{ kind: 'follow', id: '783214', key: 'user:783214', want: true });
|
|
assert.deepEqual(matchMutation('/i/api/1.1/friendships/destroy.json', post(new URLSearchParams({ user_id: '783214' })), BASE),
|
|
{ kind: 'follow', id: '783214', key: 'user:783214', want: false });
|
|
// Promoted like bodies carry extra variables and still match.
|
|
assert.equal(matchMutation('/i/api/graphql/x/FavoriteTweet', post(JSON.stringify({ variables: { tweet_id: '7', promotedContent: { impressionId: 'a' } } })), BASE)?.key, 'tweet:7');
|
|
});
|
|
|
|
test('matcher: malformed, DM, read and other endpoints are untouched', () => {
|
|
const none: [unknown, RequestInit | undefined][] = [
|
|
['/i/api/graphql/x/FavoriteTweet', post('{not json')],
|
|
['/i/api/graphql/x/FavoriteTweet', post(likeBody(42))],
|
|
['/i/api/graphql/x/FavoriteTweet', post(likeBody('12a'))],
|
|
['/i/api/graphql/x/FavoriteTweet', post(JSON.stringify({ variables: {} }))],
|
|
['/i/api/graphql/x/FavoriteTweet', post('null')],
|
|
['/i/api/graphql/x/FavoriteTweet', post(new Blob(['x']))],
|
|
['/i/api/graphql/x/FavoriteTweet', { body: likeBody('1') }],
|
|
['/i/api/graphql/x/FavoriteTweet', undefined],
|
|
['/i/api/graphql/x/FavoriteTweets', post(likeBody('1'))],
|
|
['/i/api/graphql/x/CreateRetweets', post(likeBody('1'))],
|
|
['/i/api/graphql/x/DeleteRetweet', post(likeBody('1'))],
|
|
['/i/api/graphql/x/CreateBookmark', post(likeBody(1))],
|
|
['/i/api/graphql/x/DeleteBookmark', post(new Blob(['x']))],
|
|
['/i/api/graphql/x/TweetDetail', post(likeBody('1'))],
|
|
['/i/api/graphql/x/useDMReactionMutationAddMutation', post(likeBody('1'))],
|
|
['/i/api/1.1/dm/new2.json', post('user_id=1')],
|
|
['/i/api/1.1/friendships/cancel.json', post('user_id=1')],
|
|
['/i/api/1.1/friendships/update.json', post('user_id=1')],
|
|
['/i/api/1.1/friendships/create.json', post('user_id=abc')],
|
|
['/i/api/1.1/friendships/create.json', post('screen_name=jack')],
|
|
['/i/api/1.1/friendships/create.json', post('user_id=1&user_id=2')],
|
|
['/i/api/1.1/friendships/create.json', post(new FormData())],
|
|
['https://api.x.com/1.1/friendships/create.json', post('user_id=1')],
|
|
['https://evil.example/i/api/1.1/friendships/create.json', post('user_id=1')],
|
|
[new Request('https://x.com/i/api/1.1/friendships/create.json', { method: 'POST', body: 'user_id=1' }), undefined],
|
|
['http://[bad', post('user_id=1')],
|
|
[42, post('user_id=1')],
|
|
];
|
|
for (const [input, init] of none) assert.equal(matchMutation(input, init, BASE), null, `${String(input)} ${String(init?.body)}`);
|
|
});
|
|
|
|
test('classifier: confirmed, requested and unknown outcomes', () => {
|
|
const like: MutationMatch = { kind: 'like', id: '1', key: 'tweet:1', want: true };
|
|
const unlike: MutationMatch = { ...like, want: false };
|
|
const follow: MutationMatch = { kind: 'follow', id: '9', key: 'user:9', want: true };
|
|
const unfollow: MutationMatch = { ...follow, want: false };
|
|
assert.deepEqual(classifyResponse(like, 200, '{"data":{"favorite_tweet":"Done"}}'), { outcome: 'confirmed', state: true });
|
|
assert.deepEqual(classifyResponse(unlike, 200, '{"data":{"unfavorite_tweet":"Done"}}'), { outcome: 'confirmed', state: false });
|
|
assert.deepEqual(classifyResponse(like, 200, '{"errors":[{"code":139}],"data":{}}'), { outcome: 'confirmed', state: true });
|
|
assert.deepEqual(classifyResponse(unlike, 200, '{"errors":[{"code":139}]}'), { outcome: 'unknown' });
|
|
assert.deepEqual(classifyResponse(like, 429, '{"errors":[{"code":88}]}'), { outcome: 'unknown' });
|
|
assert.deepEqual(classifyResponse(like, 500, 'oops'), { outcome: 'unknown' });
|
|
assert.deepEqual(classifyResponse(like, 200, '{"data":{"favorite_tweet":"Nope"}}'), { outcome: 'unknown' });
|
|
assert.deepEqual(classifyResponse(follow, 200, '{"id_str":"9","following":false}'), { outcome: 'confirmed', state: true });
|
|
assert.deepEqual(classifyResponse(follow, 200, '{"id_str":"9","protected":true}'), { outcome: 'requested' });
|
|
assert.deepEqual(classifyResponse(follow, 200, '{"id_str":"9","follow_request_sent":true}'), { outcome: 'requested' });
|
|
assert.deepEqual(classifyResponse(unfollow, 200, '{"id_str":"9"}'), { outcome: 'confirmed', state: false });
|
|
assert.deepEqual(classifyResponse(follow, 403, '{"errors":[{"code":161}]}'), { outcome: 'unknown' });
|
|
assert.deepEqual(classifyResponse(follow, 200, '[]'), { outcome: 'unknown' });
|
|
});
|
|
|
|
test('synthesized bodies: like Done, follow clones the last user object', () => {
|
|
assert.equal(synthesizedBody({ kind: 'like', id: '1', key: 'tweet:1', want: true }, undefined), '{"data":{"favorite_tweet":"Done"}}');
|
|
assert.equal(synthesizedBody({ kind: 'like', id: '1', key: 'tweet:1', want: false }, undefined), '{"data":{"unfavorite_tweet":"Done"}}');
|
|
const follow: MutationMatch = { kind: 'follow', id: '9', key: 'user:9', want: false };
|
|
assert.deepEqual(JSON.parse(synthesizedBody(follow, '{"id_str":"9","screen_name":"a","following":true}')), { id_str: '9', screen_name: 'a', following: false });
|
|
assert.deepEqual(JSON.parse(synthesizedBody(follow, '{"id_str":"8"}')), { id: 9, id_str: '9', following: false });
|
|
assert.deepEqual(JSON.parse(synthesizedBody(follow, 'garbage')), { id: 9, id_str: '9', following: false });
|
|
});
|
|
|
|
// ---------- lane FSM ----------
|
|
|
|
type Result = { kind: 'wire' | 'synthetic'; want: boolean; n?: number };
|
|
type Wire = { n: number; key: string; want: boolean; resolve(outcome: 'ok' | 'unknown' | 'reject'): void };
|
|
|
|
function harness() {
|
|
const lanes = createLanes<Result>();
|
|
const wire: Wire[] = [];
|
|
const open = new Map<string, number>();
|
|
let maxConcurrent = 0;
|
|
let n = 0;
|
|
const click = (key: string, want: boolean, signal?: AbortSignal) => {
|
|
const id = n++;
|
|
const req: LaneRequest<Result> = {
|
|
want,
|
|
signal,
|
|
send: () => new Promise<Result>((resolve, reject) => {
|
|
open.set(key, (open.get(key) ?? 0) + 1);
|
|
maxConcurrent = Math.max(maxConcurrent, open.get(key)!);
|
|
wire.push({ n: id, key, want, resolve: outcome => {
|
|
open.set(key, open.get(key)! - 1);
|
|
if (outcome === 'reject') reject(new TypeError('Failed to fetch'));
|
|
else resolve({ kind: 'wire', want, n: outcome === 'ok' ? id : -1 });
|
|
} });
|
|
}),
|
|
classify: (result): Promise<Settled> => Promise.resolve(result.n === -1 ? { outcome: 'unknown' } : { outcome: 'confirmed', state: result.want }),
|
|
synthesize: () => ({ kind: 'synthetic', want }),
|
|
};
|
|
return lanes.submit(key, req);
|
|
};
|
|
return { lanes, wire, click, get maxConcurrent() { return maxConcurrent; } };
|
|
}
|
|
|
|
test('lane table: idle sends; busy holds; same direction shares; opposite pair-cancels', async () => {
|
|
const h = harness();
|
|
const a = h.click('t', true); // idle -> wire
|
|
const b = h.click('t', false); // busy -> held
|
|
const c = h.click('t', true); // opposite of held -> pair cancel b+c
|
|
assert.equal(h.wire.length, 1);
|
|
const d = h.click('t', false); // busy -> held again
|
|
const e = h.click('t', false); // same as held -> shares
|
|
assert.deepEqual(h.lanes.stats, { sent: 1, held: 2, shared: 1, paired: 1, synthesized: 0, cancelled: 0 });
|
|
h.wire[0]!.resolve('ok');
|
|
assert.deepEqual(await a, { kind: 'wire', want: true, n: 0 });
|
|
assert.deepEqual(await b, { kind: 'synthetic', want: false });
|
|
assert.deepEqual(await c, { kind: 'synthetic', want: true });
|
|
await tick();
|
|
assert.equal(h.wire.length, 2); // d released exactly once; e waits behind it
|
|
assert.equal(h.wire[1]!.n, 3);
|
|
h.wire[1]!.resolve('ok');
|
|
assert.deepEqual(await d, { kind: 'wire', want: false, n: 3 });
|
|
assert.deepEqual(await e, { kind: 'synthetic', want: false });
|
|
assert.equal(h.lanes.size(), 0);
|
|
assert.equal(h.maxConcurrent, 1);
|
|
});
|
|
|
|
test('lane table: pending matching confirmed state is satisfied without a request', async () => {
|
|
const h = harness();
|
|
const a = h.click('t', true);
|
|
const b = h.click('t', true); // stale duplicate like
|
|
h.wire[0]!.resolve('ok');
|
|
await a;
|
|
assert.deepEqual(await b, { kind: 'synthetic', want: true });
|
|
assert.equal(h.wire.length, 1);
|
|
});
|
|
|
|
test('lane table: unknown outcome and rejection release the held request as-is', async () => {
|
|
const h = harness();
|
|
const a = h.click('t', true);
|
|
const b = h.click('t', true);
|
|
h.wire[0]!.resolve('unknown');
|
|
assert.deepEqual(await a, { kind: 'wire', want: true, n: -1 });
|
|
await tick();
|
|
assert.equal(h.wire.length, 2); // not synthesized: X's own behavior stands
|
|
const c = h.click('t', false);
|
|
h.wire[1]!.resolve('reject');
|
|
await assert.rejects(b, TypeError); // rejection reaches X unchanged
|
|
await tick();
|
|
assert.equal(h.wire.length, 3);
|
|
h.wire[2]!.resolve('ok');
|
|
assert.deepEqual(await c, { kind: 'wire', want: false, n: 2 });
|
|
});
|
|
|
|
test('lane table: aborting a held request rejects it and it never reaches the wire', async () => {
|
|
const h = harness();
|
|
const a = h.click('t', true);
|
|
const controller = new AbortController();
|
|
const b = h.click('t', false, controller.signal);
|
|
controller.abort();
|
|
await assert.rejects(b, (error: unknown) => (error as Error).name === 'AbortError');
|
|
h.wire[0]!.resolve('ok');
|
|
await a;
|
|
await tick();
|
|
assert.equal(h.wire.length, 1);
|
|
assert.equal(h.lanes.size(), 0);
|
|
});
|
|
|
|
test('lane table: separate targets never block each other', () => {
|
|
const h = harness();
|
|
void h.click('t1', true);
|
|
void h.click('t2', true);
|
|
void h.click('u1', false);
|
|
assert.equal(h.wire.length, 3);
|
|
});
|
|
|
|
function rng(seed: number) {
|
|
return () => { seed = (seed * 1_103_515_245 + 12_345) & 0x7fffffff; return seed / 0x7fffffff; };
|
|
}
|
|
|
|
test('property: wire ⊆ naive in click order, never concurrent, final state == last intent', async () => {
|
|
for (let seed = 1; seed <= 300; seed++) {
|
|
const random = rng(seed);
|
|
const h = harness();
|
|
const keys = ['a', 'b', 'c'];
|
|
const intent = new Map<string, boolean>(); // X's optimistic view: each click toggles
|
|
const server = new Map<string, boolean>();
|
|
const naive: { n: number; key: string; want: boolean }[] = [];
|
|
const failures = random() < 0.3;
|
|
const results: Promise<unknown>[] = [];
|
|
const done = new Set<Wire>();
|
|
const settleOne = () => {
|
|
const openWire = h.wire.filter(w => !done.has(w));
|
|
if (!openWire.length) return;
|
|
const w = openWire[Math.floor(random() * openWire.length)]!;
|
|
done.add(w);
|
|
const roll = random();
|
|
const outcome = failures && roll < 0.2 ? 'unknown' : failures && roll < 0.3 ? 'reject' : 'ok';
|
|
if (outcome === 'ok') server.set(w.key, w.want);
|
|
w.resolve(outcome);
|
|
};
|
|
for (let step = 0; step < 40; step++) {
|
|
if (random() < 0.6) {
|
|
const key = keys[Math.floor(random() * keys.length)]!;
|
|
const want = !(intent.get(key) ?? false);
|
|
intent.set(key, want);
|
|
naive.push({ n: naive.length, key, want });
|
|
results.push(h.click(key, want).catch(() => {}));
|
|
} else settleOne();
|
|
await tick();
|
|
}
|
|
while (h.wire.length > done.size) { settleOne(); await tick(); }
|
|
await Promise.all(results);
|
|
assert.equal(h.maxConcurrent <= 1, true, `seed ${seed}: concurrent`);
|
|
// Every wire request is one of X's own, sent in click order per target.
|
|
for (const key of keys) {
|
|
const sent = h.wire.filter(w => w.key === key).map(w => w.n);
|
|
assert.deepEqual(sent, [...sent].sort((x, y) => x - y), `seed ${seed}: order`);
|
|
for (const n of sent) assert.equal(naive[n]!.key, key);
|
|
for (const w of h.wire) assert.equal(naive[w.n]!.want, w.want);
|
|
if (!failures && intent.has(key) && naive.some(c => c.key === key)) {
|
|
// With no failures the confirmed server state equals the last click intent.
|
|
assert.equal(server.get(key) ?? false, intent.get(key), `seed ${seed}: final ${key}`);
|
|
}
|
|
}
|
|
assert.equal(new Set(h.wire.map(w => w.n)).size, h.wire.length, `seed ${seed}: sent twice`);
|
|
assert.ok(h.wire.length <= naive.length);
|
|
assert.equal(h.lanes.size(), 0, `seed ${seed}: lanes drained`);
|
|
}
|
|
});
|
|
|
|
// ---------- fetch gate ----------
|
|
|
|
function fakeWindow(respond: (input: unknown, init?: RequestInit) => Promise<Response>) {
|
|
const calls: { input: unknown; init?: RequestInit }[] = [];
|
|
const original = function (input: unknown, init?: RequestInit) { calls.push({ input, init }); return respond(input, init); } as typeof fetch;
|
|
return { win: { fetch: original, location: { href: BASE } }, original, calls };
|
|
}
|
|
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
|
|
test('gate: unmatched requests return the original promise; rejections pass through', async () => {
|
|
const failure = Promise.reject(new TypeError('Failed to fetch'));
|
|
failure.catch(() => {});
|
|
const { win, calls } = fakeWindow((input, init) => String(input).includes('fail') || String(init?.body).includes('666') ? failure : Promise.resolve(json({})));
|
|
const gate = installMutationGate(win);
|
|
assert.equal(gate.installed, true);
|
|
assert.equal(win.fetch('/i/api/fail'), failure);
|
|
await assert.rejects(win.fetch('/i/api/graphql/x/FavoriteTweet', post(likeBody('666'))));
|
|
assert.equal(calls.length, 2);
|
|
gate.dispose();
|
|
});
|
|
|
|
test('gate: burst like/unlike/like on one tweet sends one request and resolves every caller', async () => {
|
|
let release!: () => void;
|
|
const gateOpen = new Promise<void>(resolve => { release = resolve; });
|
|
const { win, calls, original } = fakeWindow(async () => { await gateOpen; return json({ data: { favorite_tweet: 'Done' } }); });
|
|
const gate = installMutationGate(win);
|
|
const url = '/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet';
|
|
const unurl = '/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet';
|
|
const a = win.fetch(url, post(likeBody('5')));
|
|
const b = win.fetch(unurl, post(likeBody('5')));
|
|
const c = win.fetch(url, post(likeBody('5')));
|
|
assert.equal(calls.length, 1);
|
|
release();
|
|
assert.deepEqual(await (await a).json(), { data: { favorite_tweet: 'Done' } });
|
|
assert.deepEqual(await (await b).json(), { data: { unfavorite_tweet: 'Done' } });
|
|
assert.deepEqual(await (await c).json(), { data: { favorite_tweet: 'Done' } });
|
|
assert.equal(calls.length, 1);
|
|
gate.dispose();
|
|
assert.equal(win.fetch, original);
|
|
});
|
|
|
|
test('gate: kill switch leaves a captured wrapper as pure pass-through', async () => {
|
|
const { win, calls } = fakeWindow(() => new Promise(() => {}));
|
|
const gate = installMutationGate(win);
|
|
const captured = win.fetch;
|
|
const wrapped = { fetch: captured }; // e.g. another wrapper installed on top
|
|
gate.dispose();
|
|
void wrapped.fetch('/i/api/graphql/x/FavoriteTweet', post(likeBody('1')));
|
|
void wrapped.fetch('/i/api/graphql/x/FavoriteTweet', post(likeBody('1')));
|
|
assert.equal(calls.length, 2);
|
|
});
|
|
|
|
test('gate: composes with an earlier fetch wrapper instead of replacing it', async () => {
|
|
const { win, original } = fakeWindow(() => Promise.resolve(json({ data: { favorite_tweet: 'Done' } })));
|
|
const seen: unknown[] = [];
|
|
const observer = function (this: unknown, ...args: Parameters<typeof fetch>) { seen.push(args[0]); return Reflect.apply(original, this, args); } as typeof fetch;
|
|
win.fetch = observer;
|
|
const gate = installMutationGate(win);
|
|
await win.fetch('/i/api/graphql/x/FavoriteTweet', post(likeBody('1')));
|
|
assert.equal(seen.length, 1);
|
|
gate.dispose();
|
|
assert.equal(win.fetch, observer);
|
|
});
|
|
|
|
// ---------- click guard: see tests/clickGuard.test.ts (held clicks are delivered, never dropped) ----------
|
|
|
|
// ---------- XHR transport (X's logged-in client sends these mutations with XMLHttpRequest) ----------
|
|
|
|
type XhrWire = { name: string; id: string };
|
|
function xhrEnv(liked: boolean) {
|
|
const server = { liked, wire: [] as XhrWire[], inflight: new Map<string, number>(), concurrent: false };
|
|
const replies: (() => void)[] = [];
|
|
class FakeXHR extends EventTarget {
|
|
#s = { readyState: 0, status: 0, text: '', method: '', url: '' };
|
|
responseType: XMLHttpRequestResponseType = '';
|
|
get readyState() { return this.#s.readyState; }
|
|
get status() { return this.#s.status; }
|
|
get responseText() { return this.#s.text; }
|
|
get response() { return this.#s.text; }
|
|
get responseURL() { return this.#s.readyState === 4 ? `https://x.com${this.#s.url}` : ''; }
|
|
getResponseHeader() { return 'application/json'; }
|
|
getAllResponseHeaders() { return 'content-type: application/json\r\n'; }
|
|
open(method: string, url: string) { Object.assign(this.#s, { method, url, readyState: 1, status: 0, text: '' }); }
|
|
send(body: string) {
|
|
const name = this.#s.url.split('/').pop()!;
|
|
const id = String(JSON.parse(body).variables.tweet_id);
|
|
const n = (server.inflight.get(id) ?? 0) + 1;
|
|
server.inflight.set(id, n);
|
|
if (n > 1) server.concurrent = true;
|
|
server.wire.push({ name, id });
|
|
replies.push(() => {
|
|
server.inflight.set(id, server.inflight.get(id)! - 1);
|
|
const want = name === 'FavoriteTweet';
|
|
let text: string;
|
|
if (want && server.liked) text = '{"data":{},"errors":[{"code":139,"message":"already favorited"}]}';
|
|
else { server.liked = want; text = want ? '{"data":{"favorite_tweet":"Done"}}' : '{"data":{"unfavorite_tweet":"Done"}}'; }
|
|
Object.assign(this.#s, { readyState: 4, status: 200, text });
|
|
for (const type of ['readystatechange', 'load', 'loadend']) this.dispatchEvent(new Event(type));
|
|
});
|
|
}
|
|
abort() {
|
|
Object.assign(this.#s, { readyState: 4, status: 0 });
|
|
for (const type of ['readystatechange', 'abort', 'loadend']) this.dispatchEvent(new Event(type));
|
|
this.#s.readyState = 0;
|
|
}
|
|
}
|
|
const win = { fetch: (async () => json({})) as typeof fetch, location: { href: BASE }, XMLHttpRequest: FakeXHR as unknown as typeof XMLHttpRequest };
|
|
// X's click handler: sends the opposite of the current UI state and flips the UI optimistically.
|
|
let ui = liked;
|
|
const results: { xhr: XMLHttpRequest; events: string[] }[] = [];
|
|
const click = (id = '5') => {
|
|
const xhr = new win.XMLHttpRequest();
|
|
const events: string[] = [];
|
|
for (const type of ['readystatechange', 'load', 'abort', 'loadend']) xhr.addEventListener(type, () => events.push(type === 'readystatechange' ? `rs${xhr.readyState}` : type));
|
|
xhr.open('POST', `/i/api/graphql/q/${ui ? 'UnfavoriteTweet' : 'FavoriteTweet'}`);
|
|
xhr.send(likeBody(id));
|
|
ui = !ui;
|
|
results.push({ xhr, events });
|
|
return xhr;
|
|
};
|
|
return { win, server, replies, click, results, get ui() { return ui; } };
|
|
}
|
|
|
|
test('gate (XHR): 7 like/unlike clicks with out-of-order completion: wire ⊆ naive, final server == last intent', async () => {
|
|
const env = xhrEnv(true);
|
|
const gate = installMutationGate(env.win);
|
|
for (let i = 0; i < 7; i++) { env.click(); await tick(); }
|
|
// Naive X sends 7 concurrent requests; the lane sends the first and pairs the rest away.
|
|
assert.deepEqual(env.server.wire.map(w => w.name), ['UnfavoriteTweet']);
|
|
// Complete whatever is on the wire, newest first (out of order).
|
|
while (env.replies.length) { env.replies.pop()!(); await tick(); }
|
|
await tick();
|
|
assert.equal(env.server.liked, env.ui, 'server state equals the last click intent');
|
|
assert.equal(env.server.concurrent, false);
|
|
for (const { xhr, events } of env.results) {
|
|
assert.equal(xhr.readyState, 4);
|
|
assert.equal(xhr.status, 200);
|
|
assert.equal(events.filter(e => e === 'loadend').length, 1, 'every X request completes exactly once');
|
|
assert.ok(JSON.parse(xhr.responseText).data, 'with a success body of its own shape');
|
|
}
|
|
const held = env.results[1]!;
|
|
assert.deepEqual(held.events, ['rs2', 'rs3', 'rs4', 'load', 'loadend'], 'browser event order for a synthesized completion');
|
|
assert.deepEqual(JSON.parse(held.xhr.responseText), { data: { favorite_tweet: 'Done' } });
|
|
assert.equal(held.xhr.getResponseHeader('Content-Type'), 'application/json; charset=utf-8');
|
|
assert.equal(gate.lanes.size(), 0);
|
|
gate.dispose();
|
|
});
|
|
|
|
test('gate (XHR): randomized bursts keep wire ⊆ naive, never concurrent, final == last intent', async () => {
|
|
for (let seed = 1; seed <= 40; seed++) {
|
|
const random = rng(seed);
|
|
const env = xhrEnv(random() < 0.5);
|
|
const gate = installMutationGate(env.win);
|
|
const clicks = 1 + Math.floor(random() * 8);
|
|
for (let i = 0; i < clicks; i++) {
|
|
env.click();
|
|
await tick();
|
|
// Sometimes the server answers an in-flight request before the next click, in any order.
|
|
while (env.replies.length && random() < 0.4) { env.replies.splice(Math.floor(random() * env.replies.length), 1)[0]!(); await tick(); }
|
|
}
|
|
while (env.replies.length) { env.replies.splice(Math.floor(random() * env.replies.length), 1)[0]!(); await tick(); }
|
|
await tick();
|
|
assert.equal(env.server.concurrent, false, `seed ${seed}: concurrent`);
|
|
assert.ok(env.server.wire.length <= clicks, `seed ${seed}: wire ⊆ naive`);
|
|
assert.equal(env.server.liked, env.ui, `seed ${seed}: final state`);
|
|
for (const { events } of env.results) assert.equal(events.filter(e => e === 'loadend').length, 1, `seed ${seed}: one loadend each`);
|
|
assert.equal(gate.lanes.size(), 0, `seed ${seed}: lanes drained`);
|
|
gate.dispose();
|
|
}
|
|
});
|
|
|
|
test('gate (XHR): abort of a held request never reaches the wire; unmatched XHRs are untouched', async () => {
|
|
const env = xhrEnv(false);
|
|
const nativeSend = env.win.XMLHttpRequest.prototype.send;
|
|
const gate = installMutationGate(env.win);
|
|
env.click(); // Favorite: on the wire
|
|
const held = env.click(); // Unfavorite: held behind it
|
|
held.abort();
|
|
await tick();
|
|
assert.deepEqual(env.results[1]!.events, ['rs4', 'abort', 'loadend']);
|
|
assert.equal(held.readyState, 0);
|
|
env.replies.shift()!();
|
|
await tick();
|
|
assert.deepEqual(env.server.wire.map(w => w.name), ['FavoriteTweet']);
|
|
// Reuse after abort: the object behaves natively again.
|
|
held.open('POST', '/i/api/graphql/q/UnfavoriteTweet');
|
|
assert.equal(held.readyState, 1);
|
|
// A non-mutation XHR goes straight to the native send (the fake parses its body and throws on null).
|
|
const plain = new env.win.XMLHttpRequest();
|
|
plain.open('GET', '/i/api/2/badge_count/badge_count.json');
|
|
assert.throws(() => plain.send(null));
|
|
assert.notEqual(env.win.XMLHttpRequest.prototype.send, nativeSend, 'hooked while installed');
|
|
gate.dispose();
|
|
assert.equal(env.win.XMLHttpRequest.prototype.send, nativeSend, 'dispose restores the native send');
|
|
});
|
|
|
|
test('feature flag: fixLikeFollow defaults on and is one of the Fixes', () => {
|
|
assert.equal(FEATURE_DEFAULTS.fixLikeFollow, true);
|
|
// A core flag: owned by no plugin, so no plugin switch can turn it off (src/areas/table.ts).
|
|
assert.deepEqual(X_FEATURES.filter(feature => feature.area === null && feature.group === 'fixes').map(feature => feature.id),
|
|
['fixLikeFollow', 'fixScrollRestore', 'fixTimelineJump', 'fixHomeTab', 'fixNotificationBadge', 'fixNewPostsPill', 'fixMediaUpload', 'followingChronological', 'fixChatButton', 'composerRestorePrompt']);
|
|
for (const key of ['fixScrollRestore', 'fixTimelineJump', 'fixHomeTab', 'fixNotificationBadge', 'fixNewPostsPill', 'fixMediaUpload', 'followingChronological', 'fixChatButton', 'composerRestorePrompt'] as const) {
|
|
assert.equal(FEATURE_DEFAULTS[key], true, key);
|
|
}
|
|
});
|
|
|
|
// ---------- failed in-flight request: nothing unconfirmed is ever answered with a success ----------
|
|
|
|
type XResult = { kind: 'wire' | 'synthetic'; ok: boolean; n: number };
|
|
/**
|
|
* Lane harness with X's view of each caller: a wire result with ok=false (429, 5xx) or a rejection
|
|
* (network, abort) makes X revert that click's optimistic change; a success commits it. X's UI ends
|
|
* on the intent of the last committed click (redux-optimist replay of absolute `following`/`favorited`).
|
|
*/
|
|
function failHarness(initial = false) {
|
|
const lanes = createLanes<XResult>();
|
|
const wire: { n: number; want: boolean; settle(outcome: 'ok' | 'fail' | 'odd' | 'reject' | 'abort'): void }[] = [];
|
|
const clicks: { want: boolean; status: 'pending' | 'committed' | 'reverted'; how?: string }[] = [];
|
|
let server = initial;
|
|
let open = 0, maxOpen = 0;
|
|
const click = (want: boolean) => {
|
|
const n = clicks.length;
|
|
const entry: { want: boolean; status: 'pending' | 'committed' | 'reverted'; how?: string } = { want, status: 'pending' };
|
|
clicks.push(entry);
|
|
const promise = lanes.submit('user:9', {
|
|
want,
|
|
send: () => new Promise<XResult>((resolve, reject) => {
|
|
open++; maxOpen = Math.max(maxOpen, open);
|
|
wire.push({ n, want, settle: outcome => {
|
|
open--;
|
|
if (outcome === 'ok') { server = want; resolve({ kind: 'wire', ok: true, n }); }
|
|
else if (outcome === 'odd') resolve({ kind: 'wire', ok: true, n: -1 }); // a 200 the lane cannot read
|
|
else if (outcome === 'fail') resolve({ kind: 'wire', ok: false, n });
|
|
else reject(outcome === 'abort' ? new DOMException('The operation was aborted.', 'AbortError') : new TypeError('Failed to fetch'));
|
|
} });
|
|
}),
|
|
classify: (result): Promise<Settled> => Promise.resolve(result.ok && result.n >= 0 ? { outcome: 'confirmed', state: want } : { outcome: 'unknown' }),
|
|
synthesize: () => ({ kind: 'synthetic', ok: true, n }),
|
|
});
|
|
promise.then(result => { entry.status = result.ok ? 'committed' : 'reverted'; entry.how = result.kind; },
|
|
error => { entry.status = 'reverted'; entry.how = (error as Error).name; });
|
|
return promise.catch(() => undefined);
|
|
};
|
|
const ui = () => [...clicks].reverse().find(c => c.status === 'committed')?.want ?? initial;
|
|
return { lanes, wire, clicks, click, ui, get server() { return server; }, get maxOpen() { return maxOpen; } };
|
|
}
|
|
|
|
test('lanes: follow (429) + unfollow + follow while it is in flight: no synthesized success, the last follow is sent', async () => {
|
|
const h = failHarness(false);
|
|
const a = h.click(true);
|
|
const b = h.click(false); // held (after the confirm sheet)
|
|
const c = h.click(true); // pairs with the held unfollow
|
|
h.wire[0]!.settle('fail'); // HTTP 429
|
|
await Promise.all([a, b]);
|
|
await tick();
|
|
assert.equal(h.clicks[1]!.how, 'AbortError', 'the unfollow fails like an aborted request: X reverts it');
|
|
assert.equal(h.wire.length, 2, 'the latest intent goes out once, as X made it');
|
|
assert.deepEqual([h.wire[1]!.n, h.wire[1]!.want], [2, true]);
|
|
assert.equal(h.lanes.stats.synthesized, 0);
|
|
h.wire[1]!.settle('ok');
|
|
await c;
|
|
assert.equal(h.clicks[2]!.how, 'wire');
|
|
assert.equal(h.server, true);
|
|
assert.equal(h.ui(), h.server);
|
|
await tick();
|
|
assert.equal(h.lanes.size(), 0);
|
|
});
|
|
|
|
test('lanes: follow (429) + unfollow + follow, and the released follow is rate-limited too', async () => {
|
|
const h = failHarness(false);
|
|
const all = [h.click(true), h.click(false), h.click(true)];
|
|
h.wire[0]!.settle('fail');
|
|
await tick(); await tick();
|
|
h.wire[1]!.settle('fail');
|
|
await Promise.all(all);
|
|
assert.equal(h.server, false);
|
|
assert.equal(h.ui(), false, 'every optimistic change reverted: X shows the real state');
|
|
assert.equal(h.lanes.stats.synthesized, 0);
|
|
});
|
|
|
|
test('lanes: like (500) + held unlike: the unlike is sent for real, never answered by the lane', async () => {
|
|
const h = failHarness(false);
|
|
const a = h.click(true);
|
|
const b = h.click(false);
|
|
h.wire[0]!.settle('fail'); // HTTP 500
|
|
await a;
|
|
await tick();
|
|
assert.equal(h.wire.length, 2);
|
|
assert.deepEqual([h.wire[1]!.n, h.wire[1]!.want], [1, false]);
|
|
h.wire[1]!.settle('ok');
|
|
await b;
|
|
assert.equal(h.clicks[1]!.how, 'wire');
|
|
assert.equal(h.ui(), h.server);
|
|
assert.equal(h.lanes.stats.synthesized, 0);
|
|
});
|
|
|
|
test('lanes: X aborts the in-flight request: held pair fails, the latest intent is released', async () => {
|
|
const h = failHarness(true);
|
|
const all = [h.click(false), h.click(true), h.click(false)];
|
|
h.wire[0]!.settle('abort');
|
|
await tick(); await tick();
|
|
assert.equal(h.clicks[1]!.how, 'AbortError');
|
|
assert.equal(h.wire.length, 2);
|
|
assert.equal(h.wire[1]!.n, 2);
|
|
h.wire[1]!.settle('ok');
|
|
await Promise.all(all);
|
|
assert.equal(h.server, false);
|
|
assert.equal(h.ui(), false);
|
|
assert.equal(h.lanes.stats.synthesized, 0);
|
|
});
|
|
|
|
test('lanes: unknown outcome (a 200 the lane cannot read) confirms nothing', async () => {
|
|
const h = failHarness(false);
|
|
const all = [h.click(true), h.click(false), h.click(true), h.click(true)];
|
|
h.wire[0]!.settle('odd');
|
|
await tick(); await tick();
|
|
assert.equal(h.clicks[1]!.how, 'AbortError');
|
|
assert.equal(h.wire.length, 2, 'one follow of the trailing run is released, the other waits behind it');
|
|
assert.equal(h.wire[1]!.n, 2);
|
|
h.wire[1]!.settle('ok');
|
|
await Promise.all(all);
|
|
assert.equal(h.clicks[3]!.how, 'synthetic', 'the stale duplicate is satisfied by the confirmed follow');
|
|
assert.equal(h.wire.length, 2);
|
|
assert.equal(h.ui(), true);
|
|
assert.equal(h.server, true);
|
|
});
|
|
|
|
test('lanes: a held click in the same direction as the in-flight one no longer swallows a later reversal', async () => {
|
|
const h = failHarness(false);
|
|
const all = [h.click(true), h.click(true), h.click(false)]; // stale double follow, then unfollow
|
|
h.wire[0]!.settle('ok');
|
|
await tick(); await tick();
|
|
assert.equal(h.clicks[1]!.how, 'synthetic', 'already satisfied by the confirmed follow');
|
|
assert.equal(h.wire.length, 2, 'the unfollow is sent');
|
|
h.wire[1]!.settle('ok');
|
|
await Promise.all(all);
|
|
assert.equal(h.server, false);
|
|
assert.equal(h.ui(), false);
|
|
});
|
|
|
|
test('property: with failures the final UI equals the server; each request sent once, in click order, never concurrent', async () => {
|
|
for (let seed = 1; seed <= 400; seed++) {
|
|
const random = rng(seed);
|
|
const h = failHarness(random() < 0.5);
|
|
const results: Promise<unknown>[] = [];
|
|
let intent = h.ui();
|
|
const settled = new Set<number>();
|
|
const settleOne = () => {
|
|
const index = h.wire.findIndex((_, i) => !settled.has(i));
|
|
if (index < 0) return;
|
|
settled.add(index);
|
|
const roll = random();
|
|
h.wire[index]!.settle(roll < 0.2 ? 'fail' : roll < 0.3 ? 'reject' : roll < 0.35 ? 'abort' : 'ok');
|
|
};
|
|
for (let step = 0; step < 30; step++) {
|
|
if (random() < 0.6) { intent = !intent; results.push(h.click(intent)); }
|
|
else settleOne();
|
|
await tick();
|
|
}
|
|
while (h.wire.length > settled.size) { settleOne(); await tick(); }
|
|
await Promise.all(results);
|
|
assert.equal(h.ui(), h.server, `seed ${seed}: UI differs from the server`);
|
|
assert.ok(h.maxOpen <= 1, `seed ${seed}: concurrent`);
|
|
const ns = h.wire.map(w => w.n);
|
|
assert.deepEqual(ns, [...ns].sort((x, y) => x - y), `seed ${seed}: order`);
|
|
assert.equal(new Set(ns).size, ns.length, `seed ${seed}: sent twice`);
|
|
for (const w of h.wire) assert.equal(h.clicks[w.n]!.want, w.want);
|
|
assert.equal(h.lanes.size(), 0, `seed ${seed}: drained`);
|
|
}
|
|
});
|