estate harness: read a row's tier from a fresh visit without toggling tiers closed; catch the retryable message before sync replaces the row
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6950da865e
commit
9b66b2b250
5 changed files with 48 additions and 19 deletions
|
|
@ -15,4 +15,32 @@ exports.mDirectPuts = () =>
|
||||||
exports.hsLog().filter((l) => / PUT .*m\.direct BODY /.test(l)).map((l) => l.split(' BODY ')[1]);
|
exports.hsLog().filter((l) => / PUT .*m\.direct BODY /.test(l)).map((l) => l.split(' BODY ')[1]);
|
||||||
/** Join requests the server saw for a room id. */
|
/** Join requests the server saw for a room id. */
|
||||||
exports.joinsOf = (roomId) => exports.hsLog().filter((l) => new RegExp(` JOIN ${roomId}$`).test(l)).length;
|
exports.joinsOf = (roomId) => exports.hsLog().filter((l) => new RegExp(` JOIN ${roomId}$`).test(l)).length;
|
||||||
|
const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
|
||||||
|
/** The tier header above a row, 'absent' when the page does not show the row, 'none-above' when no tier header precedes it. */
|
||||||
|
const tierOf = (pg, 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);
|
||||||
|
/**
|
||||||
|
* The tier a room is listed under, read from a fresh visit to the room list: a collapsed tier is
|
||||||
|
* opened only if the row is not already showing, so reading never closes what is open.
|
||||||
|
*/
|
||||||
|
exports.revealedTier = async (pg, name) => {
|
||||||
|
await pg.goto('http://127.0.0.1:8080/#/home/');
|
||||||
|
await pg.waitForTimeout(2500);
|
||||||
|
let t = await tierOf(pg, name);
|
||||||
|
for (const h of HEADERS) {
|
||||||
|
if (t !== 'absent') break;
|
||||||
|
const hl = pg.getByText(h, { exact: true });
|
||||||
|
if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); t = await tierOf(pg, name); }
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
/** True when the page text matches re within ms of now (a message can be replaced by a row change). */
|
||||||
|
exports.sawText = (pg, re, ms) =>
|
||||||
|
pg.waitForFunction((src) => new RegExp(src, 'i').test(document.body.innerText), re.source, { timeout: ms, polling: 50 }).then(() => true, () => false);
|
||||||
exports.isDm = (tier) => /^direct messages$/i.test(tier);
|
exports.isDm = (tier) => /^direct messages$/i.test(tier);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Failure-mode scenarios for the estate join paths (estate tap and inbox Accept), run by run.sh
|
// 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.
|
// against fakehs.cjs with the env flag each mode needs.
|
||||||
const { chromium } = require('playwright');
|
const { chromium } = require('playwright');
|
||||||
const { expect, done, isDm, mDirectPuts, joinsOf } = require('./check.cjs');
|
const { expect, done, isDm, mDirectPuts, joinsOf, revealedTier, sawText } = require('./check.cjs');
|
||||||
const MODE = process.env.MODE;
|
const MODE = process.env.MODE;
|
||||||
const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
||||||
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
||||||
|
|
@ -20,9 +20,9 @@ setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 240000);
|
||||||
await open();
|
await open();
|
||||||
const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
|
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 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 tierOf = (name) => revealedTier(pg, 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 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); } } const r = { friend: await tierOf('Friend DM'), slow: await tierOf('Slow DM') }; log('TIER', tag, '| Friend DM', r.friend, '| Slow DM', r.slow); return r; };
|
const tiers = async (tag) => { const r = { friend: await tierOf('Friend DM'), slow: await tierOf('Slow DM') }; log('TIER', tag, '| Friend DM', r.friend, '| Slow DM', r.slow); return r; };
|
||||||
const neverUnfiled = () => mDirectPuts().every((b) => b.includes('!dm1:test'));
|
const neverUnfiled = () => mDirectPuts().every((b) => b.includes('!dm1:test'));
|
||||||
const acceptInInbox = async (name) => {
|
const acceptInInbox = async (name) => {
|
||||||
// reach the inbox through the navigator's own Inbox tab
|
// reach the inbox through the navigator's own Inbox tab
|
||||||
|
|
@ -47,9 +47,8 @@ setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 240000);
|
||||||
expect('the retry joins once', joinsOf('!dm1:test') === 1, String(joinsOf('!dm1:test')));
|
expect('the retry joins once', joinsOf('!dm1:test') === 1, String(joinsOf('!dm1:test')));
|
||||||
expect('Friend DM is filed under Direct Messages after the retry', isDm(r.friend), r.friend);
|
expect('Friend DM is filed under Direct Messages after the retry', isDm(r.friend), r.friend);
|
||||||
} else if (MODE === 'jdrop') {
|
} else if (MODE === 'jdrop') {
|
||||||
await tap('Friend DM'); await pg.waitForTimeout(4000);
|
await tap('Friend DM'); const saw = await sawText(pg, /tap again to retry/, 3000); log('AFTER_DROP', saw);
|
||||||
const bb = await body(); const k = bb.search(/tap again to retry/i); log('AFTER_DROP', k >= 0 ? bb.slice(Math.max(0, k - 60), k + 80) : 'no message');
|
expect('a dropped join shows the retryable message', saw, (await body()).slice(0, 200));
|
||||||
expect('a dropped join shows the retryable message', k >= 0, bb.slice(0, 200));
|
|
||||||
await pg.waitForTimeout(6000); const r1 = await tiers('after drop'); await open(); const r2 = await tiers('after reload');
|
await pg.waitForTimeout(6000); const r1 = await tiers('after drop'); await open(); const r2 = await tiers('after reload');
|
||||||
expect('the entry is kept after the drop', isDm(r1.friend) && neverUnfiled(), r1.friend);
|
expect('the entry is kept after the drop', isDm(r1.friend) && neverUnfiled(), r1.friend);
|
||||||
expect('Friend DM is under Direct Messages after the reload', isDm(r2.friend), r2.friend);
|
expect('Friend DM is under Direct Messages after the reload', isDm(r2.friend), r2.friend);
|
||||||
|
|
@ -66,7 +65,7 @@ setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 240000);
|
||||||
} else if (MODE === 'pjreload') {
|
} else if (MODE === 'pjreload') {
|
||||||
await tap('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open();
|
await tap('Slow DM'); await pg.waitForTimeout(2000); log('RELOAD', t()); await open();
|
||||||
await pg.waitForTimeout(8000); await tiers('pjreload');
|
await pg.waitForTimeout(8000); await tiers('pjreload');
|
||||||
const l = pg.locator('[aria-label="Join Slow DM"]'); const n = await l.count(); log('SLOW_DM_STILL_JOINABLE', n);
|
const n = await (await reveal('Slow DM')).count(); log('SLOW_DM_STILL_JOINABLE', n);
|
||||||
expect('a join cut off by a reload stays joinable', n === 1, String(n));
|
expect('a join cut off by a reload stays joinable', n === 1, String(n));
|
||||||
}
|
}
|
||||||
await quit(done());
|
await quit(done());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { chromium } = require('playwright');
|
const { chromium } = require('playwright');
|
||||||
const { expect, done, isDm, mDirectPuts, joinsOf, hsLog } = require('./check.cjs');
|
const { expect, done, isDm, mDirectPuts, hsLog, revealedTier, sawText } = require('./check.cjs');
|
||||||
const MODE = process.env.MODE; const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
const MODE = process.env.MODE; const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
||||||
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
||||||
const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
|
const HEADERS = ['GameCult', 'Projects', 'nibu', 'eureka', 'Nibu', 'Eureka', 'Elsewhere', 'Direct Messages'];
|
||||||
|
|
@ -13,8 +13,7 @@ setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 120000);
|
||||||
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.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.reload(); await pg.waitForTimeout(9000);
|
await pg.goto('http://127.0.0.1:8080/#/home/'); await pg.reload(); await pg.waitForTimeout(9000);
|
||||||
const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
|
const body = async () => (await pg.evaluate(() => document.body.innerText)).replace(/\s+/g, ' ');
|
||||||
const tierOf = (n) => 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'; }, n);
|
const tiers = async (tag) => { const f = await revealedTier(pg, 'Friend DM'); log('TIER', tag, '| Friend DM', f); return f; };
|
||||||
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); } } const f = await tierOf('Friend DM'); log('TIER', tag, '| Friend DM', f); return f; };
|
|
||||||
let joined = false;
|
let joined = false;
|
||||||
if (MODE === 'deeplink') {
|
if (MODE === 'deeplink') {
|
||||||
for (const route of ['direct', 'home']) {
|
for (const route of ['direct', 'home']) {
|
||||||
|
|
@ -32,9 +31,9 @@ setTimeout(() => { console.log('DRIVER_TIMEOUT'); quit(3); }, 120000);
|
||||||
} else if (MODE === 'gw504') {
|
} else if (MODE === 'gw504') {
|
||||||
const HL = HEADERS; let l = pg.locator('[aria-label="Join Friend DM"]');
|
const HL = HEADERS; let l = pg.locator('[aria-label="Join Friend DM"]');
|
||||||
for (let r = 0; r < 2 && !(await l.count()); r++) for (const h of HL) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); } }
|
for (let r = 0; r < 2 && !(await l.count()); r++) for (const h of HL) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); } }
|
||||||
log('JOINCTRL', await l.count()); await l.first().click(); log('CLICK', t()); await pg.waitForTimeout(4000);
|
log('JOINCTRL', await l.count()); await l.first().click(); log('CLICK', t());
|
||||||
const bb = await body(); const k = bb.search(/504|Gateway|connection dropped|Could not join/i); log('AFTER_504', k >= 0 ? bb.slice(Math.max(0, k - 60), k + 120) : 'no message');
|
const saw = await sawText(pg, /tap again to retry/, 3000); log('AFTER_504', saw);
|
||||||
expect('a gateway timeout shows the retryable message', /tap again to retry/i.test(bb), bb.slice(0, 200));
|
expect('a gateway timeout shows the retryable message', saw, (await body()).slice(0, 200));
|
||||||
await pg.waitForTimeout(4000); const f = await tiers('after 504 (join delivered by sync)');
|
await pg.waitForTimeout(4000); const f = await tiers('after 504 (join delivered by sync)');
|
||||||
expect('the DM stays filed after a gateway timeout', mDirectPuts().every((b) => b.includes('!dm1:test')) && mDirectPuts().length > 0, JSON.stringify(mDirectPuts()));
|
expect('the DM stays filed after a gateway timeout', mDirectPuts().every((b) => b.includes('!dm1:test')) && mDirectPuts().length > 0, JSON.stringify(mDirectPuts()));
|
||||||
expect('the DM is filed under Direct Messages once sync delivers it', isDm(f), f);
|
expect('the DM is filed under Direct Messages once sync delivers it', isDm(f), f);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { chromium } = require('playwright');
|
const { chromium } = require('playwright');
|
||||||
const { expect, done, isDm } = require('./check.cjs');
|
const { expect, done, isDm, revealedTier } = require('./check.cjs');
|
||||||
const MODE = process.env.MODE; // twodm | reload
|
const MODE = process.env.MODE; // twodm | reload
|
||||||
const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
const T0 = Date.now(); const t = () => `+${Date.now() - T0}ms`;
|
||||||
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
const log = (...a) => console.log(`[${MODE}]`, ...a);
|
||||||
|
|
@ -15,7 +15,7 @@ let tierBefore;
|
||||||
await open();
|
await open();
|
||||||
const text = () => pg.evaluate(() => document.body.innerText);
|
const text = () => pg.evaluate(() => document.body.innerText);
|
||||||
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 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 tierOf = (name) => revealedTier(pg, 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 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') {
|
if (MODE === 'twodm') {
|
||||||
await tap('Slow DM'); await pg.waitForTimeout(300); await tap('Late DM');
|
await tap('Slow DM'); await pg.waitForTimeout(300); await tap('Late DM');
|
||||||
|
|
@ -37,7 +37,6 @@ let tierBefore;
|
||||||
log('RELOAD', t()); await open();
|
log('RELOAD', t()); await open();
|
||||||
await pg.waitForTimeout(30000);
|
await pg.waitForTimeout(30000);
|
||||||
}
|
}
|
||||||
for (const h of HEADERS) { const hl = pg.getByText(h, { exact: true }); if (await hl.count()) { await hl.first().click(); await pg.waitForTimeout(250); } }
|
|
||||||
const slow = await tierOf('Slow DM'), late = await tierOf('Late DM'), friend = await tierOf('Friend DM');
|
const slow = await tierOf('Slow DM'), late = await tierOf('Late DM'), friend = await tierOf('Friend DM');
|
||||||
log('TIER Slow DM', slow, '| Late DM', late, '| Friend DM', friend);
|
log('TIER Slow DM', slow, '| Late DM', late, '| Friend DM', friend);
|
||||||
if (MODE === 'twodm') { expect('Slow DM is filed under Direct Messages', isDm(slow), slow); expect('Late DM is filed under Direct Messages', isDm(late), late); }
|
if (MODE === 'twodm') { expect('Slow DM is filed under Direct Messages', isDm(slow), slow); expect('Late DM is filed under Direct Messages', isDm(late), late); }
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,8 @@ const dmClient = (opts: {
|
||||||
writes.push(structuredClone(content));
|
writes.push(structuredClone(content));
|
||||||
direct = structuredClone(content);
|
direct = structuredClone(content);
|
||||||
},
|
},
|
||||||
joinRoom: async (roomId: string, opts?: unknown) => {
|
joinRoom: async (roomId: string, joinArgs?: unknown) => {
|
||||||
joinOpts.push(opts);
|
joinOpts.push(joinArgs);
|
||||||
// the join lands only after m.direct has been written: record what it held
|
// the join lands only after m.direct has been written: record what it held
|
||||||
joined.push(JSON.stringify(direct ?? {}));
|
joined.push(JSON.stringify(direct ?? {}));
|
||||||
if (opts.refuse)
|
if (opts.refuse)
|
||||||
|
|
@ -179,7 +179,11 @@ describe('activateEstateRoom when the join gets no answer', () => {
|
||||||
|
|
||||||
describe('activateEstateRoom refusal bounds', () => {
|
describe('activateEstateRoom refusal bounds', () => {
|
||||||
it.each([400, 429, 499])('unfiles the room on a %i that carries an errcode', async (status) => {
|
it.each([400, 429, 499])('unfiles the room on a %i that carries an errcode', async (status) => {
|
||||||
const c = dmClient({ status, errcode: 'M_LIMIT_EXCEEDED', invites: { '!dm1:test': '@friend:test' } });
|
const c = dmClient({
|
||||||
|
status,
|
||||||
|
errcode: 'M_LIMIT_EXCEEDED',
|
||||||
|
invites: { '!dm1:test': '@friend:test' },
|
||||||
|
});
|
||||||
const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
|
const result = await activateEstateRoom(c.mx, tap('!dm1:test'));
|
||||||
expect(result.kind).toBe('refused');
|
expect(result.kind).toBe('refused');
|
||||||
expect((result as { message: string }).message).not.toMatch(/tap again to retry/);
|
expect((result as { message: string }).message).not.toMatch(/tap again to retry/);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue