aboutsummaryrefslogtreecommitdiffstats
path: root/tools/safe-boot-test.js
blob: a23c0673ddb5370f8715e3941485a94503d0d3d0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// The crash-loop guard: the inline BOOT_GUARD counts page starts, the app
// clears the count once it has lived or leaves cleanly, and a third start
// in a row opens in safe mode — a notice naming the last boot phase, no
// automatic Ark connection, and "Open normally" back to a plain boot.
// Run: bun tools/safe-boot-test.js
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { BOOT_GUARD } from '../build.js';

const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true,
  plugins: [{ name: 'safe-boot-test', setup(build) {
    build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + `
window.sbApp = { ui, wallet, render, enterWallet, get ready() { return !_bootDeciding; } };
` }));
    build.onLoad({ filter: /src\/features\/ark\.js$/ }, async ({ path }) => ({ loader: 'js', contents:
      (await Bun.file(path).text()).replace("  return {\n    id: 'ark',", `
  window.sbArk = { get ark() { return ark; }, get connecting() { return !!arkConnectPromise; }, get lastAutoInit() { return lastAutoInit; } };
  return {\n    id: 'ark',`) }));
    build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' }));
  } }] });
assert(bundle.success, bundle.logs.join('\n'));
const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>${await Bun.file('src/style.css').text()}</style><div id="app"></div>${BOOT_GUARD}<script type="module">${await bundle.outputs[0].text()}</script>`;
const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname === '/'
  ? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
try {
  const page = await browser.newPage(), errors = [];
  page.on('pageerror', (e) => errors.push(e.message));
  await page.setViewport({ width: 390, height: 844 });
  await page.setRequestInterception(true);
  page.on('request', (r) => r.url().startsWith(server.url.origin) || r.url().startsWith('data:') ? r.continue() : r.abort());
  await page.evaluateOnNewDocument(() => {
    if (!localStorage.getItem('sb-seeded')) {
      localStorage.setItem('sb-seeded', '1');
      localStorage.setItem('btc-wallet-network', 'regtest');
      localStorage.setItem('btc-wallet-ark-provider:regtest', 'local');
      // two starts already, neither cleared: the last one died merging the worker's state
      localStorage.setItem('coinos-boot', '2');
      localStorage.setItem('coinos-boot-phase', 'ark-merge');
    }
  });
  await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
  await page.waitForFunction(() => window.sbApp?.ready);
  const guard = await page.evaluate(() => ({ safe: window.__safeBoot, phase: window.__bootPhase, count: localStorage.getItem('coinos-boot') }));
  check('the third start in a row opens in safe mode, knowing the phase the last one reached', guard.safe === 3 && guard.phase === 'ark-merge' && guard.count === '3', JSON.stringify(guard));
  await page.evaluate(async (mn) => {
    await sbApp.enterWallet(mn, '', { fresh: true, generated: true });
    sbApp.ui.onb = null; localStorage.removeItem('btc-wallet-onb-step');
    sbApp.ui.tab = 'wallet'; sbApp.render();
  }, generateMnemonic(wordlist));
  await new Promise((r) => setTimeout(r, 1500));
  const notice = await page.evaluate(() => { const n = [...document.querySelectorAll('.notice')].find((x) => /safe mode/i.test(x.textContent)); return n ? n.textContent : ''; });
  check('the wallet home says so and names the phase', /safe mode/i.test(notice) && /ark-merge/.test(notice) && /2 times/.test(notice), notice.slice(0, 120));
  const arkState = await page.evaluate(() => ({ ark: !!sbArk.ark, connecting: sbArk.connecting, auto: sbArk.lastAutoInit }));
  check('nothing dialled the Ark server on its own', !arkState.ark && !arkState.connecting && !arkState.auto, JSON.stringify(arkState));
  // a clean leave clears the count
  await page.evaluate(() => dispatchEvent(new Event('pagehide')));
  check('leaving the page cleanly clears the count', (await page.evaluate(() => localStorage.getItem('coinos-boot'))) === null);
  // "Open normally": the count is cleared and the page reloads into a plain boot
  await page.evaluate(() => localStorage.setItem('coinos-boot', '3'));
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.evaluate(() => { const b = [...document.querySelectorAll('.notice button')].find((x) => /open normally/i.test(x.textContent)); b.click(); }),
  ]);
  await page.waitForFunction(() => window.sbApp?.ready);
  const after = await page.evaluate(() => ({ safe: window.__safeBoot, count: localStorage.getItem('coinos-boot'), notice: [...document.querySelectorAll('.notice')].some((x) => /safe mode/i.test(x.textContent)) }));
  check('"Open normally" reloads into a plain boot (one start counted, no notice)', after.safe === undefined && after.count === '1' && !after.notice, JSON.stringify(after));
  await new Promise((r) => setTimeout(r, 8600));
  check('a boot that lives eight seconds clears its count', (await page.evaluate(() => localStorage.getItem('coinos-boot'))) === null);
  check('no page errors', errors.length === 0, errors.join(' | ').slice(0, 200));
} finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ safe mode after a crash loop' : '\n❌ failed');
process.exit(ok ? 0 : 1);