import assert from 'node:assert/strict'; import { test } from 'node:test'; import { parseRelationshipObservations } from '../src/observer/relationships.ts'; import { installResponseObserver, isSupportedPage, MAX_RESPONSE_BYTES, MAX_OBSERVATION_READERS, MAX_OBSERVATION_READ_MS, type PageObservation } from '../src/observer/network.ts'; import { createCaptureQueue, type CaptureAck, type CapturePayload } from '../src/observer/capture-queue.ts'; import { isExcludedComposerPath } from '../src/observer/composer.ts'; const fixture = (relationship: Record = { following: true, followed_by: false }) => ({ data: { user: { result: { __typename: 'User', rest_id: '123', legacy: { screen_name: 'fixture', ...relationship }, private_text: 'must never escape', email: 'private@example.test' } } }, }); const tick = () => new Promise(resolve => setTimeout(resolve, 15)); function deferred() { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function fakePage(fetch: typeof globalThis.fetch) { class MockXHR extends EventTarget { status = 200; responseType = ''; responseText = JSON.stringify(fixture()); responseURL = 'https://x.com/i/api/graphql/test/UserByScreenName'; response: unknown = null; sent = 0; open(..._args: unknown[]) {} send(..._args: unknown[]) { this.sent++; } getResponseHeader(name: string) { return name.toLowerCase() === 'content-type' ? 'application/json' : String(this.responseText.length); } } const page = Object.assign(new EventTarget(), { fetch, XMLHttpRequest: MockXHR, CustomEvent, location: { origin: 'https://x.com', href: 'https://x.com/home' }, top: null as unknown, }); page.top = page; return page as unknown as Window & typeof globalThis; } const apiUrl = 'https://x.com/i/api/graphql/test/UserByScreenName'; const jsonResponse = (body: unknown) => new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' } }); test('relationship extraction emits only IDs and explicitly observed booleans', () => { assert.deepEqual(parseRelationshipObservations(fixture()), [{ userId: '123', following: true, followedBy: false }]); assert.deepEqual(parseRelationshipObservations(fixture({ following: false })), [{ userId: '123', following: false, followedBy: null }]); assert.deepEqual(parseRelationshipObservations(fixture({ following: 'false' })), []); assert.deepEqual(parseRelationshipObservations({ __typename: 'Tweet', rest_id: '123', legacy: { following: true } }), []); }); test('perspectives take precedence; traversal tolerates cycles and deeply nested objects', () => { const user = { __typename: 'User', rest_id: '123', legacy: { following: true }, relationship_perspectives: { following: false, followed_by: true } }; assert.deepEqual(parseRelationshipObservations(user), [{ userId: '123', following: false, followedBy: true }]); const cycle: Record = {}; cycle.self = cycle; assert.deepEqual(parseRelationshipObservations(cycle), []); let nested: unknown = user; for (let i = 0; i < 30; i++) nested = { nested }; assert.deepEqual(parseRelationshipObservations(nested), []); }); test('only exact https x.com top frames can install; repeat installation is idempotent', () => { const page = fakePage(async () => jsonResponse(fixture())); const original = page.fetch; const handle = installResponseObserver(page); assert.equal(handle.installed, true); assert.equal(installResponseObserver(page), handle); handle.dispose(); assert.equal(page.fetch, original); Object.assign(page.location, { origin: 'https://x.com.attacker.test' }); assert.equal(isSupportedPage(page), false); assert.equal(installResponseObserver(page).installed, false); Object.assign(page.location, { origin: 'https://x.com' }); Object.assign(page, { top: {} }); assert.equal(isSupportedPage(page), false); }); test('fetch preserves Promise/Response identity, original body, call count, start epoch and redaction', async () => { const request = deferred(); let calls = 0; const page = fakePage(() => { calls++; return request.promise; }); const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); handle.advanceEpoch(); const promise = page.fetch(apiUrl); assert.equal(promise, request.promise); const response = jsonResponse(fixture()); request.resolve(response); assert.equal(await promise, response); assert.deepEqual(await response.json(), fixture()); await tick(); assert.equal(calls, 1); assert.equal(events.length, 1); assert.equal(events[0].requestEpoch, 1); assert.equal(events[0].accountId, null); assert.equal(JSON.stringify(events).includes('private'), false); assert.equal(JSON.stringify(events).includes('fixture'), false); handle.dispose(); }); test('fetch rejection and subscriber exceptions do not change X results', async () => { const failure = new Error('offline'); const page = fakePage(() => Promise.reject(failure)); const handle = installResponseObserver(page); await assert.rejects(page.fetch(apiUrl), error => error === failure); handle.dispose(); const healthy = fakePage(async () => jsonResponse(fixture())); const listener = installResponseObserver(healthy, { onObservation() { throw new Error('consumer'); } }); assert.deepEqual(await (await healthy.fetch(apiUrl)).json(), fixture()); await tick(); listener.dispose(); }); test('non-API, non-JSON, oversized and malformed payloads are ignored', async () => { const outputs = [ jsonResponse(fixture()), new Response(JSON.stringify(fixture()), { headers: { 'content-type': 'text/html' } }), new Response(JSON.stringify(fixture()), { headers: { 'content-type': 'application/json', 'content-length': String(MAX_RESPONSE_BYTES + 1) } }), jsonResponse({ padding: 'x'.repeat(MAX_RESPONSE_BYTES), ...fixture() }), new Response('{invalid', { headers: { 'content-type': 'application/json' } }), ]; const page = fakePage(async () => outputs.shift()!); const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); await page.fetch('https://example.test/i/api/graphql/test'); for (let i = 0; i < 4; i++) await (await page.fetch(apiUrl)).text(); await tick(); assert.deepEqual(events, []); handle.dispose(); }); test('XHR only observes existing sends and stamps the epoch at send time', () => { const page = fakePage(async () => jsonResponse({})); const originalOpen = page.XMLHttpRequest.prototype.open; const originalSend = page.XMLHttpRequest.prototype.send; const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); const request = new page.XMLHttpRequest(); request.open('GET', apiUrl); handle.advanceEpoch(); request.send(); request.dispatchEvent(new Event('load')); request.dispatchEvent(new Event('loadend')); assert.equal((request as unknown as { sent: number }).sent, 1); assert.equal(events.length, 1); assert.equal(events[0].requestEpoch, 1); handle.dispose(); assert.equal(page.XMLHttpRequest.prototype.open, originalOpen); assert.equal(page.XMLHttpRequest.prototype.send, originalSend); }); test('disposing removes in-flight XHR listeners and suppresses late fetch observations', async () => { const request = deferred(); const page = fakePage(() => request.promise); const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); const promise = page.fetch(apiUrl); const xhr = new page.XMLHttpRequest(); xhr.open('GET', apiUrl); xhr.send(); handle.dispose(); request.resolve(jsonResponse(fixture())); await promise; xhr.dispatchEvent(new Event('load')); await tick(); assert.deepEqual(events, []); }); test('responses from a previous account epoch are discarded', async () => { const request = deferred(); const page = fakePage(() => request.promise); const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); const result = page.fetch(apiUrl); const xhr = new page.XMLHttpRequest(); xhr.open('GET', apiUrl); xhr.send(); handle.advanceEpoch(); request.resolve(jsonResponse(fixture())); await (await result).text(); xhr.dispatchEvent(new Event('load')); await tick(); assert.deepEqual(events, []); handle.dispose(); }); test('a stalled observation times out without cancelling the original response consumer', async () => { let stream!: ReadableStreamDefaultController; let cancelled = false; const body = new ReadableStream({ start(controller) { stream = controller; }, cancel() { cancelled = true; } }); const response = new Response(body, { headers: { 'content-type': 'application/json' } }); const page = fakePage(async () => response); const events: PageObservation[] = []; const handle = installResponseObserver(page, { onObservation: event => events.push(event) }); const original = await page.fetch(apiUrl); const consumed = original.json(); await new Promise(resolve => setTimeout(resolve, MAX_OBSERVATION_READ_MS + 80)); assert.equal(cancelled, false); stream.enqueue(new TextEncoder().encode(JSON.stringify(fixture()))); stream.close(); assert.deepEqual(await consumed, fixture()); await tick(); assert.deepEqual(events, []); handle.dispose(); }); test('in-flight observation readers are capped and disposal preserves each original stream', async () => { const streams: ReadableStreamDefaultController[] = []; let clones = 0; const page = fakePage(async () => { const response = new Response(new ReadableStream({ start(controller) { streams.push(controller); } }), { headers: { 'content-type': 'application/json' } }); const clone = response.clone.bind(response); response.clone = () => { clones++; return clone(); }; return response; }); const handle = installResponseObserver(page); const responses = await Promise.all(Array.from({ length: MAX_OBSERVATION_READERS + 3 }, () => page.fetch(apiUrl))); await tick(); assert.equal(clones, MAX_OBSERVATION_READERS); handle.dispose(); for (const stream of streams) { stream.enqueue(new TextEncoder().encode('{"ok":true}')); stream.close(); } assert.deepEqual(await Promise.all(responses.map(response => response.json())), responses.map(() => ({ ok: true }))); }); test('capture queue serializes writes, retains the latest text, and waits for native ACK', async () => { const calls: CapturePayload[] = []; const first = deferred(); const queue = createCaptureQueue({ composerId: 'fixture-composer', save: async capture => { calls.push(capture); return capture.sequence === 1 ? first.promise : { sequence: capture.sequence, revision: capture.sequence }; } }); const snapshot = { text: 'first', kind: 'post' as const, contextUrl: 'https://x.com/home' }; queue.enqueue(snapshot); await Promise.resolve(); assert.equal(queue.getStatus(), 'saving'); queue.enqueue({ ...snapshot, text: 'middle' }); queue.enqueue({ ...snapshot, text: 'latest' }); assert.equal(calls.length, 1); assert.notEqual(queue.getStatus(), 'saved'); first.resolve({ sequence: 1, revision: 1 }); await queue.flush(); assert.deepEqual(calls.map(c => c.text), ['first', 'latest']); assert.deepEqual(calls.map(c => c.sequence), [1, 2]); assert.equal(queue.getStatus(), 'saved'); queue.enqueue({ ...snapshot, text: '' }); await queue.flush(); assert.equal(calls[2].text, ''); assert.equal(calls[2].sequence, 3); }); test('ambiguous failed saves retry the exact payload/sequence before new text', async () => { const calls: CapturePayload[] = []; let attempts = 0; const queue = createCaptureQueue({ composerId: 'fixture-composer', save: async capture => { calls.push({ ...capture }); if (attempts++ === 0) throw new Error('unknown IPC outcome'); return { sequence: capture.sequence, revision: capture.sequence }; } }); const snapshot = { text: 'persist me', kind: 'reply' as const, contextUrl: 'https://x.com/example/status/123' }; queue.enqueue(snapshot); await queue.flush(); assert.equal(queue.getStatus(), 'error'); queue.enqueue({ ...snapshot, text: 'newer edit' }); await queue.flush(); assert.deepEqual(calls[0], calls[1]); assert.equal(calls[2].text, 'newer edit'); assert.equal(calls[2].sequence, 2); assert.equal(queue.getStatus(), 'saved'); }); test('incorrect native ACK never produces a saved state', async () => { const statuses: string[] = []; const queue = createCaptureQueue({ composerId: 'fixture-composer', onStatus: status => statuses.push(status), save: async () => ({ sequence: 999, revision: 1 }) }); queue.enqueue({ text: 'text', kind: 'quote', contextUrl: 'https://x.com/home' }); await queue.flush(); assert.equal(queue.getStatus(), 'error'); assert.equal(statuses.includes('saved'), false); }); test('DM/chat and Grok routes are excluded without matching unrelated profile names', () => { for (const path of ['/messages', '/messages/123', '/i/chat', '/i/chat/123', '/i/grok']) assert.equal(isExcludedComposerPath(path), true); for (const path of ['/home', '/messagesapp', '/someone/status/123']) assert.equal(isExcludedComposerPath(path), false); });