Ctrl K
ComunidadGuías…

__tests__/session-ring.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
 * A SESSION THAT HITS A WALL MOVES TO A DEVICE.
 *
 * Measured 2026-10-30: through the BIAB connector, bol.com answered 403, Google Trends 429 and CJ a
 * "verify you are human" page — all because a session runs on the cluster's datacenter address,
 * while the owner's phone sat connected on a home line the whole time. The ring took jobs, never
 * sessions. These tests hold the handover to its promise with a stubbed phone.
 */
import { describe, it, expect } from 'vitest';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const { createSessionRing, signInPage, looksWalled, unwrap, toElements, regionScriptFor } = require('../src/sessionRing');

function phone() {
  const calls = [];
  const answers = {
    '/v1/navigate': { url: 'https://www.bol.com/nl/' },
    '/v1/info': JSON.stringify({ url: 'https://www.bol.com/nl/', title: 'bol.com | de winkel van ons allemaal' }),
    '/v1/content': JSON.stringify('Bestsellers\nWekkerlicht €39,99'),
    '/v1/analyze': JSON.stringify([{ i: 0, tag: 'a', text: 'Bestsellers', href: '/bestsellers' }, { i: 1, tag: 'input', label: 'Zoeken' }]),
    '/v1/click': '"ok"', '/v1/type': '"ok"', '/v1/scroll': '"ok"',
    '/v1/screenshot': { png_base64: Buffer.from('png').toString('base64') },
  };
  // The hub hands back the device's own POST body: { id, result: <string> }.
  const run = async (deviceId, spec) => { calls.push({ deviceId, ...spec }); return { id: 'c1', result: { id: 'c1', result: answers[spec.path] } }; };
  return { run, calls };
}
const device = { deviceId: 'd1', name: 'SM-F926B' };

describe('telling a wall from a page', () => {
  it('reads refusals and challenges as walls', () => {
    expect(looksWalled({ status: 403 })).toMatch(/403/);
    expect(looksWalled({ status: 429 })).toMatch(/429/);
    expect(looksWalled({ status: 200, title: 'Just a moment...' })).toMatch(/challenge/);
    expect(looksWalled({ status: 200, sample: 'Please verify you are a human to continue' })).toMatch(/challenge/);
  });
  it('does not mistake a real page for one', () => {
    expect(looksWalled({ status: 200, title: 'Wekkerlicht kopen', sample: 'x'.repeat(5000) })).toBe('');
    expect(looksWalled({ status: 200, title: 'Blog', sample: 'How captcha works. ' + 'y'.repeat(5000) })).toBe('');
    expect(looksWalled({ status: 404 })).toBe('');
  });
});

describe('the phone answers in wrappers', () => {
  it('unwraps the hub and device envelopes and JSON strings', () => {
    expect(unwrap({ id: 'c', result: { id: 'c', result: '{"a":1}' } })).toEqual({ a: 1 });
    expect(unwrap({ id: 'c', result: '"ok"' })).toBe('ok');
    expect(() => unwrap({ id: 'c', error: 'no webview' })).toThrow(/no webview/);
  });
  it('numbers the phone list the way the cluster does', () => {
    const els = toElements([{ i: 0, tag: 'a', text: 'A' }, { i: 1, tag: 'button', text: 'B' }]);
    expect(els.map((e) => e.index)).toEqual([1, 2]);
  });
});

describe('a session on the device', () => {
  it('attaches only when a device with a home connection is online', () => {
    const ring = createSessionRing({ run: phone().run, pickDevice: () => null, log: {} });
    const s = { id: 's1' };
    expect(ring.attach(s, ['me'])).toBe(null);
    expect(s.ring).toBeUndefined();
  });
  it('navigates, reads, analyzes and clicks on the phone, with the cluster numbering', async () => {
    const p = phone();
    const ring = createSessionRing({ run: p.run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's1' };
    ring.attach(s, ['me']);
    const nav = await ring.navigate(s, 'https://www.bol.com/nl/');
    expect(nav).toMatchObject({ url: 'https://www.bol.com/nl/', title: expect.stringMatching(/bol\.com/), onDevice: 'SM-F926B' });
    expect((await ring.content(s)).content).toMatch(/Wekkerlicht/);
    const a = await ring.analyze(s);
    expect(a.elements[0]).toMatchObject({ index: 1, text: 'Bestsellers' });
    await ring.click(s, { index: 2 });
    expect(p.calls.find((c) => c.path === '/v1/click').body).toEqual({ index: 1 });
    await ring.type(s, { index: 2, text: 'wekkerlicht' });
    expect(p.calls.find((c) => c.path === '/v1/type').body).toEqual({ index: 1, text: 'wekkerlicht' });
    expect((await ring.screenshot(s)).data.toString()).toBe('png');
    expect(p.calls.every((c) => c.deviceId === 'd1')).toBe(true);
  });
  /* 2026-11-10: on Google's profile editor (a form in a frame) the top-page Enter script typed into
     Google's own search box. A phone with keys gets a real Enter into what has focus. */
  it('submits with a real Enter on the phone, and falls back to the script on an older app', async () => {
    const p = phone();
    const ring = createSessionRing({ run: p.run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's1' }; ring.attach(s, ['me']);
    await ring.type(s, { index: 1, text: 'Softwarebedrijf', submit: true });
    expect(p.calls.find((c) => c.path === '/v1/key').body).toEqual({ key: 'Enter' });
    expect(p.calls.some((c) => c.path === '/v1/eval' && /requestSubmit/.test(c.body.code))).toBe(false);

    const old = [];
    const oldRun = async (id, spec) => { old.push(spec); return { result: { result: spec.path === '/v1/key' ? '{"error":"unknown path"}' : '"ok"' } }; };
    const ring2 = createSessionRing({ run: oldRun, pickDevice: () => device, log: {}, wait: async () => {} });
    const s2 = { id: 's2' }; ring2.attach(s2, ['me']);
    await ring2.type(s2, { index: 1, text: 'wekker', submit: true });
    expect(old.some((c) => c.path === '/v1/eval' && /requestSubmit/.test(c.body.code))).toBe(true);
  });
  it('presses one key on the phone', async () => {
    const p = phone();
    const ring = createSessionRing({ run: p.run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's1' }; ring.attach(s, ['me']);
    const r = await ring.key(s, 'ArrowDown');
    expect(p.calls.find((c) => c.path === '/v1/key').body).toEqual({ key: 'ArrowDown' });
    expect(r).toMatchObject({ pressed: 'ArrowDown', onDevice: 'SM-F926B' });
  });
  it('remembers a host that walled the server, for a while', () => {
    let t = 1000;
    const ring = createSessionRing({ run: phone().run, pickDevice: () => device, log: {}, now: () => t });
    ring.remember('https://www.wholesale-shop.example/nl/s/?q=x', 'HTTP 403');
    expect(ring.knownWall('https://wholesale-shop.example/nl/')).toBe('HTTP 403');
    t += 7 * 60 * 60 * 1000;
    expect(ring.knownWall('https://wholesale-shop.example/nl/')).toBe('');
  });
});

describe('working the page like a person', () => {
  it('accepts the cookie banner after loading a page, and says so', async () => {
    const p = phone();
    const run = async (id, spec) => (spec.path === '/v1/eval'
      ? { result: { result: JSON.stringify('Akceptuj pliki cookie') } }
      : p.run(id, spec));
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's2' }; ring.attach(s, ['me']);
    const nav = await ring.navigate(s, 'https://www.bol.com/nl/');
    expect(nav.acceptedCookies).toBe('Akceptuj pliki cookie');
  });
  it('sets AliExpress to the shop market and loads the page again with it', async () => {
    const calls = [];
    const run = async (id, spec) => {
      calls.push(spec);
      if (spec.path === '/v1/eval') return { result: { result: JSON.stringify(/aep_usuc_f/.test(spec.body.code) ? 'set' : '') } };
      return { result: { result: JSON.stringify({ url: 'https://nl.aliexpress.com/w/x.html', title: 'x' }) } };
    };
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's3' }; ring.attach(s, ['me']);
    await ring.navigate(s, 'https://www.aliexpress.com/w/wholesale-derma-roller.html');
    expect(calls.filter((c) => c.path === '/v1/navigate')).toHaveLength(2);
    expect(calls.find((c) => /aep_usuc_f/.test(c.body.code || '')).body.code).toMatch(/region=NL/);
  });
  it('leaves other sites alone', () => {
    expect(regionScriptFor('https://www.bol.com/nl/')).toBe('');
    expect(regionScriptFor('https://nl.aliexpress.com/item/1.html')).toMatch(/aep_usuc_f/);
  });
});

describe('marketplaces go to the device first', () => {
  it('sends AliExpress, bol.com and Amazon to the device even when the server is let in', () => {
    const ring = createSessionRing({ run: phone().run, pickDevice: () => device, log: {} });
    expect(ring.knownWall('https://www.aliexpress.com/w/x.html')).toMatch(/home connection/);
    expect(ring.knownWall('https://nl.aliexpress.com/item/1.html')).toMatch(/home connection/);
    expect(ring.knownWall('https://www.bol.com/nl/')).toMatch(/home connection/);
    expect(ring.knownWall('https://www.amazon.nl/s?k=x')).toMatch(/home connection/);
    expect(ring.knownWall('https://example.com/')).toBe('');
  });
});

describe('a page that never finishes loading on the device', () => {
  it('says the app must be open instead of returning an empty page', async () => {
    let t = 0;
    const run = async (id, spec) => (spec.path === '/v1/info'
      ? { result: { result: JSON.stringify({ url: 'https://nl.aliexpress.com/w/x.html', title: '', ready: 'loading' }) } }
      : { result: { result: '""' } });
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, now: () => t, wait: async (ms) => { t += ms; } });
    const s = { id: 's9' }; ring.attach(s, ['me']);
    const nav = await ring.navigate(s, 'https://www.aliexpress.com/w/x.html');
    expect(nav.notice).toMatch(/Still loading on SM-F926B/);
    const a = await ring.analyze(s);
    expect(a.elementCount).toBe(0);
    expect(a.notice).toMatch(/still loading/);
  });
});

describe('the app on the device is closed', () => {
  it('says the app must be opened instead of returning an empty page', async () => {
    let t = 0;
    const run = async (id, spec) => ({ result: { result: 'null' } });
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, now: () => t, wait: async (ms) => { t += ms; } });
    const s = { id: 's10' }; ring.attach(s, ['me']);
    const nav = await ring.navigate(s, 'https://www.bol.com/nl/');
    expect(nav.notice).toMatch(/app on SM-F926B is closed/);
    expect((await ring.content(s)).notice).toMatch(/closed/);
  });
});

describe('a phone browser nobody has drawn', () => {
  it('lists the page from the DOM when the screen scan finds next to nothing, and clicks through the page', async () => {
    const calls = [];
    const run = async (id, spec) => {
      calls.push(spec);
      if (spec.path === '/v1/analyze') return { result: { result: JSON.stringify([{ i: 0, tag: 'div', text: 'x' }]) } };
      if (spec.path === '/v1/eval' && /__ringEls=keep/.test(spec.body.code)) return { result: { result: JSON.stringify([{ tag: 'a', text: 'Haargroei serum €3,99', href: 'https://nl.aliexpress.com/item/1.html' }, { tag: 'a', text: 'b' }, { tag: 'a', text: 'c' }, { tag: 'a', text: 'd' }, { tag: 'a', text: 'e' }, { tag: 'button', text: 'Zoeken' }]) } };
      if (spec.path === '/v1/info') return { result: { result: JSON.stringify({ url: 'https://nl.aliexpress.com/w/x.html', title: 't', ready: 'complete' }) } };
      return { result: { result: '""' } };
    };
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's11' }; ring.attach(s, ['me']);
    const a = await ring.analyze(s);
    expect(a.elementCount).toBe(6);
    expect(a.elements[0]).toMatchObject({ index: 1, href: 'https://nl.aliexpress.com/item/1.html' });
    await ring.click(s, { index: 1 });
    expect(calls.some((c) => c.path === '/v1/eval' && /__ringEls\[0\]/.test(c.body.code))).toBe(true);
    expect(calls.some((c) => c.path === '/v1/click')).toBe(false);
  });
});

describe('a clicked link goes where it points', () => {
  it('opens the link when the tap left the page unchanged', async () => {
    const calls = []; let url = 'https://nl.aliexpress.com/w/x.html';
    const run = async (id, spec) => {
      calls.push(spec);
      if (spec.path === '/v1/navigate') url = spec.body.url;
      if (spec.path === '/v1/info') return { result: { result: JSON.stringify({ url, title: 't', ready: 'complete' }) } };
      if (spec.path === '/v1/analyze') return { result: { result: JSON.stringify(Array.from({ length: 6 }, (_, i) => ({ i, tag: 'a', text: 'p' + i, href: `//nl.aliexpress.com/item/${i}.html` }))) } };
      return { result: { result: '"ok"' } };
    };
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's12' }; ring.attach(s, ['me']);
    await ring.analyze(s);
    const c = await ring.click(s, { index: 3 });
    expect(c.url).toBe('https://nl.aliexpress.com/item/2.html');
  });
});

describe('a human check on the device', () => {
  it('is reported as one, never read as an empty product', async () => {
    let url = 'https://nl.aliexpress.com/w/x.html';
    const run = async (id, spec) => {
      if (spec.path === '/v1/navigate') url = 'https://nl.aliexpress.com//item/1.html/_____tmd_____/punish?x5secdata=abc';
      if (spec.path === '/v1/info') return { result: { result: JSON.stringify({ url, title: '', ready: 'complete' }) } };
      if (spec.path === '/v1/analyze') return { result: { result: JSON.stringify(Array.from({ length: 6 }, (_, i) => ({ i, tag: 'a', text: 'p', href: `//nl.aliexpress.com/item/${i}.html` }))) } };
      return { result: { result: '""' } };
    };
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    const s = { id: 's13' }; ring.attach(s, ['me']);
    await ring.analyze(s);
    const c = await ring.click(s, { index: 2 });
    expect(c.notice).toMatch(/verify you are human/);
    expect(c.notice).toMatch(/Do not try to solve it/);
  });
});

describe('the session profile on the device', () => {
  it('sends the profile the session was opened with (as the device holds it), none for default', async () => {
    const seen = [];
    const run = async (id, spec) => { seen.push(spec.body); return { result: { result: JSON.stringify({ url: 'u', title: 't', ready: 'complete' }) } }; };
    const ring = createSessionRing({ run, pickDevice: () => device, log: {}, wait: async () => {} });
    // This device advertises no profiles, so the session asks for the plain name (device-profile.test.js has the rest).
    const a = { id: 'a', profile: 'linkedin' }; ring.attach(a, ['me']); await ring.scroll(a, 100);
    expect(seen.every((b) => b.profile === 'linkedin')).toBe(true);
    seen.length = 0;
    const b = { id: 'b', profile: 'default' }; ring.attach(b, ['me']); await ring.scroll(b, 100);
    expect(seen.every((x) => x.profile === undefined)).toBe(true);
  });
});

/*
 * WHERE A SESSION RUNS IS THE CALLER'S CHOICE TOO (2026-11-04). The owner signed in to Google in "default"
 * on the phone; the cluster's "default" had no Google session and Merchant Center showed its sign-in page.
 */
describe('a sign-in page where a login was expected', () => {
  it('recognises the sign-in pages of the sites people sign in to', () => {
    expect(signInPage({ url: 'https://accounts.google.com/v3/signin/identifier?flowName=GlifWebSignIn', title: 'Sign in - Google Accounts' })).toMatch(/accounts\.google\.com asked to sign in/);
    expect(signInPage({ url: 'https://accounts.google.com/ServiceLogin?continue=x', title: '' })).toMatch(/sign in/);
    expect(signInPage({ url: 'https://www.linkedin.com/uas/login', title: 'LinkedIn Login' })).toMatch(/linkedin/);
    expect(signInPage({ url: 'https://shop.example.com/login', title: 'Inloggen' })).toMatch(/asked to sign in/);
  });
  it('does not take an ordinary page for one', () => {
    expect(signInPage({ url: 'https://business.google.com/pl/merchant-center/', title: 'Merchant Center' })).toBe('');
    expect(signInPage({ url: 'https://shop.example.com/login', title: 'Our login bags collection' })).toBe('');
    expect(signInPage({ url: 'https://accounts.google.com/', title: 'Google Account' })).toBe('');
    expect(signInPage({ url: 'not a url' })).toBe('');
  });
});

describe('a device the caller chose', () => {
  it('runs the session on that device, not on the one pickDevice would pick', async () => {
    const { run, calls } = phone();
    const picked = [];
    const ring = createSessionRing({ run, pickDevice: (o, opts) => { picked.push(opts); return { deviceId: 'other', name: 'Desktop' }; }, wait: async () => {} });
    const s = { id: 'm1', profile: 'default' };
    expect(ring.attach(s, ['me'], { device })).toMatchObject({ deviceId: 'd1', name: 'SM-F926B' });
    expect(picked).toEqual([]);
    await ring.scroll(s, 100);
    expect(calls.every((c) => c.deviceId === 'd1')).toBe(true);
  });
  it('without a choice, asks pickDevice for a device holding the session\'s profile', () => {
    const asked = [];
    const ring = createSessionRing({ run: async () => ({}), pickDevice: (o, opts) => { asked.push(opts); return device; } });
    ring.attach({ id: 'm2', profile: 'AliExpress' }, ['me']);
    expect(asked).toEqual([{ profile: 'AliExpress' }]);
  });
});