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
@@ -29,6 +29,13 @@ function makeDatabase(overrides: Partial<CatalogDatabase> = {}): CatalogDatabase
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",
@@ -66,6 +73,9 @@ const orphan = makeDatabase({
workspaceId: "retired",
workspaceName: "Retired workspace",
workspaceAvailable: false,
workspaceRevision: null,
workspaceEvidence: { sourceType: null, state: "workspace_unavailable" },
runtimeBinding: null,
});
function renderPage({
@@ -192,7 +202,7 @@ const synchronizationScopes = [
{ scope: "all", label: "Synchronize all" },
] as const;
test("renders Fleet Ledger with real metrics, conceptual row tooltips, and contextual model selection", async () => {
test("renders Fleet Ledger with real metrics, conceptual row tooltips, and persistent model selection", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/metrics", () => HttpResponse.json({
@@ -223,10 +233,15 @@ test("renders Fleet Ledger with real metrics, conceptual row tooltips, and conte
expect(screen.queryByText("Thoth catalog · Fleet ledger")).not.toBeInTheDocument();
expect(screen.queryByText("Inspect physical metadata, curate descriptions and control catalog operations.")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Back to workspace" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Add database/i })).not.toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: /Revision \/ Evidence/ })).toBeVisible();
expect(screen.getByRole("columnheader", { name: /NL→SQL runtime/ })).toBeVisible();
expect(screen.getByRole("columnheader", { name: /Metadata Catalog/ })).toBeVisible();
const summary = screen.getByRole("region", { name: "Fleet summary" });
expect(await within(summary).findByText("2,275")).toBeVisible();
expect(within(summary).getByText("75%")).toBeVisible();
expect(screen.queryByRole("combobox", { name: "Metadata description model" })).not.toBeInTheDocument();
const modelSelector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
expect(modelSelector).toHaveValue("local-qwen");
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
const tablesAction = within(databaseRow).getByRole("button", { name: "View tables for Policlinico San Donato" });
@@ -247,10 +262,103 @@ test("renders Fleet Ledger with real metrics, conceptual row tooltips, and conte
const actionPicker = screen.getByRole("combobox", { name: "Batch action" });
expect(within(actionPicker).getByRole("option", { name: /^Synchronize all(?:,|$)/ })).toBeVisible();
await user.selectOptions(actionPicker, "generate-missing");
expect(await screen.findByRole("combobox", { name: "Metadata description model" })).toHaveValue("local-qwen");
expect(screen.getAllByRole("combobox", { name: "Metadata-generation LLM model" })).toHaveLength(1);
expect(modelSelector).toHaveValue("local-qwen");
});
test("shows repository, NL→SQL runtime, and Metadata Catalog states independently", async () => {
const configured = makeDatabase({ connectionStatus: "reachable", testedVersion: 3 });
const needsRuntimeConfiguration = makeDatabase({
...unconfigured,
workspaceEvidence: { sourceType: "http", state: "configuration_required" },
runtimeBinding: {
transport: "rest_api",
configurationState: "configuration_required",
sessionTransportSupported: true,
},
});
renderPage({ rows: [configured, needsRuntimeConfiguration, orphan], presentation: "fleet" });
const configuredRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
expect(within(configuredRow).getByText("Active revision aaaaaaa")).toBeVisible();
expect(within(configuredRow).getByText("Evidence materialized · filesystem")).toBeVisible();
expect(within(configuredRow).getByText("Ready")).toBeVisible();
expect(within(configuredRow).getByText("Configured")).toBeVisible();
expect(within(configuredRow).getByText("Connection reachable")).toBeVisible();
const needsConfigurationRow = screen.getByRole("row", { name: /Research laboratory/ });
expect(within(needsConfigurationRow).getByText("Evidence credentials required · http")).toBeVisible();
expect(within(needsConfigurationRow).getByText("Configuration required")).toBeVisible();
expect(within(needsConfigurationRow).getByText("Not configured")).toBeVisible();
const orphanRow = screen.getByRole("row", { name: /Retired workspace/ });
expect(within(orphanRow).getByText("Workspace missing")).toBeVisible();
expect(within(orphanRow).getByText("Unavailable")).toBeVisible();
expect(within(orphanRow).getByText("Orphaned configuration")).toBeVisible();
});
test("keeps Fleet Ledger visible when a catalog response omits the Evidence projection", async () => {
const legacyDatabase = makeDatabase();
delete (legacyDatabase as Partial<CatalogDatabase>).workspaceEvidence;
renderPage({ rows: [legacyDatabase], presentation: "fleet" });
expect(await screen.findByRole("heading", { name: "Database management" })).toBeVisible();
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
expect(within(databaseRow).getByText("Evidence state unavailable")).toBeVisible();
});
test("opens the relationship map directly from a Fleet database and restores focus on return", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/databases/:databaseId/relationships", () => HttpResponse.json([])),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
presentation: "fleet",
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
const relationshipsAction = within(databaseRow).getByRole("button", {
name: "View relationships for Policlinico San Donato",
});
expect(relationshipsAction.parentElement).toHaveAttribute("data-tooltip", "Relationships");
expect(relationshipsAction).not.toHaveAttribute("title");
await user.click(relationshipsAction);
expect(await screen.findByRole("region", {
name: "Relationships for Policlinico San Donato",
})).toBeVisible();
expect(screen.getByText("Relationship map")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Back to databases" }));
await waitFor(() => expect(screen.getByRole("button", {
name: "View relationships for Policlinico San Donato",
})).toHaveFocus());
});
test("offers catalog configuration directly on an unconfigured Fleet workspace", async () => {
const user = userEvent.setup();
renderPage({ rows: [unconfigured], presentation: "fleet" });
const databaseRow = await screen.findByRole("row", { name: /Research laboratory/ });
expect(within(databaseRow).queryByRole("button", {
name: "View relationships for Research laboratory",
})).not.toBeInTheDocument();
expect(within(databaseRow).queryByRole("button", {
name: "View tables for Research laboratory",
})).not.toBeInTheDocument();
await user.click(within(databaseRow).getByRole("button", {
name: "Configure catalog for Research laboratory",
}));
const drawer = await screen.findByRole("dialog", { name: "Configure catalog" });
expect(within(drawer).getByLabelText("Workspace")).toHaveAttribute("readonly");
expect(within(drawer).getByLabelText("Workspace")).toHaveValue("Research laboratory");
});
test("keeps only the local back action while browsing Fleet Ledger columns", async () => {
const user = userEvent.setup();
server.use(
@@ -276,6 +384,32 @@ test("keeps only the local back action while browsing Fleet Ledger columns", asy
expect(screen.queryByRole("button", { name: "Back to tables" })).not.toBeInTheDocument();
});
test("uses the database back-action background treatment for the table back action", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase()], presentation: "fleet" });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("button", {
name: "View tables for Policlinico San Donato",
}));
const databaseBackAction = await screen.findByRole("button", { name: "Back to databases" });
expect(databaseBackAction).toHaveClass("thot-fleet-back-action");
const backgroundTreatment = (element: HTMLElement) => element.className
.split(/\s+/)
.filter((className) => /(^|:)bg-/.test(className))
.sort();
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const tableBackAction = await screen.findByRole("button", { name: "Back to tables" });
expect(tableBackAction).toHaveClass("thot-fleet-back-action");
expect(backgroundTreatment(tableBackAction)).toEqual(backgroundTreatment(databaseBackAction));
});
test("reopens database synchronization history when no run is active", async () => {
const user = userEvent.setup();
const completed: CatalogSyncRun = {
@@ -312,7 +446,7 @@ test("selects the configured metadata-description model by default", async () =>
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
await waitFor(() => expect(selector).toHaveValue("local-qwen"));
});
@@ -320,7 +454,7 @@ test("keeps both run-history buttons visible beside the metadata-description sel
const user = userEvent.setup();
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
const toolbar = screen.getByRole("group", { name: "Database management controls" });
const metadataControls = screen.getByRole("group", { name: "Metadata description controls" });
const actions = screen.getByRole("group", { name: "Database management actions" });
@@ -344,7 +478,7 @@ test("keeps both run-history buttons visible beside the metadata-description sel
expect(toolbar.lastElementChild).toBe(actions);
expect(actions).toHaveClass("sm:justify-end");
expect(actions).toContainElement(screen.getByRole("button", { name: "Refresh" }));
expect(actions).toContainElement(screen.getByRole("button", { name: "Add database" }));
expect(within(actions).queryByRole("button", { name: /Add database/i })).not.toBeInTheDocument();
expect(screen.queryByRole("note", {
name: "Metadata generation source data disclosure",
})).not.toBeInTheDocument();
@@ -371,7 +505,7 @@ test("changes the metadata-description model in page-local state", async () => {
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
await waitFor(() => expect(selector).toHaveValue("local-qwen"));
await user.selectOptions(selector, "openai-mini");
@@ -385,10 +519,10 @@ test("explains application setup when no metadata-description model is configure
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
expect(selector).toBeDisabled();
expect(await screen.findByText(
"Configure a metadata-generation model in application setup to enable description generation.",
"No metadata-generation LLM model is configured for this installation.",
)).toBeVisible();
});
@@ -399,11 +533,11 @@ test("uses a safe disabled setup state when metadata-description models are unav
}, { status: 503 })));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
await waitFor(() => expect(selector).toHaveDisplayValue("Model choices unavailable"));
expect(selector).toBeDisabled();
expect(screen.getByText(
"Configure a metadata-generation model in application setup to enable description generation.",
"Metadata-generation LLM model choices are unavailable.",
)).toBeVisible();
expect(screen.queryByText("internal model registry details")).not.toBeInTheDocument();
});
@@ -417,11 +551,11 @@ test.each([null, "missing-model"])(
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
await waitFor(() => expect(selector).toHaveDisplayValue("No usable model configured"));
expect(selector).toBeDisabled();
expect(screen.getByText(
"Configure a metadata-generation model in application setup to enable description generation.",
"No metadata-generation LLM model is configured for this installation.",
)).toBeVisible();
},
);
@@ -441,7 +575,7 @@ test("renders only the public metadata-generation model contract", async () => {
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Metadata description model" });
const selector = await screen.findByRole("combobox", { name: "Metadata-generation LLM model" });
await waitFor(() => expect(selector).toHaveValue("approved-model"));
expect(within(selector).getAllByRole("option").map((option) => option.textContent))
.toEqual(["Approved model"]);
@@ -458,8 +592,9 @@ test("starts with a full-width list and applies the row action matrix", async ()
expect(screen.getByRole("button", { name: "Delete Policlinico San Donato" })).toBeEnabled();
expect(screen.getByRole("button", { name: "View Research laboratory" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Edit Research laboratory" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Delete Research laboratory" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Configure catalog for Research laboratory" })).toBeEnabled();
expect(screen.queryByRole("button", { name: "Edit Research laboratory" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Delete Research laboratory" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "View Retired workspace" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Edit Retired workspace" })).toBeDisabled();
@@ -558,7 +693,7 @@ test("keeps Fleet Generate Missing behind the source-data disclosure", async ()
"generate-missing",
);
await waitFor(() => expect(screen.getByRole("combobox", {
name: "Metadata description model",
name: "Metadata-generation LLM model",
})).toHaveValue("local-qwen"));
await user.click(screen.getByRole("button", { name: "Run action" }));
@@ -951,7 +1086,7 @@ test("replaces the list with View and returns focus to the originating action",
expect(screen.getByRole("button", { name: "View Policlinico San Donato" })).toBeVisible();
});
test("opens an unconfigured row as Edit while creating its first saved configuration", async () => {
test("configures the catalog for the selected workspace and locks that workspace", async () => {
const user = userEvent.setup();
let postBody: unknown;
const saved = makeDatabase({
@@ -971,16 +1106,16 @@ test("opens an unconfigured row as Edit while creating its first saved configura
);
renderPage({ rows: () => rows });
await user.click(await screen.findByRole("button", { name: "Edit Research laboratory" }));
await user.click(await screen.findByRole("button", { name: "Configure catalog for Research laboratory" }));
expect(screen.getByRole("heading", { name: "Edit database" })).toBeVisible();
expect(screen.getByLabelText("Workspace")).toBeDisabled();
expect(screen.getByRole("heading", { name: "Configure catalog" })).toBeVisible();
expect(screen.getByLabelText("Workspace")).toHaveAttribute("readonly");
await user.selectOptions(screen.getByLabelText("Transport"), "rest_api");
await user.type(screen.getByLabelText("Base URL"), "https://psd.example/api");
expect(screen.getByLabelText("Diagnostic endpoint")).toHaveValue("/health");
expect(screen.getByLabelText("Diagnostic endpoint")).toHaveAttribute("readonly");
await user.click(screen.getByRole("button", { name: "Save database" }));
await user.click(screen.getByRole("button", { name: "Save catalog configuration" }));
await waitFor(() => expect(postBody).toMatchObject({
workspaceId: "lab",
@@ -989,7 +1124,7 @@ test("opens an unconfigured row as Edit while creating its first saved configura
expect(await screen.findByRole("heading", { name: "Edit database" })).toBeVisible();
});
test("global Add offers only unconfigured workspaces and lets the operator choose one", async () => {
test("has no global database creation path and scopes configuration to the chosen row", async () => {
const user = userEvent.setup();
const second = makeDatabase({
...unconfigured,
@@ -999,14 +1134,13 @@ test("global Add offers only unconfigured workspaces and lets the operator choos
});
renderPage({ rows: [makeDatabase(), unconfigured, second] });
await user.click(await screen.findByRole("button", { name: "Add database" }));
expect(screen.queryByRole("button", { name: /Add database/i })).not.toBeInTheDocument();
await user.click(await screen.findByRole("button", { name: "Configure catalog for Radiology" }));
expect(screen.getByRole("heading", { name: "Add database" })).toBeVisible();
expect(screen.getByRole("button", { name: "Add database" })).toBeVisible();
const selector = screen.getByLabelText("Workspace");
expect(selector).toBeEnabled();
expect(screen.queryByRole("option", { name: "Policlinico San Donato" })).not.toBeInTheDocument();
await user.selectOptions(selector, "radiology");
expect(screen.getByRole("heading", { name: "Configure catalog" })).toBeVisible();
const workspace = screen.getByLabelText("Workspace");
expect(workspace).toHaveAttribute("readonly");
expect(workspace).toHaveValue("Radiology");
expect(screen.getByDisplayValue("radiology_dwh")).toBeVisible();
});
@@ -1064,7 +1198,8 @@ test("uses an in-page destructive form and returns the YAML workspace to Not con
await waitFor(() => expect(deleteCalled).toBe(true));
expect(deleteVersion).toBe("3");
expect(await screen.findByRole("button", { name: "Delete Policlinico San Donato" })).toBeDisabled();
expect(await screen.findByRole("button", { name: "Configure catalog for Policlinico San Donato" })).toBeEnabled();
expect(screen.queryByRole("button", { name: "Delete Policlinico San Donato" })).not.toBeInTheDocument();
expect(screen.getByText("Not configured")).toBeVisible();
});
@@ -2677,6 +2812,8 @@ test("navigates from a table to columns and from the database to physical relati
deferrable: false,
initiallyDeferred: false,
columns: [{ position: 1, sourceColumnId: "1", sourceColumnName: "patient_id", targetColumnId: idColumn.id, targetColumnName: "id" }],
origin: "physical",
status: "active",
lastSyncedDatabaseVersion: 3,
lastSyncedAt: "2026-08-27T10:00:00Z",
createdAt: "2026-08-27T10:00:00Z",