Files
kandev/apps/web/e2e/tests/chat/clarification-submit-failure.spec.ts
T
2026-09-06 18:52:30 +01:00

109 lines
3.9 KiB
TypeScript

// Regression coverage for the silent-clarification-submit-failure fix:
// a failed /respond POST must surface a retry affordance instead of a dead
// button, and a 409 caused by the bundle going inactive must never be
// reported to the user as a successful answer.
import { test, expect } from "../../fixtures/test-base";
import { activeSessionId, seedClarificationSession } from "../../helpers/clarification";
import { watchWs } from "../../helpers/causal-waits";
import { waitForSessionSettled } from "./quick-chat-helpers";
test.describe("Clarification submit failure feedback", () => {
test("surfaces a failed submit with a retry that preserves the answer", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(60_000);
const ws = watchWs(testPage);
const session = await seedClarificationSession(
testPage,
apiClient,
seedData,
"Clarification Submit Failure",
{ scenario: "clarification" },
);
const sessionId = await activeSessionId(testPage);
if (!sessionId) throw new Error("expected an active session for clarification retry");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
let attempt = 0;
await testPage.route("**/api/v1/clarification/*/respond", async (route) => {
attempt += 1;
if (attempt === 1) {
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({
error: "clarification response is temporarily unavailable",
code: "temporarily_unavailable",
}),
});
return;
}
await route.continue();
});
const postgres = session.clarificationOption("PostgreSQL");
await postgres.click();
const errorBanner = testPage.getByTestId("clarification-submit-error");
await expect(errorBanner).toBeVisible({ timeout: 15_000 });
// The user's answer must survive the failed submit -- no re-typing on retry.
await expect(postgres).toHaveAttribute("data-selected", "true");
await expect(session.clarificationSkip()).toBeEnabled();
await expect(session.idleInput()).toHaveCount(0);
await session.clarificationCollapseToggle().click();
await expect(session.clarificationOverlay()).toBeHidden();
await session.clarificationCollapseToggle().click();
await expect(session.clarificationOverlay()).toBeVisible();
const settled = waitForSessionSettled(ws, sessionId);
await testPage.getByTestId("clarification-retry").click();
await settled;
await expect(errorBanner).toHaveCount(0);
await expect(session.idleInput()).toBeVisible();
expect(attempt).toBe(2);
});
test("treats an inactive-bundle 409 as expired, never as a silent success", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(60_000);
const session = await seedClarificationSession(
testPage,
apiClient,
seedData,
"Clarification Expired Conflict",
{ scenario: "clarification" },
);
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
await testPage.route("**/api/v1/clarification/*/respond", async (route) => {
await route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
error: "clarification request is no longer active",
code: "not_active",
}),
});
});
await session.clarificationOption("PostgreSQL").click();
const expiredBanner = testPage.getByTestId("clarification-expired");
await expect(expiredBanner).toBeVisible({ timeout: 15_000 });
await expect(testPage.getByTestId("clarification-retry")).toHaveCount(0);
// A dropped answer must never flip the chat back to idle -- that would be
// reporting success for an answer nobody received.
await expect(session.idleInput()).toHaveCount(0);
await expect(session.clarificationOverlay()).toBeVisible();
});
});