171 lines
7.5 KiB
TypeScript
171 lines
7.5 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
type ServiceSnapshot = {
|
|
service: {
|
|
id: string;
|
|
name: string;
|
|
depends_on: string[];
|
|
};
|
|
latest_probe: { status: string } | null;
|
|
effective_status: string;
|
|
};
|
|
|
|
const topologyOf = (snapshots: ServiceSnapshot[]) =>
|
|
snapshots
|
|
.map(({ service }) => ({
|
|
id: service.id,
|
|
name: service.name,
|
|
depends_on: [...service.depends_on].sort(),
|
|
}))
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
|
|
const isDemoMutation = (path: string) =>
|
|
(response: {
|
|
url: () => string;
|
|
request: () => { method: () => string };
|
|
}) =>
|
|
response.url().endsWith(path) && response.request().method() === "POST";
|
|
|
|
test("Explore demo exposes evidence-backed root cause and reset-safe exit", async ({ page, request }) => {
|
|
test.setTimeout(60_000);
|
|
|
|
// The Playwright server uses a persistent temporary database, so restore any
|
|
// session left by an interrupted run before taking this test's baseline.
|
|
const cleanup = await request.post("/api/v1/demo/exit");
|
|
expect(cleanup.ok()).toBeTruthy();
|
|
|
|
const baselineResponse = await request.get("/api/v1/services");
|
|
expect(baselineResponse.ok()).toBeTruthy();
|
|
const baselineTopology = topologyOf(await baselineResponse.json());
|
|
|
|
await page.goto("/");
|
|
const explore = page.getByRole("button", { name: "Explore demo", exact: true });
|
|
await expect(explore).toBeEnabled({ timeout: 30_000 });
|
|
|
|
const loadResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo"));
|
|
await explore.click();
|
|
if (baselineTopology.length > 0) {
|
|
const confirmation = page
|
|
.getByRole("alert")
|
|
.filter({ hasText: "temporarily replace this topology" });
|
|
await expect(confirmation).toBeVisible();
|
|
await confirmation.getByRole("button", { name: "Explore demo", exact: true }).click();
|
|
}
|
|
const loadResponse = await loadResponsePromise;
|
|
expect(loadResponse.ok()).toBeTruthy();
|
|
expect(await loadResponse.json()).toMatchObject({
|
|
active: true,
|
|
synthetic: true,
|
|
scenario_name: "Northstar Platform",
|
|
service_count: 9,
|
|
total: 9,
|
|
root_cause_service_name: "PostgreSQL",
|
|
affected_count: 5,
|
|
});
|
|
|
|
const demoServicesResponse = await request.get("/api/v1/services");
|
|
expect(demoServicesResponse.ok()).toBeTruthy();
|
|
const demoServices: ServiceSnapshot[] = await demoServicesResponse.json();
|
|
expect(demoServices).toHaveLength(9);
|
|
const postgres = demoServices.find(({ service }) => service.name === "PostgreSQL");
|
|
expect(postgres).toBeDefined();
|
|
expect(postgres!.latest_probe?.status).toBe("down");
|
|
expect(postgres!.effective_status).toBe("down");
|
|
|
|
const [incidentsResponse, historyResponse, availabilityResponse] =
|
|
await Promise.all([
|
|
request.get("/api/v1/incidents"),
|
|
request.get(`/api/v1/services/${postgres!.service.id}/history?limit=50`),
|
|
request.get(`/api/v1/services/${postgres!.service.id}/availability?hours=24`),
|
|
]);
|
|
expect(incidentsResponse.ok()).toBeTruthy();
|
|
const incident = (await incidentsResponse.json()) as {
|
|
active: boolean;
|
|
candidates: Array<{
|
|
service_name: string;
|
|
affected_service_ids: string[];
|
|
}>;
|
|
};
|
|
expect(incident.active).toBe(true);
|
|
expect(incident.candidates[0]?.service_name).toBe("PostgreSQL");
|
|
expect(incident.candidates[0]?.affected_service_ids).toHaveLength(5);
|
|
|
|
expect(historyResponse.ok()).toBeTruthy();
|
|
expect(await historyResponse.json()).toHaveLength(24);
|
|
expect(availabilityResponse.ok()).toBeTruthy();
|
|
expect(await availabilityResponse.json()).toMatchObject({
|
|
total_checks: 24,
|
|
down_checks: 2,
|
|
uptime_percent: 91.67,
|
|
});
|
|
|
|
const demoBanner = page.getByRole("status", { name: "Explore demo scenario" });
|
|
await expect(demoBanner).toBeVisible();
|
|
await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
|
await expect(page.getByText("Demo scenario", { exact: false })).toBeVisible();
|
|
await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible();
|
|
await expect(page.getByText("Likely root cause", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Health evidence", { exact: true })).toBeVisible();
|
|
|
|
const workspaces = page.getByRole("navigation", { name: "ServiceMap workspaces" });
|
|
await workspaces.getByRole("button", { name: "Overview" }).click();
|
|
await expect(page.getByRole("heading", { name: "9/9 with current health evidence" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Review unresolved" })).toBeDisabled();
|
|
await page.locator(".overview-lead").getByRole("button", { name: /1 down/i }).click();
|
|
await expect(page.getByRole("combobox", { name: "Status", exact: true })).toHaveValue("down");
|
|
await workspaces.getByRole("button", { name: "Incidents" }).click();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Active and historical incidents" }),
|
|
).toBeVisible();
|
|
await page.getByLabel("Search incidents").fill("does not exist");
|
|
await expect(page.getByText("No incidents match these filters.")).toBeVisible();
|
|
await page.getByLabel("Search incidents").fill("");
|
|
await workspaces.getByRole("button", { name: "Topology", exact: true }).click();
|
|
await expect(page.getByRole("region", { name: "Topology commands" })).toBeVisible();
|
|
await page.getByRole("region", { name: "Topology commands" }).locator("summary").filter({ hasText: "Filters" }).click();
|
|
await expect(page.getByRole("combobox", { name: "Filter topology by status" })).toBeVisible();
|
|
const inspector = page.getByRole("complementary");
|
|
await expect(inspector.getByRole("heading", { name: "PostgreSQL" })).toBeVisible();
|
|
await expect(inspector.getByText("Likely root cause", { exact: true })).toBeVisible();
|
|
await expect(inspector.getByText("Health evidence", { exact: true })).toBeVisible();
|
|
|
|
const reset = page.locator("header").getByRole("button", { name: "Reset demo" });
|
|
const resetResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/reset"));
|
|
await reset.click();
|
|
const resetResponse = await resetResponsePromise;
|
|
expect(resetResponse.ok()).toBeTruthy();
|
|
expect(await resetResponse.json()).toMatchObject({
|
|
active: true,
|
|
root_cause_service_name: "PostgreSQL",
|
|
});
|
|
|
|
// A rendered PostgreSQL node can be left over from before Reset completed.
|
|
// Wait for the demo region to leave its busy state before attempting Exit.
|
|
await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
|
await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible();
|
|
const exit = page.getByRole("button", { name: "Exit demo", exact: true });
|
|
await expect(exit).toBeEnabled();
|
|
|
|
const exitResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/exit"));
|
|
await exit.click();
|
|
const exitResponse = await exitResponsePromise;
|
|
expect(exitResponse.ok()).toBeTruthy();
|
|
expect(await exitResponse.json()).toMatchObject({ active: false, restored: true });
|
|
|
|
await expect(demoBanner).toBeHidden({ timeout: 30_000 });
|
|
await expect(
|
|
page.getByText("Demo closed. Your previous topology was restored.", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Explore demo", exact: true }),
|
|
).toBeEnabled({ timeout: 30_000 });
|
|
|
|
const restoredDemoResponse = await request.get("/api/v1/demo");
|
|
expect(restoredDemoResponse.ok()).toBeTruthy();
|
|
expect(await restoredDemoResponse.json()).toMatchObject({ active: false });
|
|
|
|
const restoredServicesResponse = await request.get("/api/v1/services");
|
|
expect(restoredServicesResponse.ok()).toBeTruthy();
|
|
expect(topologyOf(await restoredServicesResponse.json())).toEqual(baselineTopology);
|
|
});
|