mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 18:04:55 +00:00
test(app): allow the Updating messages toast when the daemon answers slowly
A freshness check unanswered after 500ms reports Updating messages by design, but the resume spec asserted no toast regardless of how long the daemon took. On a loaded runner a cold reload's answer took 512ms, so the toast was correct and the spec failed. Assert no toast only when the answer came back promptly, using answer times the gate records.
This commit is contained in:
@@ -63,7 +63,7 @@ test.describe("Agent timeline resume", () => {
|
||||
|
||||
await test.step("back from the background", async () => {
|
||||
const requests = rememberTimelineRequestCounts(gate);
|
||||
const expectNoUpdatingToast = await watchForUpdatingToast(page);
|
||||
const expectNoUpdatingToast = await watchForUpdatingToast(page, gate, requests);
|
||||
|
||||
await hideTabAndReturn(page);
|
||||
await waitForResumeAnswer(gate, requests);
|
||||
@@ -75,7 +75,7 @@ test.describe("Agent timeline resume", () => {
|
||||
|
||||
await test.step("back from a dropped connection", async () => {
|
||||
const requests = rememberTimelineRequestCounts(gate);
|
||||
const expectNoUpdatingToast = await watchForUpdatingToast(page);
|
||||
const expectNoUpdatingToast = await watchForUpdatingToast(page, gate, requests);
|
||||
|
||||
await disconnectViewedTimeline(page, gate);
|
||||
await restoreUnchangedViewedTimeline(page, gate);
|
||||
@@ -88,7 +88,7 @@ test.describe("Agent timeline resume", () => {
|
||||
await test.step("back from reopening the app", async () => {
|
||||
const requests = rememberTimelineRequestCounts(gate);
|
||||
|
||||
const expectNoUpdatingToast = await reopenAppWatchingForUpdatingToast(page);
|
||||
const expectNoUpdatingToast = await reopenAppWatchingForUpdatingToast(page, gate, requests);
|
||||
await waitForResumeAnswer(gate, requests);
|
||||
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
||||
|
||||
|
||||
@@ -328,8 +328,10 @@ export async function installDaemonWebSocketGate(page: Page, options: { port?: n
|
||||
};
|
||||
const clientRequestCounts = new Map<string, number>();
|
||||
const clientRequests = new Map<string, ClientRequest[]>();
|
||||
const clientRequestTimes = new Map<string, number[]>();
|
||||
const timelineRequestCounts = new Map<string, number>();
|
||||
const serverMessageCounts = new Map<string, number>();
|
||||
const serverMessageTimes = new Map<string, number[]>();
|
||||
const agentStreamEventCounts = new Map<string, number>();
|
||||
const agentStreamItemCounts = new Map<string, number>();
|
||||
const serverMessageWaiters = new Set<() => void>();
|
||||
@@ -363,8 +365,13 @@ export async function installDaemonWebSocketGate(page: Page, options: { port?: n
|
||||
const messageType = typeof message?.type === "string" ? message.type : null;
|
||||
const itemType = readAgentStreamItemType(message);
|
||||
const eventType = readAgentStreamEventType(message);
|
||||
if (messageType)
|
||||
if (messageType) {
|
||||
serverMessageCounts.set(messageType, (serverMessageCounts.get(messageType) ?? 0) + 1);
|
||||
serverMessageTimes.set(messageType, [
|
||||
...(serverMessageTimes.get(messageType) ?? []),
|
||||
Date.now(),
|
||||
]);
|
||||
}
|
||||
if (itemType)
|
||||
agentStreamItemCounts.set(itemType, (agentStreamItemCounts.get(itemType) ?? 0) + 1);
|
||||
if (eventType)
|
||||
@@ -481,6 +488,10 @@ export async function installDaemonWebSocketGate(page: Page, options: { port?: n
|
||||
const requests = clientRequests.get(request.type) ?? [];
|
||||
requests.push(request);
|
||||
clientRequests.set(request.type, requests);
|
||||
clientRequestTimes.set(request.type, [
|
||||
...(clientRequestTimes.get(request.type) ?? []),
|
||||
Date.now(),
|
||||
]);
|
||||
}
|
||||
if (request?.type === heldClientRequestType) {
|
||||
heldClientRequest = { server, message };
|
||||
@@ -851,6 +862,17 @@ export async function installDaemonWebSocketGate(page: Page, options: { port?: n
|
||||
getClientRequestCount(type: string): number {
|
||||
return clientRequestCounts.get(type) ?? 0;
|
||||
},
|
||||
/** The longest the daemon took to answer a request of this type since `fromIndex`, in ms. */
|
||||
getSlowestAnswerMs(requestType: string, responseType: string, fromIndex = 0): number {
|
||||
const answers = serverMessageTimes.get(responseType) ?? [];
|
||||
const latencies = (clientRequestTimes.get(requestType) ?? [])
|
||||
.slice(fromIndex)
|
||||
.flatMap((askedAt) => {
|
||||
const answeredAt = answers.find((at) => at >= askedAt);
|
||||
return answeredAt === undefined ? [] : [answeredAt - askedAt];
|
||||
});
|
||||
return Math.max(0, ...latencies);
|
||||
},
|
||||
getClientRequests(type: string): ReadonlyArray<ClientRequest> {
|
||||
return [...(clientRequests.get(type) ?? [])];
|
||||
},
|
||||
|
||||
@@ -13,6 +13,12 @@ const SUBSCRIPTION_REQUEST = "agent.timeline.set_subscription.request";
|
||||
const SUBSCRIPTION_RESPONSE = "agent.timeline.set_subscription.response";
|
||||
const TIMELINE_REQUEST = "fetch_agent_timeline_request";
|
||||
const TIMELINE_RESPONSE = "fetch_agent_timeline_response";
|
||||
/**
|
||||
* A check unanswered after `FRESHNESS_CHECK_QUIET_MS` (500) reports Updating messages by design, and
|
||||
* a loaded runner can take that long. The gate sees the answer before the app does, so this stops
|
||||
* short of 500.
|
||||
*/
|
||||
const SLOW_ANSWER_MS = 300;
|
||||
|
||||
export interface TimelineRequestCounts {
|
||||
agentId?: string;
|
||||
@@ -78,7 +84,11 @@ export async function waitForResumeAnswer(
|
||||
}
|
||||
|
||||
/** Records every appearance of the toast, including one that lasts a single commit. */
|
||||
export async function watchForUpdatingToast(page: Page): Promise<() => Promise<void>> {
|
||||
export async function watchForUpdatingToast(
|
||||
page: Page,
|
||||
gate: DaemonWebSocketGate,
|
||||
before: TimelineRequestCounts,
|
||||
): Promise<() => Promise<void>> {
|
||||
const watch = await page.evaluateHandle(() => {
|
||||
let shown = false;
|
||||
const check = () => {
|
||||
@@ -98,12 +108,16 @@ export async function watchForUpdatingToast(page: Page): Promise<() => Promise<v
|
||||
await waitForNextPaint(page);
|
||||
const shown = await watch.evaluate((recorder) => recorder.stop());
|
||||
await watch.dispose();
|
||||
expect(shown, "the Updating messages toast appeared").toBe(false);
|
||||
expectNoToastForPromptAnswer(shown, gate, before);
|
||||
};
|
||||
}
|
||||
|
||||
/** Reopens the app on this chat, recording the toast from the first paint of the new page. */
|
||||
export async function reopenAppWatchingForUpdatingToast(page: Page): Promise<() => Promise<void>> {
|
||||
export async function reopenAppWatchingForUpdatingToast(
|
||||
page: Page,
|
||||
gate: DaemonWebSocketGate,
|
||||
before: TimelineRequestCounts,
|
||||
): Promise<() => Promise<void>> {
|
||||
await page.addInitScript(() => {
|
||||
const recorder = window as Window & { updatingToastShown?: boolean };
|
||||
new MutationObserver(() => {
|
||||
@@ -117,10 +131,24 @@ export async function reopenAppWatchingForUpdatingToast(page: Page): Promise<()
|
||||
const shown = await page.evaluate(
|
||||
() => (window as Window & { updatingToastShown?: boolean }).updatingToastShown === true,
|
||||
);
|
||||
expect(shown, "the Updating messages toast appeared").toBe(false);
|
||||
expectNoToastForPromptAnswer(shown, gate, before);
|
||||
};
|
||||
}
|
||||
|
||||
function expectNoToastForPromptAnswer(
|
||||
shown: boolean,
|
||||
gate: DaemonWebSocketGate,
|
||||
before: TimelineRequestCounts,
|
||||
): void {
|
||||
const slowest = gate.getSlowestAnswerMs(
|
||||
SUBSCRIPTION_REQUEST,
|
||||
SUBSCRIPTION_RESPONSE,
|
||||
before.subscriptions,
|
||||
);
|
||||
if (slowest >= SLOW_ANSWER_MS) return;
|
||||
expect(shown, "the Updating messages toast appeared").toBe(false);
|
||||
}
|
||||
|
||||
async function waitForNextPaint(page: Page): Promise<void> {
|
||||
await page.evaluate(
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user