feat: consolidate database management work

Add catalog-owned logical relationships and runtime snapshots, extend the database-management UI and validation coverage, and document the updated operational workflow.

Keep active sensitive-generation status in a tooltip and indicator, and update the layout E2E to follow the history action in its new database-scoped location.
This commit is contained in:
Codex
2026-09-01 14:46:55 +02:00
parent f586152636
commit 076c9742c5
73 changed files with 6966 additions and 610 deletions
+239 -44
View File
@@ -1,4 +1,4 @@
import { expect, test, type Page, type Route } from "@playwright/test";
import { expect, test, type Locator, type Page, type Route } from "@playwright/test";
import { createAuthenticationStack } from "./fixtures/auth-stack.mjs";
test.describe.configure({ mode: "serial" });
@@ -12,6 +12,13 @@ const database = {
workspaceId: "psd-clinical",
workspaceName: "Policlinico San Donato",
workspaceAvailable: true,
workspaceRevision: { commit: "a".repeat(40), blob: "b".repeat(40) },
workspaceEvidence: { sourceType: "filesystem", state: "materialized_current_revision" },
runtimeBinding: {
transport: "postgres_direct",
configurationState: "ready",
sessionTransportSupported: true,
},
configured: true,
engine: "postgres",
databaseName: "warehouse",
@@ -37,6 +44,22 @@ const database = {
},
};
const unconfiguredDatabase = {
...database,
id: undefined,
workspaceId: "research-lab",
workspaceName: "Research laboratory",
configured: false,
databaseName: "research",
schema: "analytics",
version: 0,
createdAt: "",
updatedAt: "",
binding: { transport: "postgres_direct", port: 5432 },
connectionStatus: "untested",
testedVersion: undefined,
};
const table = {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
databaseId,
@@ -118,7 +141,112 @@ async function expectContextPanelGeometry(page: Page, accessibleName: string) {
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(managerBox.y + managerBox.height + 1);
}
async function expectContextPanelHeaderMatchesNewSession(
async function expectCurrentNavigation(page: Page, accessibleName: string) {
const navigation = page.getByRole("complementary", { name: "Session navigation" });
const button = navigation.getByRole("button", { name: accessibleName, exact: true });
const available = navigation.locator('[data-navigation-state="available"]').first();
await expect(navigation.locator('[data-navigation-state="current"]')).toHaveCount(1);
await expect(button).toHaveAttribute("data-navigation-state", "current");
await expect(button).toHaveAttribute("aria-current", "page");
expect(await button.getAttribute("class")).toContain("bg-[oklch(var(--nav-active))]");
const [style, availableStyle] = await Promise.all([
button.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderTopColor,
})),
available.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderTopColor,
})),
]);
expect(style.backgroundColor).not.toBe(availableStyle.backgroundColor);
expect(style.borderColor).not.toBe(availableStyle.borderColor);
const tokens = await page.evaluate(() => {
const root = getComputedStyle(document.documentElement);
const read = (name: string) => root.getPropertyValue(name).trim().split(/\s+/).map(Number);
return { primary: read("--primary"), active: read("--nav-active") };
});
expect(tokens.active[2]).toBeCloseTo(tokens.primary[2], 1);
expect(tokens.active[1]).toBeLessThan(tokens.primary[1]);
expect(tokens.active[0]).toBeGreaterThan(tokens.primary[0]);
}
async function expectSelectedSessionScopeTab(page: Page, accessibleName: string) {
const tablist = page.getByRole("tablist", { name: "Session scope" });
const selected = tablist.getByRole("tab", { name: accessibleName, exact: true });
const inactive = tablist.locator('[role="tab"][aria-selected="false"]');
await expect(tablist.locator('[role="tab"][aria-selected="true"]')).toHaveCount(1);
await expect(selected).toHaveAttribute("data-tab-state", "active");
expect(await selected.getAttribute("class")).toContain("bg-[oklch(var(--nav-active))]");
const [selectedStyle, inactiveStyle] = await Promise.all([
selected.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderBottomColor,
})),
inactive.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderBottomColor,
borderWidth: getComputedStyle(element).borderTopWidth,
})),
]);
expect(selectedStyle.backgroundColor).not.toBe(inactiveStyle.backgroundColor);
expect(selectedStyle.borderColor).not.toBe(inactiveStyle.borderColor);
expect(inactiveStyle.borderWidth).toBe("1px");
expect(inactiveStyle.backgroundColor).not.toMatch(/^(?:transparent|rgba\(0, 0, 0, 0\))$/);
expect(inactiveStyle.borderColor).not.toMatch(/^(?:transparent|rgba\(0, 0, 0, 0\))$/);
}
function cssLightness(color: string): number {
const oklab = color.match(/^okl(?:ab|ch)\(([\d.]+)(%)?/);
if (oklab) {
const value = Number(oklab[1]);
return oklab[2] ? value / 100 : value;
}
const channels = color.match(/[\d.]+/g)?.slice(0, 3).map(Number) ?? [];
if (channels.length !== 3) return Number.NaN;
return color.startsWith("color(srgb") ? Math.max(...channels) : Math.max(...channels) / 255;
}
async function expectFleetRowTooltipBelow(trigger: Locator, expectedContent: string) {
await trigger.hover();
await expect.poll(() => trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
return owner ? getComputedStyle(owner, "::after").visibility : "missing";
})).toBe("visible");
await expect.poll(() => trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
return owner ? getComputedStyle(owner, "::after").opacity : "missing";
})).toBe("1");
const state = await trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
if (!owner) throw new Error("Row action tooltip owner is missing");
const style = getComputedStyle(owner, "::after");
return {
content: style.content.replace(/^['\"]|['\"]$/g, ""),
ownerHeight: owner.getBoundingClientRect().height,
top: Number.parseFloat(style.top),
right: Number.parseFloat(style.right),
opacity: style.opacity,
pointerEvents: style.pointerEvents,
backgroundColor: style.backgroundColor,
color: style.color,
};
});
expect(state.content).toBe(expectedContent);
expect(state.top - state.ownerHeight).toBeCloseTo(3, 1);
expect(state.right).toBe(0);
expect(state.opacity).toBe("1");
expect(state.pointerEvents).toBe("none");
expect(cssLightness(state.backgroundColor)).toBeLessThan(0.35);
expect(cssLightness(state.color)).toBeGreaterThan(0.9);
}
async function expectContextPanelHeaderUsesPrimary(
page: Page,
accessibleName: string,
) {
@@ -127,26 +255,22 @@ async function expectContextPanelHeaderMatchesNewSession(
.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([
const currentNavigation = page.locator('[data-navigation-state="current"]');
const [headerStyle, navigationStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
newSession.evaluate((element) => ({
currentNavigation.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);
expect(headerStyle.backgroundColor).not.toBe(navigationStyle.backgroundColor);
}
async function expectWorkAreaPanelGeometry(page: Page, accessibleName: string) {
@@ -181,7 +305,7 @@ async function expectWorkAreaPanelGeometry(page: Page, accessibleName: string) {
expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(workAreaBox.y + workAreaBox.height + 1);
}
async function expectWorkAreaPanelHeaderMatchesNewSession(
async function expectWorkAreaPanelHeaderUsesPrimary(
page: Page,
accessibleName: string,
) {
@@ -189,21 +313,19 @@ async function expectWorkAreaPanelHeaderMatchesNewSession(
.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([
const currentNavigation = page.locator('[data-navigation-state="current"]');
const [headerStyle, navigationStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
newSession.evaluate((element) => ({
currentNavigation.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
})),
]);
expect(headerStyle.backgroundImage).toBe("none");
expect(headerStyle.backgroundColor).toBe(actionStyle.backgroundColor);
expect(headerStyle.backgroundColor).not.toBe(navigationStyle.backgroundColor);
}
test.beforeAll(async () => {
@@ -218,10 +340,14 @@ test.afterAll(async () => {
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/databases": [database, unconfiguredDatabase],
"GET /api/catalog/metadata-generation/models": {
models: [],
default: null,
models: [
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
{ id: "glm-53", label: "GLM 5.3" },
{ id: "qwen-36", label: "AritmoLab Qwen 3.6 35B A3B" },
],
default: "glm-53",
},
"GET /api/catalog/description-generation-runs?limit=50": [],
"GET /api/catalog/sensitive-data-suggestion-runs?limit=50": [],
@@ -252,10 +378,40 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
await page.goto(stack.publicUrl);
await signInAsAdmin(page);
await expectCurrentNavigation(page, "New session");
await expectSelectedSessionScopeTab(page, "My sessions");
await page.getByRole("tab", { name: "All sessions", exact: true }).click();
await expectSelectedSessionScopeTab(page, "All sessions");
await page.getByRole("tab", { name: "My sessions", exact: true }).click();
await expectSelectedSessionScopeTab(page, "My sessions");
await page
const adminNavigationRail = page.getByRole("complementary", { name: "Session navigation" });
const administration = adminNavigationRail.getByRole("button", { name: "Administration", exact: true });
await expect(administration).toHaveAttribute("aria-expanded", "false");
await expect(adminNavigationRail.getByRole("region", { name: "Administration" })).toHaveCount(0);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
const administrationPanel = adminNavigationRail.getByRole("region", { name: "Administration" });
await expect(administrationPanel).toBeVisible();
expect(await administrationPanel.locator(":scope > *").evaluateAll((elements) => elements.map((element) => (
element.getAttribute("role") === "separator" ? "separator" : element.textContent?.trim()
)))).toEqual([
"Database management",
"separator",
"Workspace management",
"Pi management",
]);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "false");
await expect(adminNavigationRail.getByRole("button", { name: "Database management", exact: true })).toHaveCount(0);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
await adminNavigationRail
.getByRole("button", { name: "Database management", exact: true })
.click();
await expectCurrentNavigation(page, "Database management");
await expect(
page.getByRole("complementary", { name: "Session navigation" }),
@@ -268,27 +424,41 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
await expect(
page.getByRole("button", { name: "Back to workspace", exact: true }),
).toHaveCount(0);
await expect(page.getByRole("button", { name: /Add database/i })).toHaveCount(0);
await expect(page.getByRole("columnheader", { name: /Revision \/ Evidence/ })).toBeVisible();
await expect(page.getByRole("columnheader", { name: /NL→SQL runtime/ })).toBeVisible();
await expect(page.getByRole("columnheader", { name: /Metadata Catalog/ })).toBeVisible();
const metadataModelSelector = page.getByRole("combobox", {
name: "Metadata-generation LLM model",
});
await expect(metadataModelSelector).toHaveCount(1);
await expect(metadataModelSelector).toHaveValue("glm-53");
await expect(metadataModelSelector.locator("option")).toHaveText([
"DeepSeek V4 Pro",
"GLM 5.3",
"AritmoLab Qwen 3.6 35B A3B",
]);
await page
.getByRole("button", { name: "Configure catalog for Research laboratory", exact: true })
.click();
const configurePanel = page.getByRole("dialog", { name: "Configure catalog" });
await expectContextPanelGeometry(page, "Configure catalog");
const configuredWorkspace = configurePanel.getByLabel("Workspace", { exact: true });
await expect(configuredWorkspace).toHaveValue("Research laboratory");
await expect(configuredWorkspace).toHaveAttribute("readonly", "");
await configurePanel.getByRole("button", { name: "Close database panel" }).click();
await page
.getByRole("button", { name: "Edit Policlinico San Donato", exact: true })
.click();
await expectContextPanelGeometry(page, "Edit database");
await expectContextPanelHeaderMatchesNewSession(page, "Edit database");
await expectContextPanelHeaderUsesPrimary(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 })
@@ -300,12 +470,12 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
.click();
await page.setViewportSize({ width: 1910, height: 911 });
await page
.getByRole("button", {
name: "View tables for Policlinico San Donato",
exact: true,
})
.click();
const databaseTablesAction = page.getByRole("button", {
name: "View tables for Policlinico San Donato",
exact: true,
});
await expectFleetRowTooltipBelow(databaseTablesAction, "Tables");
await databaseTablesAction.click();
await expect(
page.getByRole("region", {
@@ -313,6 +483,16 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
}),
).toBeVisible();
await page
.getByRole("button", { name: "Description history", exact: true })
.click();
await expectContextPanelGeometry(page, "Description generation");
await expectContextPanelHeaderUsesPrimary(page, "Description generation");
await page
.getByRole("dialog", { name: "Description generation" })
.getByRole("button", { name: /close/i })
.click();
const applicationBar = page.locator(
'main[aria-label="Database management"] .thot-fleet-ledger__header',
);
@@ -326,6 +506,10 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
await expect(
applicationBar.getByRole("group", { name: "Database management actions" }),
).toBeVisible();
await expect(
applicationBar.getByRole("combobox", { name: "Metadata-generation LLM model" }),
).toHaveValue("glm-53");
await expect(metadataModelSelector).toHaveCount(1);
await expect(applicationBar).not.toContainText("Thoth catalog · Fleet ledger");
await expect(applicationBar).not.toContainText("Inspect physical metadata");
@@ -355,11 +539,14 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
await expect(page.getByRole("row", { name: /patients/ })).toBeVisible();
await page
.getByRole("button", { name: "Edit description for patients", exact: true })
.click();
const editPatientsAction = page.getByRole("button", {
name: "Edit description for patients",
exact: true,
});
await expectFleetRowTooltipBelow(editPatientsAction, "Edit metadata");
await editPatientsAction.click();
await expectContextPanelGeometry(page, "Review table description");
await expectContextPanelHeaderMatchesNewSession(page, "Review table description");
await expectContextPanelHeaderUsesPrimary(page, "Review table description");
const tableEditor = page.getByRole("dialog", { name: "Review table description" });
const tableEditorFooter = tableEditor.locator(
':scope > [data-catalog-panel-region="footer"]',
@@ -500,6 +687,10 @@ test("Workspace and Pi management share the centered work-area panel without cov
await page.goto(stack.publicUrl);
await signInAsAdmin(page);
const administration = page.getByRole("button", { name: "Administration", exact: true });
await expect(administration).toHaveAttribute("aria-expanded", "false");
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
for (const viewport of [
{ width: 1910, height: 911 },
@@ -511,25 +702,29 @@ test("Workspace and Pi management share the centered work-area panel without cov
exact: true,
});
await workspaceTrigger.click();
await expectCurrentNavigation(page, "Workspace management");
await expectWorkAreaPanelGeometry(page, "Workspace management");
await expectWorkAreaPanelHeaderMatchesNewSession(page, "Workspace management");
await expectWorkAreaPanelHeaderUsesPrimary(page, "Workspace management");
const piTrigger = page.getByRole("button", { name: "Pi management", exact: true });
await piTrigger.click();
await expectCurrentNavigation(page, "Pi management");
await expect(
page.getByRole("dialog", { name: "Workspace management" }),
).toHaveCount(0);
await expectWorkAreaPanelGeometry(page, "Pi management");
await expectWorkAreaPanelHeaderMatchesNewSession(page, "Pi management");
await expectWorkAreaPanelHeaderUsesPrimary(page, "Pi management");
await page
.getByRole("dialog", { name: "Pi management" })
.getByRole("button", { name: "Close Pi management" })
.click();
await expect(piTrigger).toBeFocused();
await expectCurrentNavigation(page, "New session");
}
await page.setViewportSize({ width: 720, height: 800 });
await page.getByRole("button", { name: "Workspace management", exact: true }).click();
await expectCurrentNavigation(page, "Workspace management");
const compactWorkArea = page.getByTestId("conversation-column");
const compactPanel = compactWorkArea.getByRole("dialog", { name: "Workspace management" });
const [compactWorkAreaBox, compactPanelBox] = await Promise.all([