ci: gate the e2e activity-phase DOM assertions on the persisted phase (#14821)

`activity-phases` asserted the parent `summary` was visible immediately after
`sendMessage` resolved. A parent phase only exists once the turn completes, the
phase closes, and its summary round-trips to the phase-label model — so that
assertion raced the entire pipeline and only survived on Playwright's retries.
It shows as `1 flaky` on the memory lane of a green dev run, and fails all three
attempts on slower hardware.

Gate the DOM on the durable projection instead. The test already fetched
/api/messages twice; the first fetch now also waits for the persisted phase part
before any DOM assertion runs, so the client is only asked about a phase the
server has already written.

Also drops the duplicate fetch. The two poll blocks queried the same endpoint
for the same message and both asserted
`finalTextIndex === activity_end_index`; the removed copy left `liveAssistant`,
`livePhase` and `liveFinalTextIndex` shadowing their durable equivalents.

No coverage removed — every assertion is preserved, reordered to follow the
dependency chain: persisted shape, then DOM, then label-model requests, then
the reload round-trip.
This commit is contained in:
Danny Avila 2026-08-14 01:42:39 -04:00 committed by GitHub
parent 2f0cd2eb75
commit d4c64d485f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -147,64 +147,15 @@ test.describe('parent activity phases', () => {
const run = await sendMessage(page, `E2E_ACTIVITY_PHASE_REPLY:${label}`);
expect(run.ok()).toBeTruthy();
const parent = messagesView(page).locator(`summary[aria-label="${PARENT_LABEL}"]`);
await expect(parent).toBeVisible({ timeout: 60000 });
/** Inspect the durable projection before the live DOM assertion so a
* failure identifies whether the server bound or client grouping is
* wrong. This remains a useful contract assertion after the bug is fixed. */
const liveConversationId = await getConversationId(page);
const liveToken = await getAccessToken(page);
let liveAssistant: PersistedMessage | undefined;
await expect
.poll(
async () => {
const messages = await fetchJson<PersistedMessage[]>(
page,
`/api/messages/${encodeURIComponent(liveConversationId)}`,
liveToken,
);
liveAssistant = messages.find(
(message) =>
message.isCreatedByUser === false && messageText(message).includes(finalText),
);
return liveAssistant?.unfinished;
},
{ timeout: 30000 },
)
.toBe(false);
const liveContent = liveAssistant?.content ?? [];
const livePhase = liveContent.find(
(part) => part?.type === 'activity_label' && part.activity_label_type === 'phase',
);
const liveFinalTextIndex = liveContent.findIndex((part) =>
contentPartText(part).includes(finalText),
);
expect(liveFinalTextIndex).toBe(livePhase?.activity_end_index);
await expect(messagesView(page).getByText(finalText)).toBeVisible({ timeout: 60000 });
await parent.click();
await expect(messagesView(page).getByRole('button', { name: childLabels.first })).toBeVisible();
await expect(
messagesView(page).getByRole('button', { name: childLabels.second }),
).toBeVisible();
await expect.poll(async () => (await getLabelRequestsFor(request, label)).length).toBe(3);
const labelRequests = await getLabelRequestsFor(request, label);
const phaseRequest = labelRequests.find((entry) => entry.model === PHASE_LABEL_MODEL);
const childRequests = labelRequests.filter((entry) => entry !== phaseRequest);
expect(phaseRequest).toMatchObject({ model: PHASE_LABEL_MODEL, stream: false });
expect(childRequests).toHaveLength(2);
expect(childRequests.map((entry) => entry.model)).toEqual([
CHILD_LABEL_MODEL,
CHILD_LABEL_MODEL,
]);
expect(
childRequests.some((entry) => entry.prompt.includes(`activity phase alpha ${label}`)),
).toBe(true);
expect(
childRequests.some((entry) => entry.prompt.includes(`activity phase beta ${label}`)),
).toBe(true);
/**
* A parent phase only exists once the turn completes, the phase closes, and
* its summary round-trips to the phase-label model. Gate on the durable
* projection first: the DOM cannot show a `summary` before the server has
* written one, so asserting the DOM up front races that whole pipeline and
* leaves retries as the only thing hiding it. Waiting for the persisted
* phase also keeps failures attributable a phase the server never wrote
* fails on the content assertions below rather than as a bare "not visible".
*/
const conversationId = await getConversationId(page);
const token = await getAccessToken(page);
let assistant: PersistedMessage | undefined;
@ -220,11 +171,16 @@ test.describe('parent activity phases', () => {
(message) =>
message.isCreatedByUser === false && messageText(message).includes(finalText),
);
return assistant?.unfinished;
if (assistant?.unfinished !== false) {
return false;
}
return (assistant.content ?? []).some(
(part) => part?.type === 'activity_label' && part.activity_label_type === 'phase',
);
},
{ timeout: 30000 },
{ timeout: 60000 },
)
.toBe(false);
.toBe(true);
expect(assistant).toBeDefined();
expect(assistant?.error).not.toBe(true);
@ -258,6 +214,32 @@ test.describe('parent activity phases', () => {
const finalTextIndex = content.findIndex((part) => contentPartText(part).includes(finalText));
expect(finalTextIndex).toBe(phasePart?.activity_end_index);
const parent = messagesView(page).locator(`summary[aria-label="${PARENT_LABEL}"]`);
await expect(parent).toBeVisible({ timeout: 30000 });
await expect(messagesView(page).getByText(finalText)).toBeVisible({ timeout: 30000 });
await parent.click();
await expect(messagesView(page).getByRole('button', { name: childLabels.first })).toBeVisible();
await expect(
messagesView(page).getByRole('button', { name: childLabels.second }),
).toBeVisible();
await expect.poll(async () => (await getLabelRequestsFor(request, label)).length).toBe(3);
const labelRequests = await getLabelRequestsFor(request, label);
const phaseRequest = labelRequests.find((entry) => entry.model === PHASE_LABEL_MODEL);
const childRequests = labelRequests.filter((entry) => entry !== phaseRequest);
expect(phaseRequest).toMatchObject({ model: PHASE_LABEL_MODEL, stream: false });
expect(childRequests).toHaveLength(2);
expect(childRequests.map((entry) => entry.model)).toEqual([
CHILD_LABEL_MODEL,
CHILD_LABEL_MODEL,
]);
expect(
childRequests.some((entry) => entry.prompt.includes(`activity phase alpha ${label}`)),
).toBe(true);
expect(
childRequests.some((entry) => entry.prompt.includes(`activity phase beta ${label}`)),
).toBe(true);
await page.reload();
const reloadedParent = messagesView(page).locator(`summary[aria-label="${PARENT_LABEL}"]`);
await expect(reloadedParent).toBeVisible({ timeout: 30000 });