test(app): check unanswered freshness when reopening cached history

Foreground checks intentionally stay silent since #9242. Exercise the opening check instead, waiting for a durable cursor before reload so cached messages remain readable while the daemon answer is held.
This commit is contained in:
Mohamed Boudra
2026-11-10 11:18:28 +01:00
parent 63ea3e5561
commit 8bccb3bad6
2 changed files with 21 additions and 7 deletions
@@ -22,7 +22,7 @@ import {
restoreUnchangedViewedTimeline,
restoreViewedTimelineHoldingCatchUp,
restoreViewedTimelineWithHeldResponse,
returnWithFreshnessCheckUnanswered,
reopenWithFreshnessCheckUnanswered,
waitForResumeAnswer,
reopenAppWatchingForUpdatingToast,
watchForUpdatingToast,
@@ -103,7 +103,9 @@ test.describe("Agent timeline resume", () => {
}
});
test("a check the daemon has not answered reports the update until it does", async ({ page }) => {
test("opening cached history reports an unanswered check until the daemon answers", async ({
page,
}) => {
test.setTimeout(120_000);
const gate = await installDaemonWebSocketGate(page);
const agent = await seedLongMockAgentTimeline({ turns: 3 });
@@ -112,7 +114,7 @@ test.describe("Agent timeline resume", () => {
await expectTimelinePromptVisible(page, agent.newestPrompt);
const requests = rememberTimelineRequestCounts(gate);
await returnWithFreshnessCheckUnanswered(page, gate);
await reopenWithFreshnessCheckUnanswered(page, gate, agent);
await answerFreshnessCheck(page, gate);
expectResumeAnsweredWithoutFetch(gate, requests);
@@ -1,5 +1,6 @@
import { expect, type Page } from "@playwright/test";
import type { installDaemonWebSocketGate } from "./daemon-websocket-gate";
import { readReplicaCache } from "./replica-cache-storage";
import type { LongTimelineAgent } from "./timeline-pagination";
import {
expectReconnectingToastGone,
@@ -14,7 +15,7 @@ const SUBSCRIPTION_RESPONSE = "agent.timeline.set_subscription.response";
const TIMELINE_REQUEST = "fetch_agent_timeline_request";
const TIMELINE_RESPONSE = "fetch_agent_timeline_response";
/**
* `FRESHNESS_CHECK_QUIET_MS` is 500: a check unanswered that long reports Updating messages by
* `FRESHNESS_CHECK_QUIET_MS` is 500: an opening check unanswered that long reports Updating messages by
* design, however long the daemon takes. The app starts that wait just before it sends the question
* this helper times, so the bound stops short of 500.
*/
@@ -151,13 +152,24 @@ export async function reopenAppWatchingForUpdatingToast(page: Page): Promise<()
return () => expectToastWaitedForTheDaemon(page);
}
/** Returns to the chat with the daemon's answer held, past the point the toast reports it. */
export async function returnWithFreshnessCheckUnanswered(
/** Opens persisted history with the daemon's freshness answer held. */
export async function reopenWithFreshnessCheckUnanswered(
page: Page,
gate: DaemonWebSocketGate,
agent: LongTimelineAgent,
): Promise<void> {
// A cold open needs a persisted authoritative cursor to ask a freshness question.
await expect
.poll(async () => {
const cache = await readReplicaCache(page);
return cache?.hosts
?.flatMap((host) => host.timelines)
.find((timeline) => timeline.agentId === agent.agentId)?.range?.endSeq;
})
.toBeGreaterThan(0);
gate.holdNextClientRequest(SUBSCRIPTION_REQUEST);
await hideTabAndReturn(page);
await page.reload();
await expect(page.getByText(agent.newestPrompt, { exact: true })).toBeVisible();
await gate.waitForHeldClientRequest();
await expect(page.getByRole("alert").filter({ hasText: "Updating messages" })).toBeVisible();
}