Files
confidence-engine/tests/e2e/recovery-states.spec.js
T

193 lines
7.9 KiB
JavaScript

/**
* Recovery-state E2E tests — provider error and malformed response flows.
*
* What these tests validate:
* - Error state renders an error panel with structured message
* - Workspace remains visible alongside the recovery card
* - ProviderUnavailableCard / MalformedResponseCard content visible
* - Session persistence survives a recovery state (recovery key in sessionStorage)
*
* What these tests do NOT validate:
* - Error handling logic correctness (that is unit-level concern)
*/
import { test, expect } from "@playwright/test";
import { INVESTIGATION_SCENARIOS, SCENARIO_LOOKUP } from "./fixtures/investigation-scenarios.js";
const PROVIDER_ERROR = SCENARIO_LOOKUP["Provider error"];
const MALFORMED_RESPONSE = SCENARIO_LOOKUP["Malformed response"];
/* ── Test helpers ─────────────────────────────────────────────── */
async function waitForLoading(page) {
await expect(page.locator('[data-testid="loading-overlay"]')).toBeVisible();
}
async function waitForWorkspaceReady(page, options = {}) {
const { expectLoadingGone = true } = options;
if (expectLoadingGone) {
await page.waitForSelector('[data-testid="loading-overlay"]', { state: "hidden", timeout: 30_000 });
}
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible({ timeout: 15_000 });
}
/* ═══════════════ Test: provider error after start ══════════════ */
test("provider error: analyse returns failure → error panel visible alongside workspace", async ({ page }) => {
await page.goto("/");
// Enable mock mode in error scenario
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "error";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(PROVIDER_ERROR.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Loading overlay appears briefly then disappears
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
// Workspace should still be visible (even with error result)
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Error panel with structured error message
const errorPanel = page.locator('div[class*="border-red"][class*="bg-red"]');
await expect(errorPanel).toBeVisible();
await expect(errorPanel.locator('div[class*="text-sm"]').first()).toContainText(/error/i);
});
/* ═══════════════ Test: provider error during update ════════════ */
test("provider error during update turn: error panel appears after answer submit", async ({ page }) => {
await page.goto("/");
// Use a non-error scenario first to get into success state, then switch to error for update
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = ""; // normal default for start
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
await waitForLoading(page);
await waitForWorkspaceReady(page);
// Switch to error scenario for the update call
await page.evaluate(() => {
window.__MOCK_SCENARIO = "error";
});
// Submit an answer to trigger the update call (which will now get error)
const answerTextarea = page.locator('[data-testid="response-textarea"]');
if (await answerTextarea.isVisible({ timeout: 3000 })) {
await answerTextarea.fill("Some answer that should fail");
await page.getByRole("button", { name: "Update situation" }).click();
// Verify error panel appears after the update call errors
const errorPanel = page.locator('div[class*="border-red"][class*="bg-red"]');
await expect(errorPanel).toBeVisible({ timeout: 10_000 });
}
});
/* ═══════════════ Test: malformed response during start ═════════ */
test("malformed response during start: error state visible, workspace still rendered", async ({ page }) => {
await page.goto("/");
// Malformed responses come through the error mock path (same as provider error)
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "error";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(MALFORMED_RESPONSE.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Loading then workspace
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
// Error text present in the red panel
const errorText = page.locator('div[class*="border-red"][class*="text-sm"]').first();
await expect(errorText).toHaveText(/error|failed/i);
});
/* ═══════════════ Test: recovery state persists in session ══════ */
test("recovery state persists via sessionStorage after error response", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "error";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(PROVIDER_ERROR.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Wait for workspace to render with error state
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Verify sessionStorage has the session key with situationGraph from error response
const sessionRaw = await page.evaluate(() => {
const raw = sessionStorage.getItem("confidence-engine-session");
return raw ? JSON.parse(raw) : null;
});
// Session should exist (even in error recovery path, the scenario text is persisted)
expect(sessionRaw).not.toBeNull();
});
/* ═══════════════ Test: multiple error transitions ══════════════ */
test("multiple successive errors maintain UI state without crash", async ({ page }) => {
await page.goto("/");
// Enable mock mode with error scenario
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "error";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(PROVIDER_ERROR.centralStatement);
// First analyse call
await page.getByRole("button", { name: "Analyse" }).click();
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// If there's an answer textarea, try another update (also errors)
const answerTextarea = page.locator('[data-testid="response-textarea"]');
if (await answerTextarea.isVisible({ timeout: 2000 })) {
await answerTextarea.fill("Second attempt");
await page.getByRole("button", { name: "Update situation" }).click();
// Error panel should still be visible (no crash, no layout shift)
const errorPanel = page.locator('div[class*="border-red"]');
await expect(errorPanel).toBeVisible({ timeout: 10_000 });
}
// Workspace should remain stable — no errors thrown
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
});