diff options
| author | Adam Soltys <[email protected]> | 2026-10-13 13:04:52 +0000 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-13 13:04:52 +0000 |
| commit | f3d03c642bef06b3afc8a3038136c5e0fc1e21f7 (patch) | |
| tree | 11f43f80c14d9c712d12ba86820ca38fa51b9bf1 /tools | |
| parent | cd058495e11d76bb5afe4fe85b5131adaa387ca1 (diff) | |
| download | coinosv3-f3d03c642bef06b3afc8a3038136c5e0fc1e21f7.tar.gz coinosv3-f3d03c642bef06b3afc8a3038136c5e0fc1e21f7.zip | |
Profiles show boosts as boosts, and their saved copy keeps each post's kind
A boost that reached a profile from the feed was saved as a kind 1 note,
so reopened it drew its raw JSON as a reply; video events and polls lost
their kind and tags the same way. The saved copy now keeps kinds (boosts
whole), old copies are dropped, and profiles ask for boosts too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/profile-repost-test.js | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/tools/profile-repost-test.js b/tools/profile-repost-test.js new file mode 100644 index 0000000..7825efe --- /dev/null +++ b/tools/profile-repost-test.js @@ -0,0 +1,76 @@ +// A profile lists the person's boosts, as a feed does: the note under +// "<name> reposted", never the boost's raw JSON. And its saved copy keeps +// each post's kind: a boost reopened from cache (relays silent) is still a +// boost, a video event still has its clip — they were all saved as kind 1 +// notes, so a boost came back as "reply · {"content":…}". +// Run: bun tools/profile-repost-test.js +import puppeteer from './puppeteer.js'; +import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; +import { npubEncode } from 'nostr-tools/nip19'; +import { buildHtml } from '../build.js'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +const key = () => { const sk = generateSecretKey(); return { sk, pk: getPublicKey(sk) }; }; +const ME = key(), THEM = key(); +const now = Math.floor(Date.now() / 1000); +const THEIRS = finalizeEvent({ kind: 1, created_at: now - 900, tags: [], content: 'their offer to grow nostr' }, THEM.sk); +const EVS = [ + finalizeEvent({ kind: 1, created_at: now - 100, tags: [], content: 'my own words here' }, ME.sk), + finalizeEvent({ kind: 6, created_at: now - 200, tags: [['e', THEIRS.id, '', 'mention'], ['p', THEM.pk]], content: JSON.stringify(THEIRS) }, ME.sk), + finalizeEvent({ kind: 21, created_at: now - 300, tags: [['title', 'My clip title'], ['imeta', 'url https://example.invalid/v/clipfile', 'm video/mp4', 'dim 180x320']], content: 'about my clip' }, ME.sk), +]; +const PROFILES = [ + finalizeEvent({ kind: 0, created_at: now - 10, tags: [], content: JSON.stringify({ name: 'Booster' }) }, ME.sk), + finalizeEvent({ kind: 0, created_at: now - 10, tags: [], content: JSON.stringify({ name: 'Offerer' }) }, THEM.sk), +]; +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 0, fetch: (req) => { const p = new URL(req.url).pathname; if (p === '/verify-worker.js' || p.startsWith('/punks')) return new Response(Bun.file('dist' + p)); return new Response(html, { headers: { 'content-type': 'text/html' } }); } }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +await page.evaluateOnNewDocument((fx) => { + const Real = window.WebSocket; + const fits = (ev, f) => (!f.kinds || f.kinds.includes(ev.kind)) && (!f.authors || f.authors.includes(ev.pubkey)) && (!f.ids || f.ids.includes(ev.id)) && !Object.keys(f).some((k) => k.startsWith('#')); + window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (d) => { try { const m = JSON.parse(d); if (m[0] === 'REQ') { const fs = m.slice(2); setTimeout(() => { if (!sessionStorage.getItem('silent')) for (const ev of fx.filter((e) => fs.some((f) => fits(e, f)))) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], ev]) })); ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })); }, 0); return; } if (m[0] === 'EVENT') return; } catch {} return send(d); }; return ws; }; + window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); +}, [...EVS, ...PROFILES]); +const errs = []; +page.on('pageerror', (e) => errs.push(String(e))); +const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); +const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; +const rows = () => page.evaluate(() => [...document.querySelectorAll('.notes-feed [data-zap-post]')].map((r) => ({ text: r.innerText, post: r.getAttribute('data-zap-post'), video: !!r.querySelector('video.note-video') }))); +async function look(label) { + console.log('\n[' + label + ']'); + check('the profile shows its posts', await waitText('my own words here', 20000)); + await sleep(1200); + const r = await rows(); + const boost = r.find((x) => x.text.includes('their offer to grow nostr')); + check('the boost is the note it boosts, under "Booster reposted"', !!boost && /Booster reposted/.test(boost.text) && boost.post === THEIRS.id, JSON.stringify(boost)); + check('…as Offerer\'s post, not a reply', !!boost && /Offerer/.test(boost.text) && !/\breply\b/i.test(boost.text.split('\n').slice(0, 4).join(' '))); + check('no raw JSON anywhere', !r.some((x) => /"content":|"sig":/.test(x.text)), r.map((x) => x.text.slice(0, 40)).join(' | ')); + const clip = r.find((x) => x.text.includes('My clip title')); + check('the video event is still a clip', !!clip && clip.video, JSON.stringify(clip)); +} +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); + await click('create a new wallet'); await sleep(600); + await page.waitForSelector('.words .w .t'); + await click('skip verification'); + await waitText('receive', 20000); + await page.goto(server.url.href + npubEncode(ME.pk), { waitUntil: 'domcontentloaded' }); + await look('from the relays'); + await sleep(1500); // (the page cache is written behind a debounce) + const nav = await page.evaluate(() => history.state); + await page.evaluate(() => sessionStorage.setItem('silent', '1')); + // reopened in a booted app (a cold deep link opens before the saved pages are read) + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); await sleep(1500); + await page.evaluate(([st, u]) => { history.pushState(st, '', u); dispatchEvent(new PopStateEvent('popstate', { state: st })); }, [nav, '/' + npubEncode(ME.pk)]); + await look('reopened from its saved copy, the relays silent'); + check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ a profile shows boosts as boosts, and keeps them so' : '\n❌ failed'); +process.exit(ok ? 0 : 1); |
