web: open Agent at recent history and load older pages on demand
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
AgentRoomClient, AgentReadSourceChangedError, advanceAgentCursor, agentAuthFrame, agentDisplayMessages, agentUnreadCount,
|
||||
applyAgentEvent, applyAgentPage, applyAgentReadState, createAgentReadSession, createAgentReadWriter,
|
||||
createAgentRoomState, mergeAgentMessages, normalizeAgentCursor, parseAgentRoomEvent, syncAgentHistory,
|
||||
applyAgentHistoryPage, agentUnreadCountIsPartial,
|
||||
} from "../src/lib/agent-room.ts";
|
||||
import { createAgentOptimisticOutbox } from "../src/lib/agent-optimistic-outbox.ts";
|
||||
|
||||
@@ -32,7 +33,7 @@ test("tool starts and results merge across replay without downgrading completed
|
||||
assert.equal(messages[0].createdAt, start.createdAt);
|
||||
});
|
||||
|
||||
test("initial synchronization reads every page including invisible entries and counts only unread assistant messages", async () => {
|
||||
test("forward recovery reads every missing page including invisible entries and counts only unread assistant messages", async () => {
|
||||
const requests = [];
|
||||
const responses = [page("2", [message(1), message(2, "user")], true), page("4", [], true), page("7", [message(5), message(6, "tool"), message(7)], false)];
|
||||
let state = createAgentRoomState();
|
||||
@@ -47,6 +48,78 @@ test("initial synchronization reads every page including invisible entries and c
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
});
|
||||
|
||||
test("recent history is one cursorless window and older history uses the returned source boundary", async () => {
|
||||
const requests = [];
|
||||
const client = new AgentRoomClient({ fetch: async (url, options) => {
|
||||
requests.push({ url, signal: options.signal });
|
||||
return new Response(JSON.stringify({ ...page("1200", [message(1200)], true), beforeCursor: "1101" }));
|
||||
} });
|
||||
const controller = new AbortController();
|
||||
let state = applyAgentHistoryPage(createAgentRoomState(), await client.history(undefined, controller.signal));
|
||||
assert.equal(requests.length, 1, "hasMore must not drain old history");
|
||||
assert.equal(requests[0].url, "/api/v1/agent/messages?limit=100");
|
||||
assert.equal(requests[0].signal, controller.signal);
|
||||
assert.equal(state.historyLoaded, true);
|
||||
assert.equal(state.cursor, "1200");
|
||||
assert.equal(state.beforeCursor, "1101", "boundary is not derived from visible message 1200");
|
||||
assert.equal(state.hasOlder, true);
|
||||
await client.history(state.beforeCursor, controller.signal);
|
||||
assert.equal(requests[1].url, "/api/v1/agent/messages?before=1101&limit=100");
|
||||
});
|
||||
|
||||
test("delayed older pages preserve newer edits, live cursor, source and read state", () => {
|
||||
let state = applyAgentHistoryPage(sourceRoom(), { ...page("100", [message(100)], true), beforeCursor: "90" });
|
||||
state = applyAgentPage(state, page("110", [message(100, "assistant", { content: "New streamed text" }), message(110)]));
|
||||
const next = applyAgentHistoryPage(state, { ...page("89", [message(80), message(100, "assistant", { content: "Old snapshot" })], true), beforeCursor: "70" }, "90");
|
||||
assert.equal(next.messages.find(m => m.id === "entry:100").content, "New streamed text");
|
||||
assert.equal(next.cursor, "110");
|
||||
assert.equal(next.readCursor, "90");
|
||||
assert.equal(next.sourceId, sourceA);
|
||||
assert.equal(next.beforeCursor, "70");
|
||||
assert.equal(next.hasOlder, true);
|
||||
assert.equal(applyAgentHistoryPage(next, page("89", [message(80)], false), "90"), next, "retired boundary cannot replace the next window");
|
||||
});
|
||||
|
||||
test("empty internal pages advance older boundaries without chasing another page or moving read state", () => {
|
||||
const initial = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [], true), beforeCursor: "1101" });
|
||||
assert.equal(initial.historyLoaded, true);
|
||||
assert.equal(initial.messages.length, 0);
|
||||
const next = applyAgentHistoryPage(initial, { ...page("1100", [], true), beforeCursor: "1001" }, "1101");
|
||||
assert.equal(next.beforeCursor, "1001");
|
||||
assert.equal(next.cursor, "1200");
|
||||
assert.equal(next.readCursor, "0");
|
||||
assert.equal(next.hasOlder, true);
|
||||
const end = applyAgentHistoryPage(next, { ...page("0"), beforeCursor: "1001" }, "1001");
|
||||
assert.equal(end.hasOlder, false);
|
||||
assert.equal(end.cursor, "1200");
|
||||
});
|
||||
|
||||
test("malformed or stalled older pages cannot change history or delivery boundaries", () => {
|
||||
const state = applyAgentHistoryPage(createAgentRoomState(), { ...page("100", [message(100)], true), beforeCursor: "90" });
|
||||
for (const bad of [
|
||||
{ ...page("89", [], true), beforeCursor: "90" },
|
||||
{ ...page("89", [], true), beforeCursor: null },
|
||||
{ ...page("91", [], false), beforeCursor: "80" },
|
||||
{ ...page("89", [], false), beforeCursor: "91" },
|
||||
{ ...page("bad", [], false), beforeCursor: "80" },
|
||||
]) assert.throws(() => applyAgentHistoryPage(state, bad, "90"), /cursor|boundary/);
|
||||
assert.throws(() => applyAgentHistoryPage(state, { ...page("89"), conversationId: "different" }, "90"), /conversation changed/);
|
||||
assert.equal(state.beforeCursor, "90");
|
||||
assert.equal(state.cursor, "100");
|
||||
});
|
||||
|
||||
test("unread counts disclose unknown older replies until reading the latest rendered window", () => {
|
||||
let state = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [message(1199), message(1200, "user")], true), beforeCursor: "1101" });
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
assert.equal(agentUnreadCountIsPartial(state), true);
|
||||
state = applyAgentReadState(state, { cursor: "1100", updatedAt: null });
|
||||
assert.equal(agentUnreadCountIsPartial(state), false);
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
state = applyAgentReadState(state, { cursor: "1200", updatedAt: null });
|
||||
assert.equal(agentUnreadCount(state), 0);
|
||||
assert.equal(state.hasOlder, true, "reading latest does not fetch or discard older pages");
|
||||
});
|
||||
|
||||
test("queued user messages keep stable IDs and disappear if withdrawn or promoted", () => {
|
||||
const pending = message(1, "user", { id: "inbound:sybil:request1", metadata: { kind: "message", status: "queued" } });
|
||||
let state = applyAgentEvent(createAgentRoomState(), { type: "activity", conversationId: "1", activity: activity([pending]) });
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
// Local production preview; all APIs/sockets are synthetic and external requests are blocked.
|
||||
// PLAYWRIGHT_MODULE=/path/to/playwright/index.mjs WEB_TEST_URL=http://127.0.0.1:5196
|
||||
// TRANSCRIPT_EVIDENCE=/tmp/evidence TRANSCRIPT_BASELINE=1 node tests/agent-transcript.browser.mjs
|
||||
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const target = new URL(process.env.WEB_TEST_URL || 'http://127.0.0.1:5196/');
|
||||
assert.ok(target.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(target.hostname));
|
||||
const baseline = process.env.TRANSCRIPT_BASELINE === '1';
|
||||
const evidence = process.env.TRANSCRIPT_EVIDENCE;
|
||||
if (evidence) await mkdir(evidence, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_EXECUTABLE || undefined });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, serviceWorkers: 'block' });
|
||||
const page = await context.newPage();
|
||||
const errors = [], requests = [], reads = [], socketCursors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
const idle = { state: 'idle', isThinking: false, queued: 0, backgroundWorkers: 0, tools: [], queuedMessages: [] };
|
||||
const sourceId = '00000000-0000-4000-8000-000000000001';
|
||||
const row = (id, content) => ({ id: `entry:${id}`, role: id % 3 ? 'assistant' : 'user',
|
||||
createdAt: new Date(Date.UTC(2026, 9, 16, 0, 0, id)).toISOString(),
|
||||
content: content || `Fixture message ${id}\n\n${'Synthetic transcript text for delayed loading. '.repeat(id % 11 === 0 ? 30 : 3)}`,
|
||||
metadata: { kind: 'message', entryId: id, status: 'complete' } });
|
||||
const history = Array.from({ length: 1200 }, (_, i) => row(i + 1));
|
||||
let live, readCursor = '0', failOlder = false, imageBytes, emptyRecent = false;
|
||||
const start = performance.now();
|
||||
const elapsed = () => Math.round(performance.now() - start);
|
||||
await context.addInitScript(() => {
|
||||
localStorage.clear(); sessionStorage.clear();
|
||||
window.fixtureTiming = {};
|
||||
const sample = () => {
|
||||
const scroller = document.querySelector('.agent-room__scroll');
|
||||
if (scroller) {
|
||||
const box = scroller.getBoundingClientRect();
|
||||
const visible = [...scroller.querySelectorAll('[data-agent-message-id]')].filter(e => {
|
||||
const r = e.getBoundingClientRect(); return r.bottom > box.top && r.top < box.bottom;
|
||||
});
|
||||
if (visible.length && !window.fixtureTiming.first) window.fixtureTiming.first = { ms: performance.now(), ids: visible.map(e => e.dataset.agentMessageId) };
|
||||
if (visible.some(e => e.dataset.agentMessageId === 'entry:1200') && !window.fixtureTiming.latestMs) window.fixtureTiming.latestMs = performance.now();
|
||||
}
|
||||
if (!window.fixtureTiming.latestMs) requestAnimationFrame(sample);
|
||||
};
|
||||
requestAnimationFrame(sample);
|
||||
});
|
||||
await page.route('**/*', async route => {
|
||||
const req = route.request(), url = new URL(req.url());
|
||||
if (url.origin !== target.origin) return route.abort();
|
||||
if (!url.pathname.startsWith('/api/')) return route.continue();
|
||||
const path = url.pathname, method = req.method();
|
||||
assert.equal(req.headers().authorization, undefined, 'Fixture must never use a real credential');
|
||||
let value;
|
||||
if (path.endsWith('/content')) {
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
return route.fulfill({ contentType: 'image/png', body: imageBytes });
|
||||
} else if (path.endsWith('/agent/messages')) {
|
||||
assert.equal(method, 'GET', 'Fixture must not send messages');
|
||||
const after = url.searchParams.get('after'), before = url.searchParams.get('before'), limit = Number(url.searchParams.get('limit') || 200);
|
||||
const record = { query: url.search, requestedMs: elapsed() }; requests.push(record);
|
||||
await new Promise(resolve => setTimeout(resolve, before ? 1600 : 800));
|
||||
if (before && failOlder) { failOlder = false; return route.fulfill({ status: 503, json: { message: 'Fixture older page unavailable' } }); }
|
||||
const eligible = history.filter(m => after !== null ? m.metadata.entryId > Number(after) : before !== null ? m.metadata.entryId < Number(before) : true);
|
||||
const messages = after !== null ? eligible.slice(0, limit) : eligible.slice(-limit);
|
||||
value = { conversationId: 'fixture-room', messages, cursor: String(messages.at(-1)?.metadata.entryId ?? after ?? 0),
|
||||
beforeCursor: messages.length ? String(messages[0].metadata.entryId) : null, hasMore: eligible.length > messages.length };
|
||||
if (emptyRecent && before === null && after === null) value.messages = [];
|
||||
Object.assign(record, { responseMs: elapsed(), rows: messages.length, bytes: Buffer.byteLength(JSON.stringify(value)) });
|
||||
} else if (path.endsWith('/read-state')) {
|
||||
if (method === 'PUT') { readCursor = req.postDataJSON().cursor; reads.push({ cursor: readCursor, ms: elapsed() }); }
|
||||
value = { cursor: readCursor, sourceId, updatedAt: null };
|
||||
} else if (path.endsWith('/agent')) value = { id: 'fixture-room', title: 'Sybil Agent', activity: idle, protocol: 'pi-room.v1', websocket: '/v1/agent/ws' };
|
||||
else if (path.endsWith('/auth/session')) value = { authenticated: true, mode: 'open' };
|
||||
else if (path.endsWith('/models')) value = { models: [], providers: {} };
|
||||
else if (path.endsWith('/tools') || path.endsWith('/chat-tools')) value = { tools: [] };
|
||||
else value = [];
|
||||
await route.fulfill({ json: value });
|
||||
});
|
||||
await page.routeWebSocket('**/api/v1/agent/ws', socket => {
|
||||
live = socket;
|
||||
socket.onMessage(raw => { const frame = JSON.parse(String(raw)); if (frame.type === 'auth') {
|
||||
assert.equal(frame.token, null); socketCursors.push(frame.after);
|
||||
socket.send(JSON.stringify({ type: 'ready', conversationId: 'fixture-room' }));
|
||||
} });
|
||||
});
|
||||
const send = messages => {
|
||||
for (const message of messages) { const index = history.findIndex(m => m.id === message.id); if (index < 0) history.push(message); else history[index] = message; }
|
||||
live.send(JSON.stringify({ type: 'messages', conversationId: 'fixture-room', messages,
|
||||
cursor: String(messages.at(-1).metadata.entryId), beforeCursor: String(messages[0].metadata.entryId), hasMore: false }));
|
||||
};
|
||||
const position = () => page.locator('.agent-room__scroll').evaluate(el => ({ top: el.scrollTop, distance: el.scrollHeight - el.scrollTop - el.clientHeight,
|
||||
rows: el.querySelectorAll('[data-agent-entry-id]').length }));
|
||||
const anchor = () => page.locator('.agent-room__scroll').evaluate(el => {
|
||||
const top = el.getBoundingClientRect().top;
|
||||
const entry = [...el.querySelectorAll('[data-agent-entry-id]')].find(e => e.getBoundingClientRect().bottom > top + 8);
|
||||
return { id: entry.dataset.agentEntryId, offset: entry.querySelector('[data-agent-message-id]').getBoundingClientRect().top - top };
|
||||
});
|
||||
const anchorDrift = saved => page.locator('.agent-room__scroll').evaluate((el, saved) => {
|
||||
const entry = [...el.querySelectorAll('[data-agent-entry-id]')].find(e => e.dataset.agentEntryId === saved.id);
|
||||
return entry.querySelector('[data-agent-message-id]').getBoundingClientRect().top - el.getBoundingClientRect().top - saved.offset;
|
||||
}, saved);
|
||||
const metrics = { mode: baseline ? 'before' : 'after', fixtureRows: 1200, pageDelayMs: 800, olderDelayMs: 1600 };
|
||||
try {
|
||||
await page.goto(new URL('/?agent=root', target).href, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('[data-testid="agent-sidebar-item"]').click();
|
||||
await page.waitForFunction(() => window.fixtureTiming?.first);
|
||||
if (evidence) await page.screenshot({ path: `${evidence}/${metrics.mode}-first.png` });
|
||||
await page.waitForFunction(() => window.fixtureTiming?.latestMs, { timeout: 20000 });
|
||||
await page.getByRole('status').filter({ hasText: /^Connected$/ }).waitFor();
|
||||
await page.waitForTimeout(400);
|
||||
Object.assign(metrics, await page.evaluate(() => window.fixtureTiming), { initial: await position(), initialRequests: requests.map(r => ({ ...r })), socketCursors: [...socketCursors] });
|
||||
if (evidence) await page.screenshot({ path: `${evidence}/${metrics.mode}-latest.png` });
|
||||
if (!baseline) {
|
||||
assert.equal(requests.length, 1, 'No eager older/forward chain on initial load');
|
||||
assert.equal(new URLSearchParams(requests[0].query).has('after'), false);
|
||||
assert.equal(socketCursors[0], '1200');
|
||||
assert.ok(metrics.initial.rows <= 100);
|
||||
assert.ok(metrics.initial.distance < 2);
|
||||
await page.waitForTimeout(1800);
|
||||
assert.equal(requests.length, 1, 'Idle initial view must not drain history');
|
||||
await page.locator('.agent-room__scroll').evaluate(el => { el.scrollTop = 0; });
|
||||
await page.waitForTimeout(150);
|
||||
const saved = await anchor(), readBefore = readCursor;
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).click();
|
||||
send([row(1201)]);
|
||||
await page.waitForTimeout(500);
|
||||
assert.equal(readCursor, readBefore, 'Incoming while reading must stay unread');
|
||||
metrics.incomingDriftPx = await anchorDrift(saved);
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).waitFor();
|
||||
await page.waitForTimeout(400);
|
||||
metrics.prependDriftPx = await anchorDrift(saved);
|
||||
metrics.afterPrepend = await position();
|
||||
assert.ok(Math.abs(metrics.incomingDriftPx) < 2);
|
||||
assert.ok(Math.abs(metrics.prependDriftPx) < 2);
|
||||
assert.equal(readCursor, readBefore);
|
||||
assert.equal(requests.length, 2, 'One user action loads one older page');
|
||||
assert.equal(await page.locator('[data-agent-entry-id="entry:1001"] [data-animation="enter"]').count(), 0);
|
||||
const beforeGrowth = await anchor();
|
||||
send([row(1201, 'Dynamic streamed content.\n\n'.repeat(80))]);
|
||||
await page.waitForTimeout(350);
|
||||
metrics.streamingReadingDriftPx = await anchorDrift(beforeGrowth);
|
||||
assert.ok(Math.abs(metrics.streamingReadingDriftPx) < 2);
|
||||
await page.locator('.agent-room__scroll').evaluate(el => { el.scrollTop = el.scrollHeight; });
|
||||
await page.waitForTimeout(250);
|
||||
send([row(1201, 'Dynamic streamed content continues.\n\n'.repeat(100))]);
|
||||
await page.waitForTimeout(400);
|
||||
metrics.streamingPinnedDistancePx = (await position()).distance;
|
||||
assert.ok(metrics.streamingPinnedDistancePx < 2);
|
||||
const order = await page.locator('.agent-room__timeline').evaluate(el => {
|
||||
const ids = [...el.querySelectorAll('[data-agent-entry-id]')].map(e => Number(e.dataset.agentEntryId.split(':')[1]));
|
||||
const range = document.createRange(); range.selectNodeContents(el);
|
||||
return { ids, text: range.toString(), direction: getComputedStyle(el).flexDirection,
|
||||
skipped: [...el.querySelectorAll('[data-agent-message-id]')].filter(e => !e.checkVisibility({ contentVisibilityAuto: true })).length };
|
||||
});
|
||||
assert.deepEqual(order.ids, [...order.ids].sort((a, b) => a - b));
|
||||
assert.ok(order.text.indexOf('Fixture message 1001') < order.text.indexOf('Fixture message 1200'));
|
||||
assert.equal(order.direction, 'column');
|
||||
assert.equal(await page.getByRole('button', { name: /latest/i }).count(), 0);
|
||||
metrics.skippedOffscreenRows = order.skipped;
|
||||
assert.ok(order.skipped > 0, 'Offscreen rows should skip layout/paint');
|
||||
const ax = await page.locator('.agent-room__scroll').ariaSnapshot();
|
||||
assert.ok(ax.includes('Fixture message 1001') && ax.includes('Fixture message 1200'));
|
||||
assert.ok(ax.indexOf('Fixture message 1001') < ax.indexOf('Fixture message 1200'));
|
||||
|
||||
// Authenticated attachment bytes are synthetic, delayed, and never fetched off origin.
|
||||
imageBytes = Buffer.from(await page.evaluate(() => {
|
||||
const canvas = document.createElement('canvas'); canvas.width = 240; canvas.height = 180;
|
||||
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#8974da'; ctx.fillRect(0, 0, 240, 180);
|
||||
return canvas.toDataURL('image/png').split(',')[1];
|
||||
}), 'base64');
|
||||
const file = { id: 'att_12345678-1234-4234-8234-123456789abc', filename: 'fixture.png', mimeType: 'image/png', kind: 'image',
|
||||
sizeBytes: imageBytes.length, sha256: createHash('sha256').update(imageBytes).digest('hex') };
|
||||
send([{ ...row(1202), metadata: { ...row(1202).metadata, agentAttachments: [file] } }]);
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole('button', { name: 'Preview', exact: true }).click();
|
||||
await page.locator('.agent-attachment__preview').waitFor();
|
||||
await page.waitForTimeout(250);
|
||||
metrics.attachmentPinnedDistancePx = (await position()).distance;
|
||||
assert.ok(metrics.attachmentPinnedDistancePx < 2);
|
||||
send([row(1203, 'Following the delayed attachment.\n\n'.repeat(60))]);
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('.agent-room__scroll').evaluate(el => {
|
||||
const entry = el.querySelector('[data-agent-entry-id="entry:1203"]');
|
||||
el.scrollTop += entry.getBoundingClientRect().top - el.getBoundingClientRect().top + 80;
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
const attachmentAnchor = await anchor();
|
||||
// Collapse an earlier preview while reading the following message.
|
||||
await page.getByRole('button', { name: 'Hide preview', exact: true }).evaluate(el => el.click());
|
||||
await page.waitForTimeout(300);
|
||||
metrics.attachmentReadingDriftPx = await anchorDrift(attachmentAnchor);
|
||||
assert.ok(Math.abs(metrics.attachmentReadingDriftPx) < 2);
|
||||
|
||||
// A failed older page is locally retryable without clearing the recent window.
|
||||
await page.locator('.agent-room__scroll').evaluate(el => { el.scrollTop = 0; });
|
||||
await page.waitForTimeout(200);
|
||||
const rowsBeforeFailure = (await position()).rows;
|
||||
failOlder = true;
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).click();
|
||||
await page.getByRole('alert').filter({ hasText: 'Fixture older page unavailable' }).waitFor();
|
||||
assert.equal((await position()).rows, rowsBeforeFailure);
|
||||
const socketsBefore = socketCursors.length;
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).click();
|
||||
await page.waitForTimeout(150);
|
||||
live.close({ code: 1012, reason: 'Fixture reconnect' });
|
||||
await page.getByRole('button', { name: 'Reconnect to Sybil Agent', exact: true }).click();
|
||||
await page.waitForTimeout(2200);
|
||||
assert.ok(socketCursors.length > socketsBefore);
|
||||
assert.equal(socketCursors.at(-1), '1203');
|
||||
assert.equal((await position()).rows, rowsBeforeFailure, 'Retired older response must not publish into the new session');
|
||||
assert.equal(await page.getByRole('alert').count(), 0);
|
||||
await page.locator('.agent-room__scroll').evaluate(el => { el.scrollTop = 0; });
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).click();
|
||||
await page.waitForFunction(() => document.querySelector('[data-agent-entry-id="entry:901"]'));
|
||||
metrics.retiredOlderIgnored = true;
|
||||
metrics.olderFailureRetry = true;
|
||||
assert.equal(requests.filter(r => r.query.includes('after=0')).length, 0);
|
||||
await page.locator('.agent-room__scroll').evaluate(el => { el.scrollTop = el.scrollHeight; });
|
||||
await page.waitForTimeout(200);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(400);
|
||||
metrics.mobilePinnedDistancePx = (await position()).distance;
|
||||
assert.ok(metrics.mobilePinnedDistancePx < 2);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
assert.equal(await page.getByRole('button', { name: /latest/i }).count(), 0);
|
||||
if (evidence) await page.screenshot({ path: `${evidence}/after-mobile.png` });
|
||||
// A source window can contain only internal entries. It stays bounded and
|
||||
// must not mark unloaded public history read or animate an older prepend.
|
||||
emptyRecent = true; readCursor = '0';
|
||||
const requestsBeforeEmpty = requests.length;
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(new URL('/?agent=root', target).href, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('[data-testid="agent-sidebar-item"]').click();
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).waitFor();
|
||||
await page.waitForTimeout(1000);
|
||||
assert.equal((await position()).rows, 0);
|
||||
assert.equal(requests.length, requestsBeforeEmpty + 1);
|
||||
assert.equal(readCursor, '0');
|
||||
await page.getByRole('button', { name: 'Load older messages', exact: true }).click();
|
||||
await page.waitForFunction(() => document.querySelectorAll('[data-agent-entry-id]').length > 0);
|
||||
assert.equal(await page.locator('[data-animation="enter"]').count(), 0);
|
||||
metrics.internalOnlyWindowBounded = true;
|
||||
metrics.internalOnlyWindowUnacknowledged = true;
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
metrics.requests = requests; metrics.reads = reads; metrics.errors = errors;
|
||||
if (evidence) await writeFile(`${evidence}/${metrics.mode}.json`, JSON.stringify(metrics, null, 2));
|
||||
console.log(JSON.stringify(metrics, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({ metrics, requests, errors, body: (await page.locator('body').innerText()).slice(0, 1800) }));
|
||||
throw error;
|
||||
} finally { await context.close(); await browser.close(); }
|
||||
Reference in New Issue
Block a user