estate harness: the join-failure and inbox Accept scenarios run with the committed estate run

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 21:10:50 +01:00
commit f6244a4781
3 changed files with 78 additions and 3 deletions

View file

@ -0,0 +1,58 @@
// Failure-mode scenarios for the estate join paths (estate tap and inbox Accept), run by run.sh
// against fakehs.cjs with the env flag each mode needs.
const { chromium } = require('playwright');
const MODE = process.env.MODE;
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'];
setTimeout(() => { console.log('DRIVER_TIMEOUT'); process.exit(3); }, 150000);
let b;
(async () => {
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') console.log('[console]', m.type(), m.text().slice(0, 160)); });
const open = async (h = '#/home/') => { await pg.goto('http://127.0.0.1:8080/' + h); 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 open();
const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
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()); };
const tiers = async (tag) => { await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(2500); 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', tag, '| Friend DM', await tierOf('Friend DM'), '| Slow DM', await tierOf('Slow DM')); };
const acceptInInbox = async (name) => {
// reach the inbox through the navigator's own Inbox tab
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.waitForTimeout(2000);
const tab = pg.locator('[aria-label="Inbox"], [aria-label*="nbox"]'); log('INBOX_TAB', await tab.count());
if (await tab.count()) { await tab.first().click(); await pg.waitForTimeout(1500); }
const inv = pg.getByText('Invites', { exact: true }); if (await inv.count()) { await inv.first().click(); await pg.waitForTimeout(1500); }
log('URL', pg.url());
if (!/invites/.test(pg.url())) { await pg.goto('http://127.0.0.1:8080/#/inbox/invites/'); await pg.waitForTimeout(3000); log('URL_FALLBACK', pg.url()); }
{ const pub = pg.getByText('Public', { exact: true }); log('PUBLIC_FILTER', await pub.count()); if (await pub.count()) { await pub.first().click(); await pg.waitForTimeout(1500); } }
for (let r = 0; r < 3; r++) { const v = pg.getByText('View All', { exact: true }); if (!(await v.count())) break; await v.first().click(); await pg.waitForTimeout(800); }
const b0 = await body(); const i = b0.indexOf(name); log('INBOX has', name, i >= 0, JSON.stringify(b0.slice(Math.max(0, i - 120), i + 120)));
const card = pg.locator('div').filter({ hasText: name }).filter({ has: pg.getByText('Accept', { exact: true }) }).last();
await card.getByText('Accept', { exact: true }).first().click(); log('ACCEPT', name, t());
};
if (MODE === 'pjfail') {
await tap('Friend DM'); await pg.waitForTimeout(3000);
const bb = await body(); const k = bb.search(/Internal server error|Could not join|Joined\./); log('AFTER_FAIL', k >= 0 ? bb.slice(Math.max(0, k - 40), k + 80) : 'no message');
await tap('Friend DM'); await pg.waitForTimeout(6000); await tiers('after retry');
} else if (MODE === 'jdrop') {
await tap('Friend DM'); await pg.waitForTimeout(4000);
const bb = await body(); const k = bb.search(/fetch|Failed|network|Could not join|Joined\./i); log('AFTER_DROP', k >= 0 ? bb.slice(Math.max(0, k - 60), k + 80) : 'no message');
await pg.waitForTimeout(6000); await tiers('after drop'); await open(); await tiers('after reload');
} else if (MODE === 'admin') {
await tap('Friend DM'); await pg.waitForTimeout(6000); await tiers('admin estate');
} else if (MODE === 'inboxadmin') {
await acceptInInbox('Friend DM'); await pg.waitForTimeout(6000); await tiers('admin inbox');
} else if (MODE === 'inboxreload') {
await acceptInInbox('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open('#/inbox/invites/');
await pg.waitForTimeout(30000); await tiers('inbox reload');
} else if (MODE === 'pjreload') {
await tap('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open();
await pg.waitForTimeout(8000); await tiers('pjreload');
const l = pg.locator('[aria-label="Join Slow DM"]'); log('SLOW_DM_STILL_JOINABLE', await l.count());
}
await b.close();
})().catch((e) => { log('ERROR', String(e.stack || e).slice(0, 300)); Promise.resolve(b && b.close()).catch(() => undefined).finally(() => process.exit(1)); });

View file

@ -20,7 +20,7 @@ const strip = (id, name, space, direct) => [
{ type: 'm.room.name', state_key: '', sender: '@admin:test', content: { name } },
{ type: 'm.room.join_rules', state_key: '', sender: '@admin:test', content: { join_rule: 'invite' } },
...(direct ? [{ type: 'm.room.member', state_key: '@friend:test', sender: '@friend:test', content: { membership: 'join', displayname: 'Friend' } }] : []),
{ type: 'm.room.member', state_key: ME, sender: direct ? '@friend:test' : '@admin:test', content: direct ? { membership: 'invite', is_direct: true } : { membership: 'invite' } },
{ type: 'm.room.member', state_key: ME, sender: direct ? (process.env.INVITER_ADMIN === '1' && id === DM1 ? '@admin:test' : '@friend:test') : '@admin:test', content: direct ? { membership: 'invite', is_direct: true } : { membership: 'invite' } },
];
const joinedRoom = (id, name, extra = [], space = false) => ({
state: { events: [se('m.room.create', space ? { type: 'm.space', room_version: '12' } : { room_version: '12' }), se('m.room.name', { name }), se('m.room.join_rules', { join_rule: 'invite' }), se('m.room.member', { membership: 'join' }, ME, ME), ...extra] },
@ -91,13 +91,18 @@ http.createServer((req, res) => {
if (id === PROJ[1].id) return json(res, 403, { errcode: 'M_FORBIDDEN', error: 'You are not invited to this room.' });
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) });
if (process.env.JOIN_DROP === '1' && id === DM1) { console.log('JOIN_DROPPED ' + Date.now() + ' ' + id); return req.socket.destroy(); }
if (process.env.JOIN_DELAY === '1') { console.log('JOIN_DELAYED ' + Date.now() + ' ' + id); return setTimeout(() => json(res, 200, { room_id: id }), 6000); }
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' && p.includes('m.direct') && process.env.FAIL_MDIRECT === '1' && !global.failedOnce) { global.failedOnce = 1; console.log('MDIRECT_FAIL ' + Date.now()); return json(res, 500, { errcode: 'M_UNKNOWN', error: 'Internal server error' }); }
if (p.includes('/account_data/') && req.method === 'PUT') {
const DELAY = p.includes('m.direct') && process.env.DELAY_MDIRECT === '1';
const type = decodeURIComponent(p.split('/account_data/')[1]);
try { const c = JSON.parse(body); adLog.push({ type, content: c, after: syncN }); const i = held.findIndex((h) => h.type === type); if (i >= 0) held.splice(i, 1); held.push({ type, content: c }); } catch (e) {}
if (DELAY) { console.log('MDIRECT_DELAYED ' + Date.now()); return setTimeout(() => json(res, 200, {}), 6000); }
return json(res, 200, {});
}
if (p.includes('/hierarchy')) return json(res, 200, { rooms: hierarchy() });

View file

@ -1,6 +1,6 @@
#!/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.
# builds the client, then drives the direct-message and join-failure scenarios against fakehs.cjs.
set -e
cd /src
npm ci --no-audit --no-fund --ignore-scripts >/tmp/npmci.log 2>&1 || { tail -20 /tmp/npmci.log; exit 1; }
@ -8,7 +8,7 @@ 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 >/tmp/pw.log 2>&1
cp /src/harness/estate/drive.cjs /tmp/pw/
cp /src/harness/estate/drive.cjs /src/harness/estate/drive-failures.cjs /tmp/pw/
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 &
@ -17,4 +17,16 @@ for m in twodm reload convert; do
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
failure() { m=$1; shift
env "$@" 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-failures.cjs; echo "DRIVE $m RC=$?"
kill $HS; sleep 1
echo "=== hs $m"; grep -E "MDIRECT|DELIVER|JOIN_|STATE_" /tmp/hs-$m.out; grep -E " JOIN " /tmp/hslog-$m.txt; }
failure pjfail FAIL_MDIRECT=1
failure pjreload DELAY_MDIRECT=1
failure jdrop JOIN_DROP=1
failure admin INVITER_ADMIN=1
failure inboxadmin INVITER_ADMIN=1
failure inboxreload JOIN_DELAY=1
echo DONE