feat: complete catalog fleet management workflow

This commit is contained in:
Codex
2026-08-31 15:58:43 +02:00
parent 866aee4249
commit 9c697dc062
56 changed files with 6692 additions and 611 deletions
@@ -0,0 +1,563 @@
import { expect, test, type Page, type Route } from "@playwright/test";
import { createAuthenticationStack } from "./fixtures/auth-stack.mjs";
test.describe.configure({ mode: "serial" });
test.use({ viewport: { width: 1910, height: 911 } });
let stack: Awaited<ReturnType<typeof createAuthenticationStack>>;
const databaseId = "11111111-1111-4111-8111-111111111111";
const database = {
id: databaseId,
workspaceId: "psd-clinical",
workspaceName: "Policlinico San Donato",
workspaceAvailable: true,
configured: true,
engine: "postgres",
databaseName: "warehouse",
schema: "datawarehouse",
version: 3,
createdAt: "2026-08-27T08:00:00Z",
updatedAt: "2026-08-27T09:00:00Z",
binding: {
transport: "postgres_direct",
host: "db.internal",
port: 5432,
username: "reader",
},
connectionStatus: "reachable",
testedVersion: 3,
secrets: {
password: false,
apiKey: false,
sshPrivateKey: false,
sshPrivateKeyPassphrase: false,
sshKnownHosts: false,
tlsCa: false,
},
};
const table = {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
databaseId,
name: "patients",
sourceComment: "Patients imported from the clinical source",
description: null,
generatedDescription: null,
version: 1,
createdAt: "2026-08-27T08:00:00Z",
updatedAt: "2026-08-27T09:00:00Z",
};
function metrics(requestedDatabaseId: string | null) {
return {
scope: requestedDatabaseId ? "database" : "global",
databaseId: requestedDatabaseId,
tables: 1,
columns: 0,
sensitiveColumns: 0,
relationships: 0,
descriptionTargets: 1,
describedTargets: 0,
descriptionCoverage: 0,
updatedAt: "2026-08-31T09:00:00Z",
};
}
function json(route: Route, body: unknown, status = 200) {
return route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
}
async function signInAsAdmin(page: Page) {
const account = stack.localAccount("admin");
await page.getByLabel("Username").fill(account.username);
await page.locator('input[name="password"]').fill(account.password);
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(page.getByTestId("app-shell")).toBeVisible({ timeout: 30_000 });
}
async function expectContextPanelGeometry(page: Page, accessibleName: string) {
const manager = page
.getByTestId("conversation-column")
.getByRole("main", { name: "Database management" });
const panel = manager.getByRole("dialog", { name: accessibleName });
const sessionRail = page.getByRole("complementary", { name: "Session navigation" });
await expect(panel).toBeVisible();
expect(
await panel.evaluate((element) => element.hasAttribute("data-catalog-panel")),
).toBe(true);
await expect(
panel.locator(':scope > [data-catalog-panel-region="header"]'),
).toHaveCount(1);
await expect(
panel.locator(':scope > [data-catalog-panel-region="body"]'),
).toHaveCount(1);
const [managerBox, panelBox, sessionRailBox] = await Promise.all([
manager.boundingBox(),
panel.boundingBox(),
sessionRail.boundingBox(),
]);
expect(managerBox).not.toBeNull();
expect(panelBox).not.toBeNull();
expect(sessionRailBox).not.toBeNull();
if (!managerBox || !panelBox || !sessionRailBox) return;
const managerCenter = managerBox.x + managerBox.width / 2;
const panelCenter = panelBox.x + panelBox.width / 2;
expect(Math.abs(panelCenter - managerCenter)).toBeLessThanOrEqual(1);
expect(Math.abs(panelBox.width / managerBox.width - 0.6)).toBeLessThanOrEqual(0.005);
expect(panelBox.x).toBeGreaterThanOrEqual(managerBox.x - 1);
expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(managerBox.x + managerBox.width + 1);
expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(sessionRailBox.x + 1);
expect(panelBox.y).toBeGreaterThanOrEqual(managerBox.y);
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(managerBox.y + managerBox.height + 1);
}
async function expectContextPanelHeaderMatchesNewSession(
page: Page,
accessibleName: string,
) {
const manager = page
.getByTestId("conversation-column")
.getByRole("main", { name: "Database management" });
const panel = manager.getByRole("dialog", { name: accessibleName });
const header = panel.locator(':scope > [data-catalog-panel-region="header"]');
const newSession = page
.getByRole("complementary", { name: "Session navigation" })
.getByRole("button", { name: "New session", exact: true });
await expect(header).toHaveCount(1);
await expect(newSession).toBeVisible();
const [headerStyle, actionStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
newSession.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
})),
]);
expect(headerStyle.backgroundImage).toBe("none");
expect(headerStyle.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
expect(headerStyle.backgroundColor).toBe(actionStyle.backgroundColor);
}
async function expectWorkAreaPanelGeometry(page: Page, accessibleName: string) {
const workArea = page.getByTestId("conversation-column");
const panel = workArea.getByRole("dialog", { name: accessibleName });
const sessionRail = page.getByRole("complementary", { name: "Session navigation" });
await expect(panel).toBeVisible();
await expect(panel).toHaveAttribute("aria-modal", "false");
await expect(panel).toHaveAttribute("data-work-area-panel", "");
await expect(panel.locator(':scope > [data-work-area-panel-region="header"]')).toHaveCount(1);
await expect(panel.locator(':scope > [data-work-area-panel-region="body"]')).toHaveCount(1);
const [workAreaBox, panelBox, sessionRailBox] = await Promise.all([
workArea.boundingBox(),
panel.boundingBox(),
sessionRail.boundingBox(),
]);
expect(workAreaBox).not.toBeNull();
expect(panelBox).not.toBeNull();
expect(sessionRailBox).not.toBeNull();
if (!workAreaBox || !panelBox || !sessionRailBox) return;
expect(Math.abs(
panelBox.x + panelBox.width / 2 - (workAreaBox.x + workAreaBox.width / 2),
)).toBeLessThanOrEqual(1);
expect(Math.abs(panelBox.width / workAreaBox.width - 0.6)).toBeLessThanOrEqual(0.005);
expect(panelBox.x).toBeGreaterThanOrEqual(workAreaBox.x - 1);
expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(workAreaBox.x + workAreaBox.width + 1);
expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(sessionRailBox.x + 1);
expect(panelBox.y).toBeGreaterThanOrEqual(workAreaBox.y);
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(workAreaBox.y + workAreaBox.height + 1);
}
async function expectWorkAreaPanelHeaderMatchesNewSession(
page: Page,
accessibleName: string,
) {
const panel = page
.getByTestId("conversation-column")
.getByRole("dialog", { name: accessibleName });
const header = panel.locator(':scope > [data-work-area-panel-region="header"]');
const newSession = page
.getByRole("complementary", { name: "Session navigation" })
.getByRole("button", { name: "New session", exact: true });
const [headerStyle, actionStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
newSession.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
})),
]);
expect(headerStyle.backgroundImage).toBe("none");
expect(headerStyle.backgroundColor).toBe(actionStyle.backgroundColor);
}
test.beforeAll(async () => {
stack = await createAuthenticationStack();
await stack.useLocalMode();
});
test.afterAll(async () => {
await stack?.close();
});
test("context panels stay inside the manager and the Tables grid sits in a sidebar-colored frame", async ({ page }) => {
const unexpectedCatalogRequests: string[] = [];
const responses: Record<string, unknown> = {
"GET /api/catalog/databases": [database],
"GET /api/catalog/metadata-generation/models": {
models: [],
default: null,
},
"GET /api/catalog/description-generation-runs?limit=50": [],
"GET /api/catalog/sensitive-data-suggestion-runs?limit=50": [],
[`GET /api/catalog/databases/${databaseId}/tables`]: [table],
};
await page.route("**/api/catalog/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
const key = `${request.method()} ${url.pathname}${url.search}`;
if (request.method() === "GET" && url.pathname === "/api/catalog/metrics") {
const requestedId = url.searchParams.get("databaseId");
if (requestedId === null || requestedId === databaseId) {
await json(route, metrics(requestedId));
return;
}
}
if (Object.hasOwn(responses, key)) {
await json(route, responses[key]);
return;
}
unexpectedCatalogRequests.push(key);
await json(route, { code: "unexpected_catalog_request" }, 500);
});
await page.goto(stack.publicUrl);
await signInAsAdmin(page);
await page
.getByRole("button", { name: "Database management", exact: true })
.click();
await expect(
page.getByRole("complementary", { name: "Session navigation" }),
).toBeVisible();
await expect(
page.getByTestId("conversation-column").getByRole("main", {
name: "Database management",
}),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Back to workspace", exact: true }),
).toHaveCount(0);
await page
.getByRole("button", { name: "Edit Policlinico San Donato", exact: true })
.click();
await expectContextPanelGeometry(page, "Edit database");
await expectContextPanelHeaderMatchesNewSession(page, "Edit database");
await page
.getByRole("dialog", { name: "Edit database" })
.getByRole("button", { name: "Close database panel" })
.click();
await page
.getByRole("button", { name: "Description history", exact: true })
.click();
await expectContextPanelGeometry(page, "Description generation");
await expectContextPanelHeaderMatchesNewSession(page, "Description generation");
await page
.getByRole("dialog", { name: "Description generation" })
.getByRole("button", { name: /close/i })
.click();
await page.setViewportSize({ width: 1280, height: 800 });
await page
.getByRole("button", { name: "Edit Policlinico San Donato", exact: true })
.click();
await expectContextPanelGeometry(page, "Edit database");
await page
.getByRole("dialog", { name: "Edit database" })
.getByRole("button", { name: "Close database panel" })
.click();
await page.setViewportSize({ width: 1910, height: 911 });
await page
.getByRole("button", {
name: "View tables for Policlinico San Donato",
exact: true,
})
.click();
await expect(
page.getByRole("region", {
name: "Tables for Policlinico San Donato",
}),
).toBeVisible();
const applicationBar = page.locator(
'main[aria-label="Database management"] .thot-fleet-ledger__header',
);
await expect(applicationBar).toHaveCount(1);
await expect(
applicationBar.getByRole("heading", { name: "Database management" }),
).toBeVisible();
await expect(
applicationBar.getByRole("region", { name: "Catalog status" }),
).toBeVisible();
await expect(
applicationBar.getByRole("group", { name: "Database management actions" }),
).toBeVisible();
await expect(applicationBar).not.toContainText("Thoth catalog · Fleet ledger");
await expect(applicationBar).not.toContainText("Inspect physical metadata");
const applicationBarLayout = await applicationBar.evaluate((element) => {
const title = element.querySelector(".thot-fleet-ledger__heading-copy");
const status = element.querySelector(".thot-fleet-ledger__header-status");
const actions = element.querySelector(".thot-fleet-ledger__header-actions");
if (!title || !status || !actions) {
throw new Error("Application bar regions are missing");
}
const centerY = (node: Element) => {
const rect = node.getBoundingClientRect();
return rect.top + rect.height / 2;
};
return {
backgroundImage: getComputedStyle(element).backgroundImage,
height: element.getBoundingClientRect().height,
centers: [centerY(title), centerY(status), centerY(actions)],
};
});
expect(applicationBarLayout.backgroundImage).toBe("none");
expect(applicationBarLayout.height).toBeLessThanOrEqual(64);
expect(
Math.max(...applicationBarLayout.centers) - Math.min(...applicationBarLayout.centers),
).toBeLessThanOrEqual(1);
await expect(page.getByRole("row", { name: /patients/ })).toBeVisible();
await page
.getByRole("button", { name: "Edit description for patients", exact: true })
.click();
await expectContextPanelGeometry(page, "Review table description");
await expectContextPanelHeaderMatchesNewSession(page, "Review table description");
const tableEditor = page.getByRole("dialog", { name: "Review table description" });
const tableEditorFooter = tableEditor.locator(
':scope > [data-catalog-panel-region="footer"]',
);
await expect(tableEditorFooter).toHaveCount(1);
await expect(
tableEditorFooter.getByRole("button", { name: "Cancel", exact: true }),
).toBeVisible();
await expect(
tableEditorFooter.getByRole("button", { name: "Save metadata", exact: true }),
).toBeVisible();
await tableEditor
.getByRole("button", { name: "Close table metadata", exact: true })
.click();
const gridRoot = page.locator(
'main[aria-label="Database management"] .thot-fleet-ledger-grid .ag-root-wrapper',
);
await expect(gridRoot).toHaveCount(1);
await expect(gridRoot).toBeVisible();
await expect.poll(
() => gridRoot.evaluate((element) => {
const manager = element.closest('main[aria-label="Database management"]');
if (!manager) throw new Error("Database manager is missing");
const managerBottom = manager.getBoundingClientRect().bottom;
const gridBottom = element.getBoundingClientRect().bottom;
return Math.max(
Math.abs(window.innerHeight - managerBottom),
Math.abs(managerBottom - gridBottom - 30),
);
}),
{ message: "AG Grid must end 30px above the manager bottom" },
).toBeLessThanOrEqual(1);
const gapColors = await gridRoot.evaluate((element) => {
const workspace = element.closest(".thot-fleet-ledger__workspace");
const tableSurface = element.closest(".thot-fleet-ledger__content");
const sidebar = document.querySelector('aside[aria-label="Session navigation"]');
if (!workspace || !tableSurface || !sidebar) {
throw new Error("Application layout is missing");
}
return {
gap: getComputedStyle(workspace).backgroundColor,
table: getComputedStyle(tableSurface).backgroundColor,
sidebar: getComputedStyle(sidebar).backgroundColor,
};
});
expect(gapColors.gap).not.toBe("rgba(0, 0, 0, 0)");
expect(gapColors.gap).toBe(gapColors.sidebar);
expect(gapColors.gap).not.toBe(gapColors.table);
const workspace = page.locator(
'main[aria-label="Database management"] .thot-fleet-ledger__workspace',
);
const tableSurface = page.locator(
'main[aria-label="Database management"] .thot-fleet-ledger__content',
);
const sessionNavigation = page.getByRole("complementary", {
name: "Session navigation",
});
await expect.poll(
async () => {
const [workspaceBox, tableBox, gridBox, sidebarBox] = await Promise.all([
workspace.boundingBox(),
tableSurface.boundingBox(),
gridRoot.boundingBox(),
sessionNavigation.boundingBox(),
]);
if (!workspaceBox || !tableBox || !gridBox || !sidebarBox) {
throw new Error("Database frame geometry is missing");
}
return Math.max(
Math.abs(tableBox.x - workspaceBox.x - 20),
Math.abs(tableBox.x + tableBox.width - sidebarBox.x),
Math.abs(tableBox.y + tableBox.height - gridBox.y - gridBox.height),
);
},
{
message:
"The table must have a 20px left frame and its right border must stop at the footer",
},
).toBeLessThanOrEqual(1);
const frameBorders = await tableSurface.evaluate((element) => {
const sidebar = document.querySelector('aside[aria-label="Session navigation"]');
if (!sidebar) throw new Error("Session navigation is missing");
return {
tableEnd: Number.parseFloat(getComputedStyle(element).borderInlineEndWidth),
sidebarStart: Number.parseFloat(
getComputedStyle(sidebar).borderInlineStartWidth,
),
};
});
expect(frameBorders.tableEnd).toBe(1);
expect(frameBorders.sidebarStart).toBe(0);
await expect(page.locator(".thot-session-navigation__boundary")).toHaveCount(0);
expect(unexpectedCatalogRequests).toEqual([]);
});
test("Workspace and Pi management share the centered work-area panel without covering the session rail", async ({ page }) => {
await page.route("**/api/workspace-registry/status", (route) => json(route, {
branch: "main",
head: "a".repeat(40),
ahead: 0,
behind: 0,
degraded: false,
repository: {
host: "git.example.test",
repository: "analytics/thoth-workspaces",
transport: "ssh",
},
}));
await page.route("**/api/workspaces", (route) => json(route, []));
await page.route("**/api/pi-management/status", (route) => json(route, {
version: "0.80.3",
ready: true,
credentials: "present",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-31T09:00:00Z",
}));
await page.route("**/api/pi-management/options", (route) => json(route, {
providers: ["zai"],
models: [{ provider: "zai", id: "glm-5.2" }],
reasoning: ["low", "medium", "high"],
checkedAt: "2026-08-31T09:00:00Z",
}));
await page.goto(stack.publicUrl);
await signInAsAdmin(page);
for (const viewport of [
{ width: 1910, height: 911 },
{ width: 1280, height: 800 },
]) {
await page.setViewportSize(viewport);
const workspaceTrigger = page.getByRole("button", {
name: "Workspace management",
exact: true,
});
await workspaceTrigger.click();
await expectWorkAreaPanelGeometry(page, "Workspace management");
await expectWorkAreaPanelHeaderMatchesNewSession(page, "Workspace management");
const piTrigger = page.getByRole("button", { name: "Pi management", exact: true });
await piTrigger.click();
await expect(
page.getByRole("dialog", { name: "Workspace management" }),
).toHaveCount(0);
await expectWorkAreaPanelGeometry(page, "Pi management");
await expectWorkAreaPanelHeaderMatchesNewSession(page, "Pi management");
await page
.getByRole("dialog", { name: "Pi management" })
.getByRole("button", { name: "Close Pi management" })
.click();
await expect(piTrigger).toBeFocused();
}
await page.setViewportSize({ width: 720, height: 800 });
await page.getByRole("button", { name: "Workspace management", exact: true }).click();
const compactWorkArea = page.getByTestId("conversation-column");
const compactPanel = compactWorkArea.getByRole("dialog", { name: "Workspace management" });
const [compactWorkAreaBox, compactPanelBox] = await Promise.all([
compactWorkArea.boundingBox(),
compactPanel.boundingBox(),
]);
expect(compactWorkAreaBox).not.toBeNull();
expect(compactPanelBox).not.toBeNull();
if (compactWorkAreaBox && compactPanelBox) {
expect(Math.abs(compactPanelBox.width - compactWorkAreaBox.width + 16)).toBeLessThanOrEqual(1);
expect(Math.abs(
compactPanelBox.x + compactPanelBox.width / 2
- (compactWorkAreaBox.x + compactWorkAreaBox.width / 2),
)).toBeLessThanOrEqual(1);
}
const compactNavigation = compactPanel.getByRole("navigation", { name: "Workspaces" });
const compactContent = compactPanel.getByRole("region", {
name: "Workspace configuration",
});
const [compactNavigationBox, compactContentBox] = await Promise.all([
compactNavigation.boundingBox(),
compactContent.boundingBox(),
]);
expect(compactNavigationBox).not.toBeNull();
expect(compactContentBox).not.toBeNull();
if (compactNavigationBox && compactContentBox) {
expect(compactContentBox.y).toBeGreaterThanOrEqual(
compactNavigationBox.y + compactNavigationBox.height - 1,
);
}
});