estate harness: Soul's fake homeserver with member-state echo, and the twodm, reload and convert direct-message scenarios
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2827699439
commit
e59cc93a2a
3 changed files with 61 additions and 105 deletions
|
|
@ -1,82 +1,41 @@
|
|||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const TAG = process.env.TAG;
|
||||
const out = (n) => `/out/${TAG}-${n}`;
|
||||
const tl = [];
|
||||
const T0 = Date.now();
|
||||
const t = () => `+${Date.now() - T0}ms`;
|
||||
const MODE = process.env.MODE; // twodm | reload
|
||||
const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
||||
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
||||
const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
|
||||
(async () => {
|
||||
const b = await chromium.launch();
|
||||
const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 });
|
||||
const pg = await ctx.newPage();
|
||||
pg.on('console', (m) => { if (m.type() === 'error' && !/OIDC|issuer|endpoint|supported|ERR_NAME|404/.test(m.text())) tl.push('console.error ' + m.text().slice(0, 200)); });
|
||||
const pg = await ctx.newPage(); pg.on('console', (m) => { if (m.type() === 'error' || /command|direct/i.test(m.text())) console.log('[console]', m.type(), m.text().slice(0, 200)); });
|
||||
const open = async () => { await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.reload(); await pg.waitForTimeout(9000); };
|
||||
await pg.goto('http://127.0.0.1:8080/');
|
||||
await pg.evaluate(() => { localStorage.setItem('cinny_access_token', 'tok'); localStorage.setItem('cinny_device_id', 'DEV'); localStorage.setItem('cinny_user_id', '@t:test'); localStorage.setItem('cinny_hs_base_url', 'http://127.0.0.1:8080'); });
|
||||
await pg.goto('http://127.0.0.1:8080/#/home/');
|
||||
await pg.waitForTimeout(12000);
|
||||
await open();
|
||||
const text = () => pg.evaluate(() => document.body.innerText);
|
||||
const snap = async (n) => { await pg.screenshot({ path: out(n + '.png') }); tl.push(`--- ${n} ${t()} url=${pg.url()}\n${await text()}`); };
|
||||
const buttons = async () => tl.push('buttons ' + JSON.stringify(await pg.evaluate(() => [...document.querySelectorAll('button')].map((e) => { const r = e.getBoundingClientRect(); return [e.getAttribute('aria-label') || e.innerText.slice(0, 30), Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)]; }))));
|
||||
const small = async () => tl.push('SMALL ' + JSON.stringify(await pg.evaluate(() => [...document.querySelectorAll('button, a, [role="button"]')].filter((e) => e.getClientRects().length).map((e) => [e.getAttribute('aria-label') || e.innerText.slice(0, 30), Math.round(e.getBoundingClientRect().width), Math.round(e.getBoundingClientRect().height)]).filter((r) => r[2] < 44))));
|
||||
const layout = async () => tl.push('layout ' + JSON.stringify(await pg.evaluate(() => ({ vw: innerWidth, scrollW: document.documentElement.scrollWidth, bodyScrollW: document.body.scrollWidth }))));
|
||||
await snap('1-landing'); await buttons(); await layout(); await small();
|
||||
if (process.env.NOROOT === '1') {
|
||||
tl.push('has Create Room: ' + (await pg.getByText('Create Room', { exact: true }).count()) + ' Join with Address: ' + (await pg.getByText('Join with Address', { exact: true }).count()) + ' finder: ' + (await pg.locator('[aria-label="Find a room"]').count()));
|
||||
await b.close(); return;
|
||||
const reveal = async (name) => { const loc = pg.locator(`[aria-label="Join ${name}"]`); if (await loc.count()) return loc; for (let r = 0; r < 2; r++) for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); if (await loc.count()) return loc; } } return loc; };
|
||||
const tierOf = (name) => pg.evaluate((n) => { const s = document.body.innerText.split('\n').map((x) => x.trim()); const i = s.indexOf(n); if (i < 0) return 'absent'; for (let j = i; j >= 0; j--) if (/^(elsewhere|direct messages)$/i.test(s[j])) return s[j]; return 'none-above'; }, name);
|
||||
const tap = async (n) => { const l = await reveal(n); log('JOINCTRL', n, await l.count()); await l.first().click(); log('CLICK', n, t()); };
|
||||
if (MODE === 'twodm') {
|
||||
await tap('Slow DM'); await pg.waitForTimeout(300); await tap('Late DM');
|
||||
await pg.waitForTimeout(45000);
|
||||
} else if (MODE === 'convert') {
|
||||
await tap('Friend DM'); await pg.waitForTimeout(6000);
|
||||
log('TIER before', await tierOf('Friend DM'));
|
||||
await pg.goto('http://127.0.0.1:8080/#/direct/!dm1:test/'); await pg.waitForTimeout(4000);
|
||||
const ed = pg.locator('[contenteditable="true"]'); log('EDITORS', await ed.count(), pg.url());
|
||||
await ed.first().click(); await pg.keyboard.type('/converttoroom', { delay: 30 }); await pg.waitForTimeout(500);
|
||||
log('ED_TEXT', JSON.stringify(await ed.first().innerText())); log('BODY', JSON.stringify((await text()).slice(-600)));
|
||||
await pg.getByText('Convert direct message to room').first().click(); await pg.waitForTimeout(400); log('ED_PICKED', JSON.stringify(await ed.first().innerHTML()).slice(0, 300));
|
||||
await pg.keyboard.press('Enter'); log('SENT', t()); await pg.waitForTimeout(800); log('ED_TEXT2', JSON.stringify(await ed.first().innerText().catch(()=>'')));
|
||||
if (await ed.first().innerText().catch(() => '')) { await pg.keyboard.press('Enter'); log('SENT2', t()); }
|
||||
await pg.waitForTimeout(8000);
|
||||
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(3000);
|
||||
} else {
|
||||
await tap('Slow DM'); await pg.waitForTimeout(3000);
|
||||
log('RELOAD', t()); await open();
|
||||
await pg.waitForTimeout(30000);
|
||||
}
|
||||
const reveal = async (sel) => {
|
||||
const loc = pg.locator(sel);
|
||||
if (await loc.count()) return loc;
|
||||
for (let round = 0; round < 2; round++) for (const h of HEADERS) {
|
||||
const hl = pg.getByText(h, { exact: true });
|
||||
if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); if (await loc.count()) { tl.push(`revealed ${sel} via ${h}`); return loc; } }
|
||||
}
|
||||
tl.push(`could not reveal ${sel}`); return loc;
|
||||
};
|
||||
// expand everything once for the inventory
|
||||
for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); tl.push('clicked header ' + h); } }
|
||||
await snap('2-expanded'); await buttons(); await layout(); await small();
|
||||
// room finder
|
||||
const finder = pg.locator('[aria-label="Find a room"], [aria-label="Search"]').first();
|
||||
await finder.click(); await pg.waitForTimeout(800);
|
||||
tl.push('after finder url=' + pg.url() + ' dialogs=' + (await pg.locator('[role="dialog"]').count()) + ' inputs=' + JSON.stringify(await pg.evaluate(() => [...document.querySelectorAll('input')].map((i) => i.placeholder))));
|
||||
await snap('3-finder');
|
||||
await pg.keyboard.press('Escape'); await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(3000);
|
||||
// join with frame poll
|
||||
const joinPoll = async (name, ms) => {
|
||||
const loc = await reveal(`[aria-label="Join ${name}"]`);
|
||||
if (!(await loc.count())) { tl.push(`no Join ${name}`); return; }
|
||||
const u0 = pg.url(); await loc.first().click(); const c = Date.now(); tl.push(`CLICK Join ${name} ${t()}`);
|
||||
let card = 0, frames = 0, urlAt = null, firstCardAt = null;
|
||||
while (Date.now() - c < ms) {
|
||||
const s = await text(); frames++;
|
||||
if (s.includes('Summary card')) { card++; if (firstCardAt === null) firstCardAt = Date.now() - c; }
|
||||
if (urlAt === null && pg.url() !== u0) urlAt = Date.now() - c;
|
||||
await pg.waitForTimeout(40);
|
||||
}
|
||||
tl.push(`POLL ${name}: frames=${frames} cardFrames=${card} firstCardAt=${firstCardAt} urlChangedAt=${urlAt} url=${pg.url()}`);
|
||||
await snap(`4-join-${name.replace(/\W/g, '')}`);
|
||||
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(2500);
|
||||
};
|
||||
await joinPoll('Commons', 6000);
|
||||
await joinPoll('Aetheria', 6000);
|
||||
await joinPoll('Friend DM', 6000);
|
||||
// timeout path, room
|
||||
const slow = async (name) => {
|
||||
const loc = await reveal(`[aria-label="Join ${name}"]`);
|
||||
if (!(await loc.count())) { tl.push(`no Join ${name}`); return; }
|
||||
await loc.first().click(); tl.push(`CLICK Join ${name} ${t()}`);
|
||||
await pg.waitForTimeout(22800); await snap(`5-slow-${name.replace(/\W/g, '')}-21s`);
|
||||
tl.push(`timeout msg shown: ${(await text()).includes('still on its way')}`);
|
||||
await pg.waitForTimeout(5000); await snap(`5-slow-${name.replace(/\W/g, '')}-27s`);
|
||||
tl.push(`after arrival: Join ${name} still present=${await pg.locator(`[aria-label="Join ${name}"]`).count()} url=${pg.url()}`);
|
||||
};
|
||||
await slow('Odin');
|
||||
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(2500);
|
||||
await slow('Slow DM');
|
||||
// wide: tier placement (wide tiers start open)
|
||||
await pg.setViewportSize({ width: 1280, height: 800 });
|
||||
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(6000); await snap('6-wide');
|
||||
for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); } }
|
||||
log('TIER Slow DM', await tierOf('Slow DM'), '| Late DM', await tierOf('Late DM'), '| Friend DM', await tierOf('Friend DM'));
|
||||
await b.close();
|
||||
})().catch((e) => { tl.push('ERROR ' + String(e.stack || e)); process.exitCode = 1; }).finally(() => fs.writeFileSync(out('timeline.txt'), tl.join('\n')));
|
||||
})().catch((e) => { log('ERROR', String(e.stack || e)); process.exitCode = 1; });
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ const log = [];
|
|||
const P = ['aetheria', 'ghostlight', 'huginn', 'idunn', 'bifrost', 'odin'];
|
||||
const STEWARD = { aetheria: 'nibu', ghostlight: 'nibu', huginn: 'eureka', idunn: 'eureka' };
|
||||
const PROJ = P.map((p, i) => ({ id: `!p${i}:test`, alias: `#${p}:test`, name: p[0].toUpperCase() + p.slice(1), key: p }));
|
||||
const ROOT = '!root:test', PSP = '!proj:test', GEN = '!gen:test', OP = '!op:test', PUB = '!pub:test', STRAY = '!stray:test', DM1 = '!dm1:test', DM2 = '!dm2:test';
|
||||
const SLOW = new Set([PROJ[5].id, DM2]);
|
||||
const NAMES = { [PUB]: 'Commons', [DM1]: 'Friend DM', [DM2]: 'Slow DM', [STRAY]: 'Adhoc' };
|
||||
const ROOT = '!root:test', PSP = '!proj:test', GEN = '!gen:test', OP = '!op:test', PUB = '!pub:test', STRAY = '!stray:test', DM1 = '!dm1:test', DM2 = '!dm2:test', DM3 = '!dm3:test';
|
||||
const SLOW = new Set([PROJ[5].id, DM2, DM3]);
|
||||
const NAMES = { [PUB]: 'Commons', [DM1]: 'Friend DM', [DM2]: 'Slow DM', [DM3]: 'Late DM', [STRAY]: 'Adhoc' };
|
||||
PROJ.forEach((r) => (NAMES[r.id] = r.name));
|
||||
const pending = []; const pendingAD = []; let ev = 0;
|
||||
const held = []; const pendingState = []; const pending = []; const pendingAD = []; let ev = 0;
|
||||
const se = (type, content, sk = '', sender = '@admin:test') => ({ type, content, state_key: sk, sender, event_id: `$e${++ev}`, origin_server_ts: Date.now() });
|
||||
const strip = (id, name, space, direct) => [
|
||||
{ type: 'm.room.create', state_key: '', sender: direct ? '@friend:test' : '@admin:test', content: space ? { type: 'm.space', room_version: '12' } : { room_version: '12' } },
|
||||
|
|
@ -35,6 +35,7 @@ const invites = () => {
|
|||
PROJ.forEach((r) => (o[r.id] = { invite_state: { events: strip(r.id, r.name) } }));
|
||||
o[DM1] = { invite_state: { events: strip(DM1, NAMES[DM1], false, true) } };
|
||||
o[DM2] = { invite_state: { events: strip(DM2, NAMES[DM2], false, true) } };
|
||||
o[DM3] = { invite_state: { events: strip(DM3, NAMES[DM3], false, true) } };
|
||||
return o;
|
||||
};
|
||||
const child = (id, steward) => ({ type: 'm.space.child', state_key: id, content: { via: ['test'], ...(steward ? { 'org.gamecult.steward': { mind: steward, repo: 'x', source: 'y', steward: 'z' } } : {}) }, sender: '@admin:test', origin_server_ts: 1 });
|
||||
|
|
@ -61,21 +62,23 @@ http.createServer((req, res) => {
|
|||
return fs.createReadStream(f).pipe(res);
|
||||
}
|
||||
let body = ''; req.on('data', (c) => (body += c)); req.on('end', () => {
|
||||
if (req.method === 'PUT' && p.includes('m.direct')) console.log('MDIRECT_PUT ' + (Date.now()) + ' ' + body);
|
||||
log.push(`${Date.now()} ${req.method} ${p}${req.method === 'PUT' && p.includes('account_data') ? ' BODY ' + body : ''}`);
|
||||
if (p === '/_matrix/client/versions') return json(res, 200, { versions: ['v1.1', 'v1.5', 'v1.11'], unstable_features: {} });
|
||||
if (p.endsWith('/sync')) {
|
||||
const since = u.searchParams.get('since');
|
||||
if (!since) {
|
||||
const join = {}; join[GEN] = joinedRoom(GEN, 'General'); join[STRAY] = joinedRoom(STRAY, 'Adhoc');
|
||||
return json(res, 200, { next_batch: 's1', rooms: { join, invite: invites(), leave: {} }, account_data: { events: [] }, presence: { events: [] }, to_device: { events: [] }, device_one_time_keys_count: { signed_curve25519: 50 } });
|
||||
return json(res, 200, { next_batch: 's1', rooms: { join, invite: invites(), leave: {} }, account_data: { events: held.slice() }, presence: { events: [] }, to_device: { events: [] }, device_one_time_keys_count: { signed_curve25519: 50 } });
|
||||
}
|
||||
const respond = () => {
|
||||
const now = Date.now(); const join = {};
|
||||
for (let i = pending.length - 1; i >= 0; i--) if (pending[i].at <= now) {
|
||||
const id = pending[i].id; pending.splice(i, 1);
|
||||
const extra = id === DM1 || id === DM2 ? [se('m.room.member', { membership: 'join', displayname: 'Friend' }, '@friend:test', '@friend:test')] : [];
|
||||
join[id] = joinedRoom(id, NAMES[id] || id, extra); log.push(`${now} SYNC delivers join ${id}`);
|
||||
const extra = id === DM1 || id === DM2 || id === DM3 ? [se('m.room.member', { membership: 'join', displayname: 'Friend' }, '@friend:test', '@friend:test')] : [];
|
||||
join[id] = joinedRoom(id, NAMES[id] || id, extra); if (extra.length) { const me = join[id].state.events.find((e) => e.type === 'm.room.member' && e.state_key === ME); me.unsigned = { prev_content: { membership: 'invite', is_direct: true } }; } log.push(`${now} SYNC delivers join ${id}`); console.log('DELIVER ' + now + ' ' + id);
|
||||
}
|
||||
for (const s of pendingState.splice(0)) { join[s.id] = join[s.id] || { state: { events: [] }, timeline: { events: [], limited: false, prev_batch: 'p' }, ephemeral: { events: [] }, account_data: { events: [] }, unread_notifications: {}, summary: {} }; join[s.id].timeline.events.push(s.ev); console.log('STATE_ECHO ' + now + ' ' + s.id + ' ' + JSON.stringify(s.ev.content)); }
|
||||
const ad = pendingAD.splice(0);
|
||||
if (Object.keys(join).length || ad.length) return json(res, 200, { next_batch: `s${++syncN}`, rooms: { join, invite: {}, leave: {} }, account_data: { events: ad } });
|
||||
return json(res, 200, { next_batch: since, rooms: {} });
|
||||
|
|
@ -86,12 +89,15 @@ http.createServer((req, res) => {
|
|||
if (req.method === 'POST' && jm) {
|
||||
const id = jm[1]; log.push(`${Date.now()} JOIN ${id}`);
|
||||
if (id === PROJ[1].id) return json(res, 403, { errcode: 'M_FORBIDDEN', error: 'You are not invited to this room.' });
|
||||
pending.push({ id, at: Date.now() + (SLOW.has(id) ? 24000 : 1000) });
|
||||
const prior = pending.find((q) => q.id === DM2 || q.id === DM3);
|
||||
pending.push({ id, at: (id === DM3 || id === DM2) && prior ? prior.at : Date.now() + (SLOW.has(id) ? 24000 : 1000) });
|
||||
return json(res, 200, { room_id: id });
|
||||
}
|
||||
const sm = p.match(/\/rooms\/([^/]+)\/state\/m\.room\.member\/(.+)$/);
|
||||
if (req.method === 'PUT' && sm) { console.log('STATE_PUT ' + Date.now() + ' ' + sm[1] + ' ' + body); const e = se('m.room.member', JSON.parse(body), sm[2], sm[2]); e.unsigned = { prev_content: { membership: 'join' } }; setTimeout(() => pendingState.push({ id: sm[1], ev: e }), 400); return json(res, 200, { event_id: e.event_id }); }
|
||||
if (p.includes('/account_data/') && req.method === 'PUT') {
|
||||
const type = decodeURIComponent(p.split('/account_data/')[1]);
|
||||
try { pendingAD.push({ type, content: JSON.parse(body) }); } catch (e) {}
|
||||
try { const c = JSON.parse(body); pendingAD.push({ type, content: c }); const i = held.findIndex((h) => h.type === type); if (i >= 0) held.splice(i, 1); held.push({ type, content: c }); } catch (e) {}
|
||||
return json(res, 200, {});
|
||||
}
|
||||
if (p.includes('/hierarchy')) return json(res, 200, { rooms: hierarchy() });
|
||||
|
|
|
|||
|
|
@ -1,29 +1,20 @@
|
|||
# Run inside playwright:v1.48.2-jammy with the repo at /src and /out writable:
|
||||
# bash /src/harness/estate/run.sh
|
||||
# Scenario A: root resolves. Scenario B (NOROOT=1): the root alias 404s.
|
||||
#!/usr/bin/env bash
|
||||
# Runs inside mcr.microsoft.com/playwright:v1.48.2-jammy with the repo at /src:
|
||||
# builds the client, then drives three direct-message scenarios against fakehs.cjs.
|
||||
set -e
|
||||
cd /src
|
||||
mkdir -p /out
|
||||
npm ci --no-audit --no-fund --ignore-scripts >/out/npmci.log 2>&1
|
||||
npm run build > /out/build.log 2>&1; echo BUILD_RC=$? >> /out/build.log
|
||||
npm ci --no-audit --no-fund --ignore-scripts >/tmp/npmci.log 2>&1 || { tail -20 /tmp/npmci.log; exit 1; }
|
||||
npm run build > /tmp/build.log 2>&1 || { tail -30 /tmp/build.log; exit 1; }
|
||||
echo BUILD_OK $(git rev-parse HEAD)
|
||||
printf '{"defaultHomeserver":0,"homeserverList":["127.0.0.1:8080"],"allowCustomHomeservers":true,"featuredCommunities":{"spaces":["#gamecult:test"],"rooms":[],"servers":["test"]},"hashRouter":{"enabled":true,"basename":"/"}}' > dist/config.json
|
||||
mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i --no-audit --no-fund playwright@1.48.2 >/out/pw.log 2>&1
|
||||
mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i --no-audit --no-fund playwright@1.48.2 >/tmp/pw.log 2>&1
|
||||
cp /src/harness/estate/drive.cjs /tmp/pw/
|
||||
runone () { # tag noroot
|
||||
HSLOG=/out/hslog-$1.txt NOROOT=$2 node /src/harness/estate/fakehs.cjs /src/dist > /out/hs-$1.out 2>&1 &
|
||||
HS=$!
|
||||
sleep 2
|
||||
TAG=$1 NOROOT=$2 node /tmp/pw/drive.cjs; echo "DRIVE $1 RC=$?"
|
||||
kill $HS
|
||||
}
|
||||
runone root 0
|
||||
runone noroot 1
|
||||
for t in root noroot; do
|
||||
echo "=== $t"
|
||||
grep -E '^(SMALL|layout|has Create|CLICK|POLL|timeout|after arrival|revealed|--- )' /out/$t-timeline.txt | cut -c1-1800 || true
|
||||
set +e
|
||||
for m in twodm reload convert; do
|
||||
HSLOG=/tmp/hslog-$m.txt NOROOT=0 node /src/harness/estate/fakehs.cjs /src/dist > /tmp/hs-$m.out 2>&1 &
|
||||
HS=$!; sleep 2
|
||||
MODE=$m node /tmp/pw/drive.cjs; echo "DRIVE $m RC=$?"
|
||||
kill $HS; sleep 1
|
||||
echo "=== hs $m"; grep -E "MDIRECT_PUT|DELIVER|STATE_" /tmp/hs-$m.out; grep -E " JOIN " /tmp/hslog-$m.txt
|
||||
done
|
||||
ls -la /out | head -30
|
||||
cat /out/noroot-timeline.txt | grep -m3 -i "could not" || true
|
||||
echo "=== m.direct and joins (root)"
|
||||
grep -E 'direct|JOIN' /out/hslog-root.txt | cut -c1-220 || true
|
||||
tail -1 /out/build.log
|
||||
echo DONE
|
||||
|
|
|
|||
Loading…
Reference in a new issue