import assert from 'node:assert/strict'; import { test } from 'node:test'; import { extractUsers, installUserInfo, LruMap, parseCreatedAt } from '../src/inject/userInfo.ts'; import { createPluginApi } from '../src/inject/pluginApi.ts'; const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }); // Tap delivery (clone().text().then(...)) runs off the fetch promise itself, so awaiting the fetch // call is not enough; give it a tick (same pattern as tests/hubs.test.ts). const tick = () => new Promise(resolve => setTimeout(resolve, 15)); /** Shaped like an X GraphQL `UserResults` node (fixture-tested only; see userInfo.ts). */ const userNode = (overrides: Record = {}) => ({ __typename: 'User', rest_id: '111', is_blue_verified: true, verified_type: 'None', legacy: { screen_name: 'someone', following: null }, relationship_perspectives: { following: true }, ...overrides, }); test('extractUsers reads blue check, org verification and following from a GraphQL user node', () => { const [user] = extractUsers({ data: { user: { result: userNode() } } }); assert.deepEqual(user, { xUserId: '111', handle: 'someone', blue: true, verifiedType: 'None', following: true, followedBy: null, createdAt: null }); }); test('extractUsers reports Business/Government verification and leaves absent fields null (never false)', () => { const [business] = extractUsers({ u: userNode({ rest_id: '2', verified_type: 'Business', is_blue_verified: false, relationship_perspectives: undefined, legacy: { screen_name: 'biz' } }) }); assert.deepEqual(business, { xUserId: '2', handle: 'biz', blue: false, verifiedType: 'Business', following: null, followedBy: null, createdAt: null }); const [unseen] = extractUsers({ u: userNode({ rest_id: '3', is_blue_verified: undefined, verified_type: undefined, legacy: { screen_name: 'plain' }, relationship_perspectives: undefined }) }); assert.deepEqual(unseen, { xUserId: '3', handle: 'plain', blue: null, verifiedType: null, following: null, followedBy: null, createdAt: null }); }); test('extractUsers ignores nodes without a valid rest_id/screen_name pair and dedupes by rest_id', () => { const noHandle = userNode({ legacy: { following: true } }); delete (noHandle as Record).legacy; assert.deepEqual(extractUsers({ a: { legacy: { screen_name: 'nope' } }, b: noHandle }), []); const dup = extractUsers({ a: userNode(), b: { nested: userNode({ is_blue_verified: false }) } }); assert.equal(dup.length, 1); }); test('installUserInfo caches per handle (case-insensitively) from tapped responses', async () => { const target = { fetch: (async () => json({ data: { user: { result: userNode() } } })) as unknown as typeof fetch }; const users = installUserInfo(target); assert.equal(users.get('someone'), null, 'nothing observed yet'); await target.fetch('https://x.com/i/api/graphql/abc/UserByScreenName?x=1'); await tick(); assert.deepEqual(users.get('SOMEONE'), { xUserId: '111', handle: 'someone', blue: true, verifiedType: 'None', following: true, followedBy: null, createdAt: null }); users.dispose(); }); test('installUserInfo merges a later response over the earlier one without erasing known fields', async () => { let body: unknown = { data: { user: { result: userNode() } } }; const target = { fetch: (async () => json(body)) as unknown as typeof fetch }; const users = installUserInfo(target); users.get('someone'); // first use installs the tap await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); await tick(); assert.equal(users.get('someone')?.following, true); // A later timeline entry that omits relationship_perspectives must not turn a known `following: // true` back into unknown. body = { data: { user: { result: userNode({ relationship_perspectives: undefined, legacy: { screen_name: 'someone' } }) } } }; await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); await tick(); assert.equal(users.get('someone')?.following, true, 'known value is not erased by a response missing the field'); users.dispose(); }); test('extractUsers reads X current user objects (core/verification, no legacy) as seen live on 2026-09-28', () => { const live = { __typename: 'User', rest_id: '444', is_blue_verified: true, core: { created_at: 'Mon Jan 01 00:00:00 +0000 2024', name: 'Someone', screen_name: 'live_shape' }, verification: { verified: false }, relationship_perspectives: { following: false, followed_by: false, blocking: false, muting: false }, }; const [user] = extractUsers({ data: { home: { instructions: [{ entries: [{ content: { user_results: { result: live } } }] }] } } }); assert.deepEqual(user, { xUserId: '444', handle: 'live_shape', blue: true, verifiedType: 'None', following: false, followedBy: false, createdAt: Date.UTC(2024, 0, 1) }); const [org] = extractUsers({ u: { ...live, rest_id: '445', core: { screen_name: 'org_shape' }, verification: { verified: true, verified_type: 'Business' } } }); assert.equal(org?.verifiedType, 'Business'); }); test('parseCreatedAt reads the two shapes X sends and nothing else (absent or odd = unknown)', () => { const now = Date.UTC(2026, 8, 30); assert.equal(parseCreatedAt('Mon Jan 01 00:00:00 +0000 2024', now), Date.UTC(2024, 0, 1)); assert.equal(parseCreatedAt('Tue Mar 21 20:50:14 +0000 2006', now), Date.UTC(2006, 2, 21, 20, 50, 14)); assert.equal(parseCreatedAt('Sat Jun 06 01:30:00 +0530 2015', now), Date.UTC(2015, 5, 5, 20, 0, 0), 'an offset is applied'); assert.equal(parseCreatedAt('2019-05-04T03:02:01.000Z', now), Date.UTC(2019, 4, 4, 3, 2, 1)); for (const odd of [undefined, null, 1704067200000, '', 'March 2015', 'Joined March 2015', '2015', 'Mon Foo 01 00:00:00 +0000 2024', '2019-05-04', 'Mon Jan 01 00:00:00 +0000 1999', 'Mon Jan 01 00:00:00 +0000 2031']) { assert.equal(parseCreatedAt(odd, now), null, String(odd)); } }); test('extractUsers reads followed_by and created_at where X sent them, and a later response never erases them', async () => { const legacy = userNode({ legacy: { screen_name: 'someone', created_at: 'Sat Jun 06 01:30:00 +0000 2015', followed_by: true } }); const [old] = extractUsers({ u: legacy }); assert.deepEqual([old!.followedBy, old!.createdAt], [true, Date.UTC(2015, 5, 6, 1, 30)]); // The perspective wins over the legacy field, and "not followed by" is a value, not a missing one. const [both] = extractUsers({ u: userNode({ relationship_perspectives: { following: true, followed_by: false }, legacy: { screen_name: 'someone', followed_by: true } }) }); assert.equal(both!.followedBy, false); let body: unknown = { u: legacy }; const target = { fetch: (async () => json(body)) as unknown as typeof fetch }; const users = installUserInfo(target); await target.fetch('https://x.com/i/api/graphql/abc/UserByScreenName'); await tick(); body = { u: userNode() }; await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); await tick(); assert.deepEqual([users.get('someone')?.followedBy, users.get('someone')?.createdAt], [true, Date.UTC(2015, 5, 6, 1, 30)]); users.dispose(); }); test('installUserInfo taps at install, so the first timeline (loaded before any plugin asks) is seen', async () => { const original = (async () => json({ data: { user: { result: userNode() } } })) as unknown as typeof fetch; const target = { fetch: original }; const users = installUserInfo(target); assert.notEqual(target.fetch, original, 'fetch is wrapped at install'); await target.fetch('https://x.com/i/api/graphql/abc/UserByScreenName'); await tick(); assert.equal(users.get('someone')?.blue, true, 'a response before any plugin asked is known'); const seen: string[][] = []; const off = users.onUpdate(handles => seen.push(handles)); await target.fetch('https://x.com/i/api/graphql/abc/UserByScreenName'); await tick(); assert.deepEqual(seen, [['someone']], 'listeners get the lowercase handles a response added'); off(); await target.fetch('https://x.com/i/api/graphql/abc/UserByScreenName'); await tick(); assert.equal(seen.length, 1, 'a disposed listener is not called again'); users.dispose(); assert.equal(target.fetch, original, 'dispose restores fetch'); }); test('LruMap evicts the least recently used entry; get and set refresh recency', () => { const lru = new LruMap(2); lru.set('a', 1); lru.set('b', 2); assert.equal(lru.get('a'), 1, 'reading a makes it the newest'); lru.set('c', 3); assert.deepEqual([...lru.map.keys()], ['a', 'c'], 'b (least recently used) is evicted, not a (oldest inserted)'); lru.set('a', 10); lru.set('d', 4); assert.deepEqual([...lru.map.keys()], ['a', 'd'], 'updating a refreshed it; c was evicted'); assert.equal(lru.get('missing'), undefined); }); test('installUserInfo cache keeps recently read users when full', async () => { let body: unknown = { data: { user: { result: userNode() } } }; const target = { fetch: (async () => json(body)) as unknown as typeof fetch }; const users = installUserInfo(target, 2); users.get('someone'); await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); body = { u: userNode({ rest_id: '2', legacy: { screen_name: 'second' } }) }; await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); await tick(); assert.ok(users.get('someone'), 'read: someone becomes most recently used'); body = { u: userNode({ rest_id: '3', legacy: { screen_name: 'third' } }) }; await target.fetch('https://x.com/i/api/graphql/abc/HomeTimeline'); await tick(); assert.ok(users.get('someone') && users.get('third'), 'recently read and newest are kept'); assert.equal(users.get('second'), null, 'the least recently used user is evicted'); users.dispose(); }); test('plugin api and api.users are frozen', () => { const win = { document: { querySelector: () => null }, location: { href: 'https://x.com/' } } as unknown as Window & typeof globalThis; const api = createPluginApi(win); assert.ok(Object.isFrozen(api) && Object.isFrozen(api.users)); assert.throws(() => { (api as unknown as Record).tweet = null; }, TypeError); assert.throws(() => { (api.users as unknown as Record).get = null; }, TypeError); assert.equal(api.users.get('x'), null); assert.equal(typeof api.users.onUpdate(() => {}), 'function'); });