Files
paseo/packages/app/e2e/browser/agent-timeline-resume.spec.ts
T
Mohamed Boudra 82687ec95b Keep plugin composer items in the toolbar when they fit (#9209)
* fix(app): keep plugin composer items in the toolbar when they fit

The density budget guessed a plugin item's width from a fixed label and
counted every registered item, so an icon toggle or an item rendering
nothing pushed the toolbar into its overflow state with room to spare.
An item is an arbitrary component, so its width is only known by
rendering it. Measure the toolbar form in an always-mounted slot; the
width never depends on the density it decides, so moving items to the
overflow cannot flip the answer.

Sheet rows truncated short values such as "Full access" because the
value's 45% cap resolved against a content-width row. Fill the row so
the cap resolves against the sheet's width.

* fix(app): close a plugin picker when its trigger moves to the overflow

The measuring slot keeps toolbar plugin items mounted, so a picker that
was open when the toolbar condensed lost its trigger but kept its popup,
which portals to the overlay root. Mark the slot's items inactive while
they are out of the row, the contract retained panels already use to
close their popups.

* 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.

* test(app): check the Updating messages toast the same way on every run

The previous repair skipped the no-toast assertion whenever the daemon
took 300ms or more to answer, so a slow run proved nothing. The toast is
correct once a check has gone unanswered for the quiet window, so the
spec now always asserts that the toast did not appear sooner than that
after the question was sent, timed in the page. The delayed-answer path
gets its own test that holds the daemon's answer.
2026-11-10 09:08:11 +07:00

149 lines
5.6 KiB
TypeScript

import { test } from "../support/fixtures";
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
import {
expectTimelinePresentationUnchanged,
expectTimelinePromptNotMounted,
expectTimelinePromptVisible,
openAgentTimeline,
rememberTimelinePresentation,
scrollTimelinePromptIntoView,
scrollTimelineUntilOlderHistoryIsReachable,
seedLongMockAgentTimeline,
} from "../support/helpers/timeline-pagination";
import {
answerFreshnessCheck,
commitTimelineTurnsWhileDisconnected,
disconnectViewedTimeline,
expectResumeAnsweredWithoutFetch,
expectResumeOverflowFallsBackToOneTail,
hideTabAndReturn,
rememberTimelineRequestCounts,
releaseHeldCatchUp,
restoreUnchangedViewedTimeline,
restoreViewedTimelineHoldingCatchUp,
restoreViewedTimelineWithHeldResponse,
returnWithFreshnessCheckUnanswered,
waitForResumeAnswer,
reopenAppWatchingForUpdatingToast,
watchForUpdatingToast,
} from "../support/helpers/timeline-resume";
test.describe("Agent timeline resume", () => {
test("a long disconnected run lands as one latest-tail update", async ({ page }) => {
test.setTimeout(120_000);
const gate = await installDaemonWebSocketGate(page);
const agent = await seedLongMockAgentTimeline({ turns: 3 });
try {
await openAgentTimeline(page, agent);
await expectTimelinePromptVisible(page, agent.newestPrompt);
const requests = rememberTimelineRequestCounts(gate);
await disconnectViewedTimeline(page, gate);
const background = await commitTimelineTurnsWhileDisconnected(agent, 24);
await restoreViewedTimelineWithHeldResponse(page, gate);
await expectTimelinePromptVisible(page, background.newestPrompt);
await expectTimelinePromptNotMounted(page, background.firstPrompt);
expectResumeOverflowFallsBackToOneTail(gate, requests);
await scrollTimelineUntilOlderHistoryIsReachable(page, background.firstPrompt);
} finally {
gate.restore();
await agent.cleanup();
}
});
test("returning to an unchanged chat shows no update and leaves it untouched", async ({
page,
}) => {
test.setTimeout(120_000);
const gate = await installDaemonWebSocketGate(page);
const agent = await seedLongMockAgentTimeline({ turns: 30 });
try {
await openAgentTimeline(page, agent);
await expectTimelinePromptVisible(page, agent.newestPrompt);
await scrollTimelinePromptIntoView(page, agent.initialTailAnchorPrompt);
const presentation = await rememberTimelinePresentation(page, agent.initialTailAnchorPrompt);
await test.step("back from the background", async () => {
const requests = rememberTimelineRequestCounts(gate);
const expectNoUpdatingToast = await watchForUpdatingToast(page);
await hideTabAndReturn(page);
await waitForResumeAnswer(gate, requests);
await expectNoUpdatingToast();
await expectTimelinePresentationUnchanged(page, presentation);
expectResumeAnsweredWithoutFetch(gate, requests);
});
await test.step("back from a dropped connection", async () => {
const requests = rememberTimelineRequestCounts(gate);
const expectNoUpdatingToast = await watchForUpdatingToast(page);
await disconnectViewedTimeline(page, gate);
await restoreUnchangedViewedTimeline(page, gate);
await expectNoUpdatingToast();
await expectTimelinePresentationUnchanged(page, presentation);
expectResumeAnsweredWithoutFetch(gate, requests);
});
await test.step("back from reopening the app", async () => {
const requests = rememberTimelineRequestCounts(gate);
const expectNoUpdatingToast = await reopenAppWatchingForUpdatingToast(page);
await waitForResumeAnswer(gate, requests);
await expectTimelinePromptVisible(page, agent.newestPrompt);
await expectNoUpdatingToast();
expectResumeAnsweredWithoutFetch(gate, requests);
});
} finally {
gate.restore();
await agent.cleanup();
}
});
test("a check the daemon has not answered reports the update until it does", async ({ page }) => {
test.setTimeout(120_000);
const gate = await installDaemonWebSocketGate(page);
const agent = await seedLongMockAgentTimeline({ turns: 3 });
try {
await openAgentTimeline(page, agent);
await expectTimelinePromptVisible(page, agent.newestPrompt);
const requests = rememberTimelineRequestCounts(gate);
await returnWithFreshnessCheckUnanswered(page, gate);
await answerFreshnessCheck(page, gate);
expectResumeAnsweredWithoutFetch(gate, requests);
} finally {
gate.restore();
await agent.cleanup();
}
});
test("returning to a changed chat reports the update and shows the new messages", async ({
page,
}) => {
test.setTimeout(120_000);
const gate = await installDaemonWebSocketGate(page);
const agent = await seedLongMockAgentTimeline({ turns: 3 });
try {
await openAgentTimeline(page, agent);
await expectTimelinePromptVisible(page, agent.newestPrompt);
const presentation = await rememberTimelinePresentation(page, agent.newestPrompt);
await disconnectViewedTimeline(page, gate);
const background = await commitTimelineTurnsWhileDisconnected(agent, 1);
await restoreViewedTimelineHoldingCatchUp(page, gate);
await expectTimelinePresentationUnchanged(page, presentation);
await releaseHeldCatchUp(page, gate);
await expectTimelinePromptVisible(page, background.newestPrompt);
} finally {
gate.restore();
await agent.cleanup();
}
});
});