Files
ThothII/frontend/src/shell/DatabaseManagementPage.test.tsx
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

3544 lines
157 KiB
TypeScript

import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { setLocale } from "../i18n";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { http, HttpResponse } from "msw";
import { server } from "../test/msw";
import type {
CatalogColumn,
CatalogDatabase,
CatalogRelationship,
CatalogSyncRun,
CatalogTable,
DescriptionGenerationRun,
SensitivityAnalysisRun,
} from "../api/catalog-databases";
import { Toaster } from "../components/ui/sonner";
import { DatabaseManagementPage } from "./DatabaseManagementPage";
import { workspacePreferences } from "../workspaces/preferences";
import { ComposerFooter } from "./SteerInput";
import { useSessionStore } from "../store/sessionStore";
beforeEach(() => { localStorage.clear(); });
const noSecrets = {
password: false,
apiKey: false,
sshPrivateKey: false,
sshPrivateKeyPassphrase: false,
sshKnownHosts: false,
tlsCa: false,
};
function makeDatabase(overrides: Partial<CatalogDatabase> = {}): CatalogDatabase {
return {
id: "11111111-1111-4111-8111-111111111111",
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",
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: "untested",
secrets: { ...noSecrets },
...overrides,
};
}
const unconfigured = makeDatabase({
id: undefined,
workspaceId: "lab",
workspaceName: "Research laboratory",
configured: false,
databaseName: "lab_warehouse",
schema: "analytics",
version: 0,
createdAt: "",
updatedAt: "",
binding: { transport: "postgres_direct", port: 5432 },
});
const orphan = makeDatabase({
id: "22222222-2222-4222-8222-222222222222",
workspaceId: "retired",
workspaceName: "Retired workspace",
workspaceAvailable: false,
workspaceRevision: null,
workspaceEvidence: { sourceType: null, state: "workspace_unavailable" },
runtimeBinding: null,
});
function renderPage({
rows = [makeDatabase(), unconfigured, orphan],
canManage = true,
canManageSecrets = true,
onNavigationStateChange,
presentation = "legacy",
}: {
rows?: CatalogDatabase[] | (() => CatalogDatabase[]);
canManage?: boolean;
canManageSecrets?: boolean;
onNavigationStateChange?: (state: { dirty: boolean; busy: boolean }) => void;
presentation?: "legacy" | "fleet";
} = {}) {
server.use(http.get("/api/catalog/databases", () => HttpResponse.json(
typeof rows === "function" ? rows() : rows,
)));
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(
<QueryClientProvider client={client}>
<DatabaseManagementPage
canManage={canManage}
canManageSecrets={canManageSecrets}
onNavigationStateChange={onNavigationStateChange}
presentation={presentation}
/>
<Toaster duration={Infinity} />
</QueryClientProvider>,
);
return { ...view, client };
}
test("updates database row actions and the open editor when switching EN and IT", async () => {
renderPage({ presentation: "fleet" });
await userEvent.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
const name = await screen.findByLabelText("Database name");
const savedName = (name as HTMLInputElement).value;
try {
act(() => setLocale("it"));
expect(screen.getByLabelText("Nome database")).toHaveValue(savedName);
expect(screen.getByRole("button", { name: "Salva modifiche" })).toBeVisible();
expect(screen.getByRole("button", { name: "Verifica connessione" })).toBeVisible();
act(() => setLocale("en"));
expect(screen.getByLabelText("Database name")).toHaveValue(savedName);
expect(screen.getByRole("button", { name: "Save changes" })).toBeVisible();
} finally { act(() => setLocale("en")); }
});
test("fleet entry opens the database list without workspace preparation or an automatic editor", async () => {
renderPage({ presentation: "fleet" });
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
expect(screen.getByText("Research laboratory")).toBeVisible();
expect(screen.queryByRole("button", { name: "Back to list" })).not.toBeInTheDocument();
expect(screen.queryByText(/Workspace preparation/)).not.toBeInTheDocument();
expect(screen.queryByRole("link", { name: "Open workspace preprocessing" })).not.toBeInTheDocument();
});
test.each(["tables", "relationships"])("fresh entry ignores a previously persisted %s screen", async kind => {
vi.stubEnv("MODE", "production");
localStorage.setItem("thothii:database-management-screen", JSON.stringify({ kind, workspaceId: "psd-clinical" }));
try {
renderPage({ presentation: "fleet" });
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Back to databases" })).not.toBeInTheDocument();
} finally {
vi.unstubAllEnvs();
}
});
function makeSyncRun(
scope: CatalogSyncRun["scope"],
tableIds: string[] = [],
): CatalogSyncRun {
return {
id: `sync-${scope}`,
databaseId: "11111111-1111-4111-8111-111111111111",
scope,
tableIds,
state: "queued",
phase: "queued",
requestedDatabaseVersion: 3,
plannedDiff: null,
confirmationToken: null,
counts: {},
errorCode: null,
errorMessage: null,
cancelRequested: false,
createdAt: "2026-08-27T10:00:00Z",
startedAt: null,
updatedAt: "2026-08-27T10:00:00Z",
finishedAt: null,
heartbeatAt: null,
};
}
function makeDescriptionGenerationRun(
overrides: Partial<DescriptionGenerationRun> = {},
): DescriptionGenerationRun {
return {
id: "77777777-7777-4777-8777-777777777777",
databaseId: "11111111-1111-4111-8111-111111111111",
modelId: "local/qwen",
scope: "selected_columns",
language: "it",
status: "queued",
total: 1,
processed: 0,
generated: 0,
nonGeneratable: 0,
failed: 0,
createdAt: "2026-08-28T10:00:00Z",
startedAt: null,
updatedAt: "2026-08-28T10:00:00Z",
finishedAt: null,
errorSummary: null,
...overrides,
};
}
function makeSensitivityAnalysisRun(
overrides: Partial<SensitivityAnalysisRun> = {},
): SensitivityAnalysisRun {
return {
id: "99999999-9999-4999-8999-999999999999",
databaseId: "11111111-1111-4111-8111-111111111111",
engine: "local",
modelId: null,
policyVersion: "sensitivity-v2",
scope: "selected_columns",
status: "completed",
total: 2,
suggestedSensitive: 1,
suggestedNonSensitive: 1,
unknown: 0,
createdAt: "2026-08-28T11:00:00Z",
startedAt: "2026-08-28T11:00:00Z",
updatedAt: "2026-08-28T11:00:01Z",
finishedAt: "2026-08-28T11:00:01Z",
errorSummary: null,
...overrides,
};
}
function registerCompletedSyncRun(run: CatalogSyncRun) {
const completed: CatalogSyncRun = {
...run,
state: "succeeded",
phase: "completed",
startedAt: "2026-08-27T10:00:00Z",
finishedAt: "2026-08-27T10:00:01Z",
};
server.use(
http.get("/api/catalog/sync-runs/:runId", () => HttpResponse.json(completed)),
http.get("/api/catalog/sync-runs/:runId/events-list", () => HttpResponse.json([])),
http.get("/api/catalog/databases/:databaseId/sync-runs", () => HttpResponse.json([completed])),
);
}
const synchronizationScopes = [
{ scope: "tables", label: "Synchronize tables" },
{ scope: "relationships", label: "Synchronize relationships" },
{ scope: "all", label: "Synchronize all" },
] as const;
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({
scope: "global",
databaseId: null,
tables: 163,
columns: 2_275,
sensitiveColumns: 5,
relationships: 18,
descriptionTargets: 2_438,
describedTargets: 1_828,
descriptionCoverage: 75,
updatedAt: "2026-08-31T09:00:00Z",
})),
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
);
renderPage({ rows: [makeDatabase()], presentation: "fleet" });
const pageHeading = await screen.findByRole("heading", { name: "Database management" });
expect(pageHeading).toBeVisible();
const applicationBar = pageHeading.closest("header");
expect(applicationBar).not.toBeNull();
expect(screen.getByRole("region", { name: "Catalog status" })).toBeVisible();
expect(screen.getByRole("group", { name: "Database management actions" })).toBeVisible();
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: /Catalog status/ })).toBeVisible();
const summary = screen.getByRole("region", { name: "Fleet summary" });
expect(await within(summary).findByText("2,275")).toBeVisible();
expect(within(summary).getByText("75%")).toBeVisible();
const modelSelector = await screen.findByRole("combobox", { name: "Global 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" });
const detailsAction = within(databaseRow).getByRole("button", { name: "View Policlinico San Donato" });
expect(tablesAction.parentElement).toHaveAttribute("data-tooltip", "Tables");
expect(tablesAction).not.toHaveAttribute("title");
expect(detailsAction).toHaveAttribute("data-tooltip", "Details");
const manager = screen.getByRole("main", { name: "Database management" });
await user.click(within(databaseRow).getByRole("button", { name: "Edit Policlinico San Donato" }));
const editDrawer = await screen.findByRole("region", { name: "Edit database form" });
expect(manager).toContainElement(editDrawer);
expect(screen.queryByRole("dialog", { name: "Edit database" })).not.toBeInTheDocument();
await user.click(within(editDrawer).getByRole("button", { name: "Back to list" }));
const restoredDatabaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(restoredDatabaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
const actionPicker = screen.getByRole("combobox", { name: "Batch action" });
expect(within(actionPicker).getByRole("option", { name: /^Synchronize all(?:,|$)/ })).toBeVisible();
expect(within(actionPicker).queryByRole("option", { name: "Synchronize all columns" })).not.toBeInTheDocument();
await user.selectOptions(actionPicker, "generate-missing");
expect(screen.getAllByRole("combobox", { name: "Global LLM model" })).toHaveLength(1);
expect(modelSelector).toHaveValue("local/qwen");
});
test("shows synchronization, description, and sensitive-data 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("Not synchronized")).toBeVisible();
expect(within(configuredRow).getByText(/Descriptions ·/)).toBeVisible();
expect(within(configuredRow).getByText(/Sensitive ·/)).toBeVisible();
const needsConfigurationRow = screen.getByRole("row", { name: /Research laboratory/ });
expect(within(needsConfigurationRow).getByText("Not synchronized")).toBeVisible();
expect(within(needsConfigurationRow).getByText(/Descriptions ·/)).toBeVisible();
const orphanRow = screen.getByRole("row", { name: /Retired workspace/ });
expect(within(orphanRow).getByText("Not synchronized")).toBeVisible();
});
test("shows the configured access type and endpoint in catalog status", async () => {
const direct = makeDatabase();
const rest = makeDatabase({
id: "33333333-3333-4333-8333-333333333333",
workspaceId: "rest-lab",
workspaceName: "REST laboratory",
binding: {
transport: "rest_api",
baseUrl: "https://203.0.113.18/dwh/",
restPath: "/health",
restAuth: "x-api-key",
},
});
const ssh = makeDatabase({
id: "44444444-4444-4444-8444-444444444444",
workspaceId: "ssh-lab",
workspaceName: "SSH laboratory",
binding: {
transport: "ssh_tunnel",
sshHost: "bastion.example.test",
sshPort: 2222,
sshUsername: "operator",
sshTargetHost: "postgres.internal",
sshTargetPort: 5432,
username: "reader",
},
});
renderPage({ rows: [direct, rest, ssh], presentation: "fleet" });
const directRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
expect(within(directRow).getByText("Direct PostgreSQL connection")).toBeVisible();
expect(within(directRow).getByText("db.internal:5432")).toBeVisible();
const restRow = screen.getByRole("row", { name: /REST laboratory/ });
expect(within(restRow).getByText("REST")).toBeVisible();
expect(within(restRow).getByText("203.0.113.18")).toBeVisible();
const sshRow = screen.getByRole("row", { name: /SSH laboratory/ });
expect(within(sshRow).getByText("SSH")).toBeVisible();
expect(within(sshRow).getByText("bastion.example.test:2222")).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("Not synchronized")).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("refreshes the relationship KPI after building generated relationships", async () => {
const user = userEvent.setup();
const generatedRelationship: CatalogRelationship = {
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
databaseId: "11111111-1111-4111-8111-111111111111",
constraintName: null,
sourceTableId: "ffffffff-ffff-4fff-8fff-ffffffffffff",
sourceTableName: "visits",
targetTableId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
targetTableName: "patients",
updateRule: null,
deleteRule: null,
deferrable: false,
initiallyDeferred: false,
columns: [{
position: 1,
sourceColumnId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
sourceColumnName: "patient_id",
targetColumnId: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
targetColumnName: "id",
}],
origin: "generated",
status: "active",
lastSyncedDatabaseVersion: null,
lastSyncedAt: null,
createdAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:00Z",
};
let relationships: CatalogRelationship[] = [];
server.use(
http.get("/api/catalog/metrics", ({ request }) => HttpResponse.json({
scope: new URL(request.url).searchParams.has("databaseId") ? "database" : "global",
databaseId: new URL(request.url).searchParams.get("databaseId"),
tables: 2,
columns: 3,
sensitiveColumns: 0,
relationships: relationships.length,
descriptionTargets: 5,
describedTargets: 0,
descriptionCoverage: 0,
updatedAt: "2026-08-27T10:00:00Z",
})),
http.get(
"/api/catalog/databases/:databaseId/relationships",
() => HttpResponse.json(relationships),
),
http.post(
"/api/catalog/databases/:databaseId/relationships/rebuild-generated",
() => {
relationships = [generatedRelationship];
return HttpResponse.json({ added: 1, alreadyPresent: 0, excluded: 0, ambiguous: 0 });
},
),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
presentation: "fleet",
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("button", {
name: "View relationships for Policlinico San Donato",
}));
const summary = await screen.findByRole("region", { name: "Database summary" });
const relationshipMetric = within(summary).getByText("Relationships").nextElementSibling;
expect(relationshipMetric).toHaveTextContent("0");
await user.selectOptions(
screen.getByRole("combobox", { name: "Relationship action" }),
"rebuild-generated",
);
await user.click(screen.getByRole("button", { name: "Run action" }));
expect(await screen.findByText("Build complete: 1 added, 0 already present, 0 excluded, 0 ambiguous."))
.toBeVisible();
await waitFor(() => expect(relationshipMetric).toHaveTextContent("1"));
});
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("region", { name: "Configure catalog form" });
expect(within(drawer).getByLabelText("Workspace")).toHaveAttribute("readonly");
expect(within(drawer).getByLabelText("Workspace")).toHaveValue("Research laboratory");
});
test("keeps the table back action in the Fleet Ledger breadcrumb while browsing columns", 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",
}));
expect(await screen.findByRole("button", { name: "Back to databases" })).toBeVisible();
expect(screen.getByRole("combobox", { name: "Table action" })).toBeDisabled();
expect(screen.queryByRole("button", { name: "Sync history" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Description history" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Sensitive history" })).not.toBeInTheDocument();
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const tableBackAction = await screen.findByRole("button", { name: "Back to tables" });
expect(tableBackAction).toBeVisible();
expect(screen.getByRole("combobox", { name: "Column action" })).toBeDisabled();
expect(screen.getByRole("navigation", { name: "Database hierarchy" }).parentElement)
.toContainElement(tableBackAction);
await waitFor(() => {
expect(screen.queryByRole("button", { name: "Back to databases" })).not.toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: "Back to tables" }));
expect(await screen.findByRole("button", { name: "Back to databases" })).toBeVisible();
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 = {
...makeSyncRun("all"),
state: "succeeded",
phase: "completed",
startedAt: "2026-08-27T10:00:00Z",
finishedAt: "2026-08-27T10:00:01Z",
};
server.use(
http.get("/api/catalog/databases/:databaseId/sync-runs", () => HttpResponse.json([completed])),
http.get("/api/catalog/sync-runs/:runId", () => HttpResponse.json(completed)),
http.get("/api/catalog/sync-runs/:runId/events-list", () => 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 Policlinico San Donato",
}));
await user.click(screen.getByRole("button", { name: "View schema synchronization logs" }));
const drawer = await screen.findByRole("dialog", { name: "Schema synchronization" });
expect(drawer).toBeVisible();
expect(screen.getByRole("main", { name: "Database management" })).toContainElement(drawer);
expect(await screen.findByRole("heading", { name: "Succeeded" })).toBeVisible();
expect(screen.getByRole("region", { name: "Synchronization history" })).toBeVisible();
});
test("selects the configured metadata-description model by default", async () => {
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [
{ id: "openai/mini", label: "OpenAI Mini" },
{ id: "local/qwen", label: "Local Qwen" },
],
default: "local/qwen",
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
await waitFor(() => expect(selector).toHaveValue("local/qwen"));
});
test("Core and Admin share one canonical selection, including models with the same short ID", async () => {
const models = [{ id: "local/qwen", label: "Local Qwen" }, { id: "remote/qwen", label: "Remote Qwen" }];
server.use(
http.get("/api/catalog/databases", () => HttpResponse.json([makeDatabase()])),
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({ models, default: "local/qwen" })),
http.get("/api/models", () => HttpResponse.json({ models: models.map((m) => ({
provider: m.id.split("/")[0], id: "qwen", name: m.label, reasoning: false,
})) })),
http.get("/api/settings", () => HttpResponse.json({ provider: "local", model: "qwen" })),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><DatabaseManagementPage canManage canManageSecrets /></QueryClientProvider>);
const admin = await screen.findByRole("combobox", { name: "Global LLM model" });
const core = await screen.findByRole("combobox", { name: "Model" });
await waitFor(() => expect(admin).toHaveValue("local/qwen"));
act(() => useSessionStore.getState().setAgentActive(true));
expect(core).toBeDisabled();
expect(admin).toBeDisabled();
act(() => useSessionStore.getState().setAgentActive(false));
act(() => client.setQueryData(["description-generation-runs", 50], [makeDescriptionGenerationRun()]));
await waitFor(() => expect(core).toBeDisabled());
await waitFor(() => expect(admin).toBeDisabled());
act(() => client.setQueryData(["description-generation-runs", 50], []));
await waitFor(() => expect(core).toBeEnabled());
await waitFor(() => expect(admin).toBeEnabled());
await userEvent.selectOptions(admin, "remote/qwen");
await waitFor(() => expect(core).toHaveValue("remote/qwen"));
await userEvent.selectOptions(core, "local/qwen");
await waitFor(() => expect(admin).toHaveValue("local/qwen"));
});
test("keeps both run-history buttons visible beside the metadata-description selector", async () => {
const user = userEvent.setup();
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global 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" });
const descriptionHistoryButton = within(metadataControls).getByRole("button", {
name: "View description generation history",
});
const suggestionHistoryButton = within(metadataControls).getByRole("button", {
name: "View sensitivity analysis history",
});
expect(toolbar).toHaveClass("sm:items-end", "sm:justify-between");
expect(toolbar.firstElementChild).toBe(metadataControls);
expect(metadataControls).toContainElement(selector);
expect(descriptionHistoryButton).toBeVisible();
expect(descriptionHistoryButton).toBeEnabled();
expect(descriptionHistoryButton).toHaveTextContent("View description generation history");
expect(descriptionHistoryButton).toHaveClass("disabled:opacity-70");
expect(suggestionHistoryButton).toBeVisible();
expect(suggestionHistoryButton).toBeEnabled();
expect(suggestionHistoryButton).toHaveTextContent("View sensitivity analysis history");
expect(toolbar.lastElementChild).toBe(actions);
expect(actions).toHaveClass("sm:justify-end");
expect(actions).toContainElement(screen.getByRole("button", { name: "Refresh" }));
expect(within(actions).queryByRole("button", { name: /Add database/i })).not.toBeInTheDocument();
expect(screen.queryByRole("note", {
name: "Metadata generation source data disclosure",
})).not.toBeInTheDocument();
expect(document.getElementById("metadata-generation-source-data-disclosure")).toBeNull();
await user.click(descriptionHistoryButton);
const descriptionDrawer = await screen.findByRole("dialog", { name: "Description generation" });
expect(within(descriptionDrawer).getByText("No description generation runs yet.")).toBeVisible();
await user.click(suggestionHistoryButton);
expect(screen.queryByRole("dialog", { name: "Description generation" })).not.toBeInTheDocument();
const suggestionDrawer = await screen.findByRole("dialog", { name: "Sensitivity analysis history" });
expect(within(suggestionDrawer).getByText("No sensitivity analysis runs yet.")).toBeVisible();
});
test("changes the global model shared with Core", async () => {
const user = userEvent.setup();
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [
{ id: "openai/mini", label: "OpenAI Mini" },
{ id: "local/qwen", label: "Local Qwen" },
],
default: "local/qwen",
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
await waitFor(() => expect(selector).toHaveValue("local/qwen"));
await user.selectOptions(selector, "openai/mini");
expect(selector).toHaveValue("openai/mini");
expect(workspacePreferences.load()).toMatchObject({ provider: "openai", model: "mini" });
});
test("explains application setup when no metadata-description model is configured", async () => {
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [],
default: null,
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
expect(selector).toBeDisabled();
expect(await screen.findByText(
"No metadata-generation LLM model is configured for this installation.",
)).toBeVisible();
});
test("uses a safe disabled setup state when metadata-description models are unavailable", async () => {
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
code: "catalog_unavailable",
message: "internal model registry details",
}, { status: 503 })));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
await waitFor(() => expect(selector).toHaveDisplayValue("Model choices unavailable"));
expect(selector).toBeDisabled();
expect(screen.getByText(
"Global LLM model choices are unavailable.",
)).toBeVisible();
expect(screen.queryByText("internal model registry details")).not.toBeInTheDocument();
});
test.each([null, "missing-model"])(
"disables metadata-description selection when the public response has no usable default (%s)",
async (configuredDefault) => {
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "openai/mini", label: "OpenAI Mini" }],
default: configuredDefault,
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
await waitFor(() => expect(selector).toHaveDisplayValue("No usable model configured"));
expect(selector).toBeDisabled();
expect(screen.getByText(
"No metadata-generation LLM model is configured for this installation.",
)).toBeVisible();
},
);
test("renders only the public metadata-generation model contract", async () => {
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{
id: "approved/model",
label: "Approved model",
provider: "private-provider",
model: "private/upstream-model",
endpoint: "https://private-model.example/v1",
apiKeyEnv: "PRIVATE_MODEL_API_KEY",
credential: "raw-private-credential",
}],
default: "approved/model",
})));
renderPage();
const selector = await screen.findByRole("combobox", { name: "Global LLM model" });
await waitFor(() => expect(selector).toHaveValue("approved/model"));
expect(within(selector).getAllByRole("option").map((option) => option.textContent))
.toEqual(["Approved model"]);
expect(document.body).not.toHaveTextContent(/private-provider|private\/upstream-model/);
expect(document.body).not.toHaveTextContent(/private-model\.example|PRIVATE_MODEL_API_KEY/);
expect(document.body).not.toHaveTextContent("raw-private-credential");
});
test("starts with a full-width list and applies the row action matrix", async () => {
renderPage();
expect(await screen.findByRole("button", { name: "View Policlinico San Donato" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Edit Policlinico San Donato" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Delete Policlinico San Donato" })).toBeEnabled();
expect(screen.getByRole("button", { name: "View Research laboratory" })).toBeEnabled();
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();
expect(screen.getByRole("button", { name: "Delete Retired workspace" })).toBeEnabled();
expect(screen.queryByRole("heading", { name: "Database details" })).not.toBeInTheDocument();
});
test.each(synchronizationScopes)(
"selected database Actions offers and starts $scope synchronization",
async ({ scope, label }) => {
const user = userEvent.setup();
let startBody: unknown;
const run = makeSyncRun(scope);
registerCompletedSyncRun(run);
server.use(http.post("/api/catalog/databases/:databaseId/sync-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(run, { status: 202 });
}));
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: synchronizationScopes[0].label })).toBeEnabled();
for (const option of synchronizationScopes.slice(1)) {
expect(screen.getByRole("menuitem", { name: option.label })).toBeEnabled();
}
await user.click(screen.getByRole("menuitem", { name: label }));
await waitFor(() => expect(startBody).toEqual({ version: 3, scope, tableIds: [] }));
expect(screen.getByText("1 selected")).toBeVisible();
},
);
test("discloses source sampling before starting Generate missing descriptions", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun({ scope: "missing", total: 3 });
let startBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate missing descriptions" }));
expect(screen.getByText("Generate missing descriptions for Policlinico San Donato?")).toBeVisible();
expect(screen.getByRole("note", {
name: "Metadata generation source data disclosure",
})).toHaveTextContent(
"When available, up to five real source rows and five representative values from columns not marked sensitive are sent to the selected provider.",
);
expect(startBody).toBeUndefined();
await user.click(screen.getByRole("button", { name: "Generate missing descriptions" }));
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "missing",
}));
expect(screen.queryByText(/replace existing generated descriptions/i)).not.toBeInTheDocument();
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
});
test("keeps Fleet Generate missing descriptions behind the source-data disclosure", async () => {
const user = userEvent.setup();
let generationStarts = 0;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => {
generationStarts += 1;
return HttpResponse.json(makeDescriptionGenerationRun({ scope: "missing" }), { status: 202 });
}),
);
renderPage({ rows: [makeDatabase()], presentation: "fleet" });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.selectOptions(
screen.getByRole("combobox", { name: "Batch action" }),
"generate-missing",
);
await waitFor(() => expect(screen.getByRole("combobox", {
name: "Global LLM model",
})).toHaveValue("local/qwen"));
await user.click(screen.getByRole("button", { name: "Run action" }));
expect(screen.getByRole("note", {
name: "Metadata generation source data disclosure",
})).toBeVisible();
expect(generationStarts).toBe(0);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.queryByRole("note", {
name: "Metadata generation source data disclosure",
})).not.toBeInTheDocument();
expect(generationStarts).toBe(0);
});
test("copies generated descriptions to every database table and column after explicit confirmation", async () => {
const user = userEvent.setup();
let consolidationBody: unknown;
server.use(
http.post("/api/catalog/databases/:databaseId/descriptions/consolidate", async ({ request }) => {
consolidationBody = await request.json();
return HttpResponse.json({ copied: 7, skipped: 2 });
}),
);
const database = makeDatabase();
const { client } = renderPage({ rows: [database], presentation: "fleet" });
const tablesQuery = ["catalog-tables", database.id] as const;
const firstColumnsQuery = ["catalog-columns", database.id, "table-one"] as const;
const secondColumnsQuery = ["catalog-columns", database.id, "table-two"] as const;
const unrelatedColumnsQuery = ["catalog-columns", "other-database", "table-one"] as const;
for (const queryKey of [
tablesQuery,
firstColumnsQuery,
secondColumnsQuery,
unrelatedColumnsQuery,
]) client.setQueryData(queryKey, []);
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.selectOptions(
screen.getByRole("combobox", { name: "Batch action" }),
"consolidate-descriptions",
);
expect(screen.getByRole("option", { name: "Copy generated description to descriptions" }))
.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Run action" }));
expect(screen.getByText(
"Copy generated descriptions to Description fields in Policlinico San Donato?",
)).toBeVisible();
expect(screen.getByText(/table and column description will replace its current Description value/i))
.toBeVisible();
expect(consolidationBody).toBeUndefined();
await user.click(screen.getByRole("button", { name: "Copy to descriptions" }));
await waitFor(() => expect(consolidationBody).toEqual({ target: "database" }));
expect(await screen.findByText("Copied 7 descriptions; skipped 2")).toBeVisible();
await waitFor(() => {
expect(client.getQueryState(tablesQuery)?.isInvalidated).toBe(true);
expect(client.getQueryState(firstColumnsQuery)?.isInvalidated).toBe(true);
expect(client.getQueryState(secondColumnsQuery)?.isInvalidated).toBe(true);
});
expect(client.getQueryState(unrelatedColumnsQuery)?.isInvalidated).toBe(false);
expect(screen.getByText("1 selected")).toBeVisible();
});
test("confirms generating all descriptions, supports cancel, and sends the database-wide scope", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun({ scope: "all", total: 6 });
let startBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate all descriptions" }));
expect(screen.getByText("Replace generated descriptions for Policlinico San Donato?")).toBeVisible();
expect(screen.getByText(/existing generated descriptions for eligible tables and columns will be replaced/i)).toBeVisible();
expect(screen.getByRole("note", {
name: "Metadata generation source data disclosure",
})).toHaveTextContent(/real source rows.*selected provider/i);
expect(startBody).toBeUndefined();
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.queryByText(/Replace generated descriptions for/)).not.toBeInTheDocument();
expect(startBody).toBeUndefined();
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate all descriptions" }));
await user.click(screen.getByRole("button", { name: "Generate all descriptions" }));
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "all",
}));
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
});
test("keeps the database selected and safely explains when no descriptions are eligible", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => HttpResponse.json({
code: "description_generation_no_eligible_targets",
message: "private provider and catalog detail",
}, { status: 409 })),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate missing descriptions" }));
await user.click(screen.getByRole("button", { name: "Generate missing descriptions" }));
expect(await screen.findByText(
"No eligible catalog tables or columns need description generation.",
)).toBeVisible();
expect(screen.getByText("1 selected")).toBeVisible();
expect(screen.queryByRole("dialog", { name: "Description generation" })).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent("private provider and catalog detail");
});
test.each([
{ scope: "missing" as const, status: "completed" as const },
{ scope: "all" as const, status: "completed_with_errors" as const },
])("refreshes every table and column result after a database-wide $scope run is $status", async ({
scope,
status,
}) => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun({ scope, total: 4 });
const terminalRun = makeDescriptionGenerationRun({
scope,
status,
total: 4,
processed: 4,
generated: 3,
failed: status === "completed_with_errors" ? 1 : 0,
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:02Z",
finishedAt: "2026-08-28T10:00:02Z",
});
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(terminalRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
const { client } = renderPage({ rows: [makeDatabase()] });
const databaseId = queuedRun.databaseId;
const tablesQuery = ["catalog-tables", databaseId] as const;
const firstColumnsQuery = ["catalog-columns", databaseId, "table-one"] as const;
const secondColumnsQuery = ["catalog-columns", databaseId, "table-two"] as const;
const unrelatedTablesQuery = ["catalog-tables", "other-database"] as const;
const unrelatedColumnsQuery = ["catalog-columns", "other-database", "table-one"] as const;
for (const queryKey of [
tablesQuery,
firstColumnsQuery,
secondColumnsQuery,
unrelatedTablesQuery,
unrelatedColumnsQuery,
]) client.setQueryData(queryKey, []);
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", {
name: scope === "all" ? "Generate all descriptions" : "Generate missing descriptions",
}));
await user.click(screen.getByRole("button", {
name: scope === "all" ? "Generate all descriptions" : "Generate missing descriptions",
}));
expect(await screen.findByRole("heading", {
name: status === "completed" ? "Completed" : "Completed with errors",
})).toBeVisible();
await waitFor(() => {
expect(client.getQueryState(tablesQuery)?.isInvalidated).toBe(true);
expect(client.getQueryState(firstColumnsQuery)?.isInvalidated).toBe(true);
expect(client.getQueryState(secondColumnsQuery)?.isInvalidated).toBe(true);
});
expect(client.getQueryState(unrelatedTablesQuery)?.isInvalidated).toBe(false);
expect(client.getQueryState(unrelatedColumnsQuery)?.isInvalidated).toBe(false);
});
test.each([
{
reason: "database.manage is absent",
canManage: false,
rows: [makeDatabase()],
models: [{ id: "local/qwen", label: "Local Qwen" }],
configuredDefault: "local/qwen",
selectedRows: 1,
},
{
reason: "no usable model is configured",
canManage: true,
rows: [makeDatabase()],
models: [],
configuredDefault: null,
selectedRows: 1,
},
{
reason: "the Workspace Database is not configured",
canManage: true,
rows: [unconfigured],
models: [{ id: "local/qwen", label: "Local Qwen" }],
configuredDefault: "local/qwen",
selectedRows: 1,
},
{
reason: "more than one Workspace Database is selected",
canManage: true,
rows: [
makeDatabase(),
makeDatabase({
id: "44444444-4444-4444-8444-444444444444",
workspaceId: "radiology",
workspaceName: "Radiology",
}),
],
models: [{ id: "local/qwen", label: "Local Qwen" }],
configuredDefault: "local/qwen",
selectedRows: 2,
},
])("disables database-wide generation when $reason", async ({
canManage,
rows,
models,
configuredDefault,
selectedRows,
}) => {
const user = userEvent.setup();
server.use(http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models,
default: configuredDefault,
})));
renderPage({ rows, canManage });
for (const database of rows.slice(0, selectedRows)) {
const row = await screen.findByRole("row", { name: new RegExp(database.workspaceName) });
await user.click(within(row).getByRole("checkbox", { name: /toggle row selection/i }));
}
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate all descriptions" }))
.toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("menuitem", { name: "Generate missing descriptions" }))
.toHaveAttribute("aria-disabled", "true");
});
test("disables database-wide generation while a description generation is active", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun({ scope: "missing" });
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json({
...queuedRun,
status: "running",
startedAt: "2026-08-28T10:00:01Z",
})),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase()] });
let databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate missing descriptions" }));
await user.click(screen.getByRole("button", { name: "Generate missing descriptions" }));
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
await user.click(screen.getByRole("button", { name: "Close description generation" }));
databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate all descriptions" }))
.toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("menuitem", { name: "Generate missing descriptions" }))
.toHaveAttribute("aria-disabled", "true");
});
test("selected database Actions confirms and deletes catalog tables for the full selection", async () => {
const user = userEvent.setup();
let cleanupBody: unknown;
const second = makeDatabase({
id: "44444444-4444-4444-8444-444444444444",
workspaceId: "radiology",
workspaceName: "Radiology",
databaseName: "radiology",
});
server.use(http.post("/api/catalog/databases/metadata-cleanup", async ({ request }) => {
cleanupBody = await request.json();
return HttpResponse.json({ tables: 8, columns: 42, relationships: 5 });
}));
renderPage({ rows: [makeDatabase(), second] });
const firstRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
const secondRow = await screen.findByRole("row", { name: /Radiology/ });
await user.click(within(firstRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(secondRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Delete all tables" })).toBeEnabled();
expect(screen.getByRole("menuitem", { name: "Delete all relationships" })).toBeEnabled();
await user.click(screen.getByRole("menuitem", { name: "Delete all tables" }));
expect(screen.getByText("Delete all catalog tables for 2 databases?")).toBeVisible();
expect(screen.getByText(/database configurations and source databases are unchanged/i)).toBeVisible();
expect(cleanupBody).toBeUndefined();
await user.click(within(secondRow).getByRole("checkbox", { name: /toggle row selection/i }));
expect(screen.queryByText(/Delete all catalog tables for/)).not.toBeInTheDocument();
expect(cleanupBody).toBeUndefined();
await user.click(within(secondRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Delete all tables" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Delete catalog tables" })).toHaveFocus());
await user.keyboard("{Escape}");
await waitFor(() => expect(screen.getByRole("button", { name: "Actions" })).toHaveFocus());
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Delete all tables" }));
await user.click(screen.getByRole("button", { name: "Delete catalog tables" }));
await waitFor(() => expect(cleanupBody).toEqual({
databaseIds: [
"11111111-1111-4111-8111-111111111111",
"44444444-4444-4444-8444-444444444444",
],
target: "tables",
}));
expect(screen.getByText("2 selected")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(screen.queryByText("2 selected")).not.toBeInTheDocument();
});
test("presents completed database synchronization steps as success and skips unneeded confirmation", async () => {
const user = userEvent.setup();
const run = {
...makeSyncRun("all"),
counts: { tables: 163, columns: 2_275, relationships: 18 },
};
registerCompletedSyncRun(run);
server.use(http.post("/api/catalog/databases/:databaseId/sync-runs", () => (
HttpResponse.json(run, { status: 202 })
)));
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Synchronize all" }));
expect(await screen.findByRole("heading", { name: "Succeeded" })).toBeVisible();
const steps = screen.getByRole("list", { name: "Synchronization steps" });
expect(steps).toHaveClass("grid-cols-2", "sm:grid-cols-3");
const connected = within(steps).getByRole("listitem", { name: "Connected, completed" });
expect(connected.querySelector("svg")).toHaveClass("text-[oklch(var(--success))]");
expect(within(steps).getByText("Tables read")).toBeVisible();
const confirmation = within(steps).getByRole("listitem", {
name: "Confirmation not required, not required",
});
expect(confirmation.querySelector("svg")).toHaveClass("text-muted-foreground");
const completed = within(steps).getByRole("listitem", { name: "Completed, completed" });
expect(completed.querySelector("svg")).toHaveClass("text-[oklch(var(--success))]");
expect(steps.querySelectorAll(".text-primary")).toHaveLength(0);
});
test("database Overview exposes the database synchronization scopes", async () => {
const user = userEvent.setup();
let startBody: unknown;
const run = makeSyncRun("relationships");
registerCompletedSyncRun(run);
server.use(http.post("/api/catalog/databases/:databaseId/sync-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(run, { status: 202 });
}));
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("button", { name: "Synchronize database schema" }));
expect(await screen.findByRole("menuitem", { name: synchronizationScopes[0].label })).toBeEnabled();
for (const option of synchronizationScopes.slice(1)) {
expect(screen.getByRole("menuitem", { name: option.label })).toBeEnabled();
}
await user.click(screen.getByRole("menuitem", { name: "Synchronize relationships" }));
await waitFor(() => expect(startBody).toEqual({
version: 3,
scope: "relationships",
tableIds: [],
}));
});
test("replaces the list with View and returns focus to the originating action", async () => {
const user = userEvent.setup();
renderPage({ rows: [makeDatabase()] });
const view = await screen.findByRole("button", { name: "View Policlinico San Donato" });
await user.click(view);
expect(screen.getByRole("heading", { name: "Database details" })).toBeVisible();
expect(screen.getByDisplayValue("db.internal")).toHaveAttribute("readonly");
expect(screen.getByRole("button", { name: "Test connection" })).toBeEnabled();
expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Back to list" }));
await waitFor(() => expect(view).toHaveFocus());
expect(screen.getByRole("button", { name: "View Policlinico San Donato" })).toBeVisible();
});
test("configures the catalog for the selected workspace and locks that workspace", async () => {
const user = userEvent.setup();
let postBody: unknown;
const saved = makeDatabase({
...unconfigured,
id: "33333333-3333-4333-8333-333333333333",
configured: true,
version: 1,
binding: { transport: "rest_api", baseUrl: "https://psd.example/api", restPath: "/health", restAuth: "x-api-key" },
});
let rows = [unconfigured];
server.use(
http.post("/api/catalog/databases", async ({ request }) => {
postBody = await request.json();
rows = [saved];
return HttpResponse.json(saved, { status: 201 });
}),
);
renderPage({ rows: () => rows });
await user.click(await screen.findByRole("button", { name: "Configure catalog for Research laboratory" }));
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 catalog configuration" }));
await waitFor(() => expect(postBody).toMatchObject({
workspaceId: "lab",
binding: expect.objectContaining({ transport: "rest_api", baseUrl: "https://psd.example/api" }),
}));
expect(await screen.findByRole("heading", { name: "Edit database" })).toBeVisible();
});
test("has no global database creation path and scopes configuration to the chosen row", async () => {
const user = userEvent.setup();
const second = makeDatabase({
...unconfigured,
workspaceId: "radiology",
workspaceName: "Radiology",
databaseName: "radiology_dwh",
});
renderPage({ rows: [makeDatabase(), unconfigured, second] });
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: "Configure catalog" })).toBeVisible();
const workspace = screen.getByLabelText("Workspace");
expect(workspace).toHaveAttribute("readonly");
expect(workspace).toHaveValue("Radiology");
expect(screen.getByDisplayValue("radiology_dwh")).toBeVisible();
});
test("guards a dirty Edit, disables testing, and reports navigation state", async () => {
const user = userEvent.setup();
const navigation = vi.fn();
const confirm = vi.spyOn(window, "confirm").mockReturnValue(false);
renderPage({ rows: [makeDatabase()], onNavigationStateChange: navigation });
await user.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
const schema = screen.getByLabelText("Schema");
await user.clear(schema);
await user.type(schema, "reporting");
expect(screen.getByRole("button", { name: "Test connection" })).toBeDisabled();
await waitFor(() => expect(navigation).toHaveBeenLastCalledWith({ dirty: true, busy: false }));
await user.click(screen.getByRole("button", { name: "Back to list" }));
expect(confirm).toHaveBeenCalledWith("Discard unsaved database changes?");
expect(screen.getByRole("heading", { name: "Edit database" })).toBeVisible();
confirm.mockReturnValue(true);
await user.click(screen.getByRole("button", { name: "Back to list" }));
expect(await screen.findByRole("button", { name: "Edit Policlinico San Donato" })).toBeVisible();
});
test("uses an in-page destructive form and returns the YAML workspace to Not configured", async () => {
const user = userEvent.setup();
let row = makeDatabase();
let deleteCalled = false;
let deleteVersion: string | null = null;
server.use(
http.delete("/api/catalog/databases/:id", ({ request }) => {
deleteCalled = true;
deleteVersion = new URL(request.url).searchParams.get("version");
row = {
...row,
id: undefined,
configured: false,
version: 0,
updatedAt: "",
connectionStatus: "untested",
secrets: { ...noSecrets },
};
return new HttpResponse(null, { status: 204 });
}),
);
renderPage({ rows: () => [row] });
await user.click(await screen.findByRole("button", { name: "Delete Policlinico San Donato" }));
expect(screen.getByRole("heading", { name: "Delete database" })).toBeVisible();
expect(screen.getByText("This removes the local database configuration.")).toBeVisible();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Delete database" }));
await waitFor(() => expect(deleteCalled).toBe(true));
expect(deleteVersion).toBe("3");
expect(await screen.findByRole("button", { name: "Configure catalog for Policlinico San Donato" })).toBeEnabled();
expect(screen.queryByRole("button", { name: "Delete Policlinico San Donato" })).not.toBeInTheDocument();
});
test("tests only the persisted version and updates the visible connection status", async () => {
const user = userEvent.setup();
let testBody: unknown;
let row = makeDatabase();
server.use(http.post("/api/catalog/databases/:id/test", async ({ request }) => {
testBody = await request.json();
row = makeDatabase({ version: 4, connectionStatus: "reachable", testedVersion: 4 });
const testResponse: Partial<CatalogDatabase> = { ...row };
delete testResponse.workspaceName;
return HttpResponse.json(testResponse);
}));
renderPage({ rows: () => [row] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("button", { name: "Test connection" }));
await waitFor(() => expect(testBody).toEqual({ version: 3 }));
const resultDialog = await screen.findByRole("dialog", { name: "Connection test successful" });
expect(resultDialog).toBeVisible();
expect(within(resultDialog).getByText("Policlinico San Donato is reachable.")).toBeVisible();
await user.click(within(resultDialog).getByRole("button", { name: "Done" }));
expect(within(screen.getByRole("region", { name: "Database details form" })).getByText("Reachable")).toBeVisible();
});
test("reports an unsuccessful selected connection test as a failure", async () => {
const user = userEvent.setup();
const failed = makeDatabase({
connectionStatus: "failed",
testedVersion: 3,
lastErrorMessage: "Password authentication failed",
});
const testResponse: Partial<CatalogDatabase> = { ...failed };
delete testResponse.workspaceName;
server.use(http.post("/api/catalog/databases/:id/test", () => HttpResponse.json(testResponse)));
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Test connections" }));
const resultDialog = await screen.findByRole("dialog", { name: "Connection test failed" });
expect(resultDialog).toBeVisible();
expect(within(resultDialog).getByText(/Policlinico San Donato: Password authentication failed/)).toBeVisible();
expect(screen.queryByText("1 connection tested")).not.toBeInTheDocument();
});
test("keeps Fleet synchronization available after an informative connection failure", async () => {
const user = userEvent.setup();
renderPage({
rows: [makeDatabase({
connectionStatus: "failed",
testedVersion: 3,
lastErrorCode: "connector_unavailable",
lastErrorMessage: "Password authentication failed",
})],
presentation: "fleet",
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
const actionPicker = screen.getByRole("combobox", { name: "Batch action" });
await user.selectOptions(actionPicker, "sync-all");
expect(within(actionPicker).getByRole("option", { name: "Synchronize all" })).toBeVisible();
expect(screen.getByRole("button", { name: "Run action" })).toBeEnabled();
});
test("reports a successful selected connection test as a success", async () => {
const user = userEvent.setup();
const reachable = makeDatabase({
connectionStatus: "reachable",
testedVersion: 3,
});
const testResponse: Partial<CatalogDatabase> = { ...reachable };
delete testResponse.workspaceName;
server.use(http.post("/api/catalog/databases/:id/test", () => HttpResponse.json(testResponse)));
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Test connections" }));
const resultDialog = await screen.findByRole("dialog", { name: "Connection test successful" });
expect(resultDialog).toBeVisible();
expect(within(resultDialog).getByText("Policlinico San Donato is reachable.")).toBeVisible();
});
test("retains a stale draft and requires an explicit reload", async () => {
const user = userEvent.setup();
server.use(
http.patch("/api/catalog/databases/:id", () => HttpResponse.json({
code: "database_stale",
message: "The database changed",
}, { status: 409 })),
);
renderPage({ rows: [makeDatabase()] });
await user.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
const schema = screen.getByLabelText("Schema");
await user.clear(schema);
await user.type(schema, "draft_schema");
await user.click(screen.getByRole("button", { name: "Save changes" }));
expect(await screen.findByText("A newer database configuration is available.")).toBeVisible();
expect(schema).toHaveValue("draft_schema");
expect(screen.getByRole("button", { name: "Save changes" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Keep editing" }));
expect(screen.getByText("Reload the latest values before this configuration can be changed.")).toBeVisible();
expect(schema).toHaveValue("draft_schema");
});
test("marks an open form stale when a background refetch advances the catalog version", async () => {
const user = userEvent.setup();
const { client } = renderPage({ rows: [makeDatabase()] });
await user.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
act(() => {
client.setQueryData(["catalog-databases"], [makeDatabase({ version: 4, schema: "server_schema" })]);
});
expect(await screen.findByText("A newer database configuration is available.")).toBeVisible();
expect(screen.getByLabelText("Schema")).toHaveValue("datawarehouse");
expect(screen.getByRole("button", { name: "Save changes" })).toBeDisabled();
});
test("does not submit credentials hidden by a transport or authentication change", async () => {
const user = userEvent.setup();
let putCalled = false;
let patchBody: Record<string, unknown> | undefined;
let row = makeDatabase();
server.use(
http.patch("/api/catalog/databases/:id", async ({ request }) => {
patchBody = await request.json() as Record<string, unknown>;
row = makeDatabase({
version: 4,
binding: { transport: "rest_api", baseUrl: "https://psd.example/api", restPath: "/health", restAuth: "none" },
});
return HttpResponse.json(row);
}),
http.put("/api/catalog/databases/:id/secrets", () => {
putCalled = true;
return HttpResponse.json(row);
}),
);
renderPage({ rows: () => [row] });
await user.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
await user.type(screen.getByLabelText("Password"), "must-not-be-sent");
await user.selectOptions(screen.getByLabelText("Transport"), "rest_api");
await user.type(screen.getByLabelText("Base URL"), "https://psd.example/api");
await user.selectOptions(screen.getByLabelText("Authentication"), "none");
await user.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(patchBody).toBeDefined());
expect(putCalled).toBe(false);
expect(patchBody).toMatchObject({
version: 3,
binding: {
transport: "rest_api",
baseUrl: "https://psd.example/api",
restPath: "/health",
restAuth: "none",
},
});
expect((patchBody?.binding as Record<string, unknown>).host).toBeUndefined();
});
test("preserves typed secrets and offers a retry when the configuration save is only partial", async () => {
const user = userEvent.setup();
let putCalls = 0;
let row = makeDatabase();
server.use(
http.put("/api/catalog/databases/:id/secrets", async ({ request }) => {
putCalls += 1;
const body = await request.json();
expect(body).toEqual({ version: 3, values: { password: "transient-secret" } });
if (putCalls === 1) {
return HttpResponse.json({
code: "catalog_unavailable",
message: "Secret store unavailable",
}, { status: 503 });
}
row = makeDatabase({ version: 4, secrets: { ...noSecrets, password: true } });
return HttpResponse.json(row);
}),
);
renderPage({ rows: () => [row] });
await user.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
await user.type(screen.getByLabelText("Password"), "transient-secret");
await user.click(screen.getByRole("button", { name: "Save changes" }));
expect(await screen.findByText("Database configuration saved; secret update could not be confirmed.")).toBeVisible();
expect(screen.getByLabelText("Password")).toHaveValue("transient-secret");
expect(screen.getByRole("button", { name: "Retry secrets" })).toBeEnabled();
await user.clear(screen.getByLabelText("Schema"));
await user.type(screen.getByLabelText("Schema"), "reporting_after_retry");
await user.click(screen.getByRole("button", { name: "Retry secrets" }));
await waitFor(() => expect(putCalls).toBe(2));
expect(screen.queryByText("Database configuration saved; secret update could not be confirmed.")).not.toBeInTheDocument();
expect(screen.getByLabelText("Schema")).toHaveValue("reporting_after_retry");
expect(screen.getByRole("button", { name: "Save changes" })).toBeEnabled();
});
test("shows secret status without exposing or enabling values when permission is absent", async () => {
const user = userEvent.setup();
renderPage({
rows: [makeDatabase({ secrets: { ...noSecrets, password: true } })],
canManageSecrets: false,
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
expect(screen.getByLabelText("Password")).toHaveValue("Configured");
expect(screen.getByLabelText("Password")).toHaveAttribute("readonly");
await user.click(screen.getByRole("button", { name: "Back to list" }));
await user.click(screen.getByRole("button", { name: "Edit Policlinico San Donato" }));
expect(await screen.findByRole("heading", { name: "Edit database" })).toBeVisible();
expect(screen.getByLabelText("Password")).toBeDisabled();
expect(screen.getByText(/requires the workspace\.secrets\.manage permission/i)).toBeVisible();
});
const patientsTable: CatalogTable = {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
databaseId: "11111111-1111-4111-8111-111111111111",
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",
};
const patientIdColumn: CatalogColumn = {
id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
tableId: patientsTable.id,
name: "id",
ordinalPosition: 1,
dataType: "bigint",
isNullable: false,
defaultExpression: null,
primaryKeyPosition: 1,
isPrimaryKey: true,
isForeignKey: false,
foreignKeyCount: 0,
sourceComment: "Patient identifier",
description: null,
generatedDescription: null,
sensitive: false,
lastSyncedDatabaseVersion: 3,
lastSyncedAt: "2026-08-27T10:00:00Z",
version: 1,
createdAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:00Z",
};
test("keeps column synchronization available when the latest connection test failed", 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([patientIdColumn])),
);
renderPage({
rows: [makeDatabase({
connectionStatus: "failed",
testedVersion: 3,
lastErrorCode: "connector_unavailable",
lastErrorMessage: "The database connector could not be reached or authenticated.",
})],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const rows = await screen.findAllByRole("row", { name: /Patient identifier/ });
const selectableRow = rows.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
expect(selectableRow).toBeDefined();
await user.click(within(selectableRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Synchronize columns for this table" }))
.not.toHaveAttribute("aria-disabled", "true");
});
test("saves a manual sensitive flag change from the Fleet columns toolbar", async () => {
const user = userEvent.setup();
const nameColumn: CatalogColumn = {
...patientIdColumn,
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
name: "name",
ordinalPosition: 2,
primaryKeyPosition: null,
isPrimaryKey: false,
sourceComment: "Patient name",
};
const columns = [patientIdColumn, nameColumn];
const patches: Array<{ columnId: string; body: Record<string, unknown> }> = [];
let requestInFlight = false;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get(
"/api/catalog/databases/:databaseId/tables/:tableId/columns",
() => HttpResponse.json(columns),
),
http.patch(
"/api/catalog/databases/:databaseId/tables/:tableId/columns/:columnId",
async ({ params, request }) => {
if (requestInFlight) {
return HttpResponse.json({
code: "catalog_operation_active",
message: "Another catalog operation is in progress.",
}, { status: 409 });
}
requestInFlight = true;
const body = await request.json() as Record<string, unknown>;
await new Promise((resolve) => window.setTimeout(resolve, 20));
patches.push({ columnId: String(params.columnId), body });
requestInFlight = false;
const current = columns.find((column) => column.id === params.columnId)!;
return HttpResponse.json({ ...current, ...body, version: current.version + 1 });
},
),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
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",
}));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
expect(screen.queryByRole("button", { name: "Save sensitive fields" })).not.toBeInTheDocument();
await user.click(await screen.findByRole("checkbox", { name: "Sensitive data for id" }));
await user.click(screen.getByRole("checkbox", { name: "Sensitive data for name" }));
const saveButton = screen.getByRole("button", { name: "Save sensitive fields" });
expect(saveButton).toBeVisible();
await user.click(saveButton);
await waitFor(() => expect(patches).toHaveLength(2));
expect(patches).toEqual(expect.arrayContaining([
expect.objectContaining({
columnId: patientIdColumn.id,
body: expect.objectContaining({ version: patientIdColumn.version, sensitive: true }),
}),
expect.objectContaining({
columnId: nameColumn.id,
body: expect.objectContaining({ version: nameColumn.version, sensitive: true }),
}),
]));
expect(await screen.findByText("Sensitive fields saved")).toBeVisible();
await waitFor(() => {
expect(screen.queryByRole("button", { name: "Save sensitive fields" })).not.toBeInTheDocument();
});
});
test("shows a read-only sensitivity reason in the Fleet column metadata matrix", async () => {
const user = userEvent.setup();
const sensitiveColumn: CatalogColumn = {
...patientIdColumn,
sensitive: true,
sensitivityReason: "Local assessment matched content rule pii.email.",
};
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get(
"/api/catalog/databases/:databaseId/tables/:tableId/columns",
() => HttpResponse.json([sensitiveColumn]),
),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
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",
}));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
await user.click(await screen.findByRole("button", { name: "Edit metadata for id" }));
const drawer = await screen.findByRole("dialog", { name: "Review column description" });
const matrix = within(drawer).getByRole("group", { name: "Column metadata fields" });
expect(matrix).toHaveClass("sm:grid-cols-2");
expect(within(matrix).getByLabelText("Source comment")).toHaveAttribute("readonly");
expect(within(matrix).getByLabelText("Description")).not.toHaveAttribute("readonly");
expect(within(matrix).getByLabelText("Generated description")).not.toHaveAttribute("readonly");
expect(within(matrix).getByLabelText("Sensitive motivation")).toHaveValue(
"Local assessment matched content rule pii.email.",
);
expect(within(matrix).getByLabelText("Sensitive motivation")).toHaveAttribute("readonly");
});
test("filters catalog tables as the operator types", async () => {
const user = userEvent.setup();
const mediciTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "bridge_medici",
sourceComment: "Doctors participating in clinical care",
};
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([
patientsTable,
mediciTable,
])),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tablesRegion = await screen.findByRole("region", { name: "Tables for Policlinico San Donato" });
expect(await screen.findByRole("button", { name: "Edit description for patients" })).toBeVisible();
expect(screen.getByRole("button", { name: "Edit description for bridge_medici" })).toBeVisible();
await user.type(screen.getByRole("textbox", { name: "Search tables" }), "medici");
expect(screen.getByRole("button", { name: "Edit description for bridge_medici" })).toBeVisible();
expect(await within(tablesRegion).findByText("1 of 2")).toBeVisible();
await waitFor(() => {
expect(screen.queryByRole("button", { name: "Edit description for patients" })).not.toBeInTheDocument();
});
});
test("selected table Actions expose table commands and send synchronization ids", async () => {
const user = userEvent.setup();
let startBody: unknown;
const run = makeSyncRun("columns", [patientsTable.id]);
registerCompletedSyncRun(run);
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.post("/api/catalog/databases/:databaseId/sync-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(run, { status: 202 });
}),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
const synchronizeColumns = await screen.findByRole("menuitem", { name: "Synchronize columns for selected tables" });
expect(synchronizeColumns).toBeEnabled();
expect(screen.getByRole("menuitem", { name: "Synchronize database tables" })).toBeEnabled();
expect(screen.getByRole("menuitem", { name: "Remove synchronized columns" })).toBeEnabled();
expect(screen.queryByRole("menuitem", { name: "Delete all relationships" })).not.toBeInTheDocument();
await user.click(synchronizeColumns);
await waitFor(() => expect(startBody).toEqual({
version: 3,
scope: "columns",
tableIds: [patientsTable.id],
}));
await waitFor(() => expect(screen.queryByText("1 selected")).not.toBeInTheDocument());
});
test("starts description generation for multiple selected tables", async () => {
const user = userEvent.setup();
const visitsTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "visits",
};
const queuedRun = makeDescriptionGenerationRun({
scope: "selected_tables",
total: 2,
});
let startBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([
patientsTable,
visitsTable,
])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(within(await screen.findByRole("row", { name: /patients/ })).getByRole("checkbox"));
await user.click(within(await screen.findByRole("row", { name: /visits/ })).getByRole("checkbox"));
await user.click(screen.getByRole("button", { name: "Actions" }));
const generate = await screen.findByRole("menuitem", { name: "Generate table description" });
expect(generate).toBeEnabled();
expect(screen.getByRole("menuitem", {
name: "Copy generated descriptions to Description",
})).toBeEnabled();
await user.click(generate);
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "selected_tables",
targetIds: [patientsTable.id, visitsTable.id],
}));
expect(await screen.findByRole("dialog", {
name: "Description generation",
})).toBeVisible();
expect(await screen.findByText("Description generation started for 2 tables")).toBeVisible();
expect(screen.getByText("2 selected")).toBeVisible();
});
test("generates descriptions for every column in selected tables", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun({ scope: "selected_columns", total: 1 });
let startBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({ models: [{ id: "local/qwen", label: "Local Qwen" }], default: "local/qwen" })),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get(`/api/catalog/databases/:databaseId/tables/${patientsTable.id}/columns`, () => HttpResponse.json([patientIdColumn])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(within(await screen.findByRole("row", { name: /patients/ })).getByRole("checkbox"));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate descriptions for all columns" }));
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "selected_columns",
targetIds: [patientIdColumn.id],
}));
expect(await screen.findByText("Column description generation started for 1 column")).toBeVisible();
});
test("observes an active run from another browser and reopens a terminal run from history", async () => {
const user = userEvent.setup();
const activeRun = makeDescriptionGenerationRun({
status: "running",
startedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const completedRun = makeDescriptionGenerationRun({
id: "88888888-8888-4888-8888-888888888888",
scope: "missing",
status: "completed",
processed: 1,
generated: 1,
createdAt: "2026-08-28T09:00:00Z",
startedAt: "2026-08-28T09:00:01Z",
updatedAt: "2026-08-28T09:00:02Z",
finishedAt: "2026-08-28T09:00:02Z",
});
let requestedLimit: string | null = null;
server.use(
http.get("/api/catalog/description-generation-runs", ({ request }) => {
requestedLimit = new URL(request.url).searchParams.get("limit");
return HttpResponse.json([activeRun, completedRun]);
}),
http.get("/api/catalog/description-generation-runs/:runId", ({ params }) => (
HttpResponse.json(params.runId === completedRun.id ? completedRun : activeRun)
)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage();
const observe = await screen.findByRole("button", {
name: "View description generation history",
});
expect(observe).toHaveTextContent("View description generation history");
expect(requestedLimit).toBe("50");
await user.click(observe);
const drawer = await screen.findByRole("dialog", { name: "Description generation" });
expect(within(drawer).getByRole("heading", { name: "Running" })).toBeVisible();
const history = within(drawer).getByRole("region", { name: "Description generation history" });
await user.click(within(history).getByRole("button", { name: /Completed.*missing/i }));
expect(await within(drawer).findByRole("heading", { name: "Completed" })).toBeVisible();
});
test("keeps the sensitivity analysis history label stable while showing active status separately", async () => {
const runningRun = makeSensitivityAnalysisRun({
status: "running",
finishedAt: null,
updatedAt: new Date().toISOString(),
});
server.use(http.get(
"/api/catalog/sensitive-data-suggestion-runs",
() => HttpResponse.json([runningRun]),
));
renderPage();
const historyButton = await screen.findByRole("button", {
name: "View sensitivity analysis history",
});
expect(historyButton).toHaveTextContent("View sensitivity analysis history");
await waitFor(() => expect(historyButton).toHaveAttribute(
"title",
"Sensitivity analysis is active",
));
expect(historyButton.querySelector("[aria-hidden='true'].bg-primary")).not.toBeNull();
});
test.each([
"completed",
"completed_with_errors",
"failed",
"cancelled",
"interrupted",
] satisfies DescriptionGenerationRun["status"][])(
"invalidates Catalog Tables and Catalog Columns for terminal status %s",
async (status) => {
const user = userEvent.setup();
const terminalRun = makeDescriptionGenerationRun({
status,
finishedAt: "2026-08-28T10:00:02Z",
});
server.use(
http.get("/api/catalog/description-generation-runs", () => HttpResponse.json([terminalRun])),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(terminalRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
const { client } = renderPage();
const tableKey = ["catalog-tables", terminalRun.databaseId] as const;
const columnKey = ["catalog-columns", terminalRun.databaseId, "table-id"] as const;
const unrelatedColumnKey = ["catalog-columns", "other-database", "table-id"] as const;
client.setQueryData(tableKey, []);
client.setQueryData(columnKey, []);
client.setQueryData(unrelatedColumnKey, []);
await user.click(await screen.findByRole("button", {
name: "View description generation history",
}));
await waitFor(() => expect(client.getQueryState(tableKey)?.isInvalidated).toBe(true));
expect(client.getQueryState(columnKey)?.isInvalidated).toBe(true);
expect(client.getQueryState(unrelatedColumnKey)?.isInvalidated).toBe(false);
},
);
test("keeps selected tables when description generation cannot start", async () => {
const user = userEvent.setup();
const visitsTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "visits",
};
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([
patientsTable,
visitsTable,
])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => HttpResponse.json({
code: "description_generation_run_active",
message: "private provider detail",
}, { status: 409 })),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(within(await screen.findByRole("row", { name: /patients/ })).getByRole("checkbox"));
await user.click(within(await screen.findByRole("row", { name: /visits/ })).getByRole("checkbox"));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate table description" }));
expect(await screen.findByText("A description generation run is already active.")).toBeVisible();
expect(screen.getByText("2 selected")).toBeVisible();
expect(screen.queryByRole("dialog", { name: "Description generation" })).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent("private provider detail");
});
test("refreshes Catalog Tables and Catalog Columns after table generation completes", async () => {
const user = userEvent.setup();
const visitsTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "visits",
};
const queuedRun = makeDescriptionGenerationRun({
scope: "selected_tables",
total: 2,
});
const completedRun = makeDescriptionGenerationRun({
scope: "selected_tables",
status: "completed",
total: 2,
processed: 2,
generated: 2,
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:02Z",
finishedAt: "2026-08-28T10:00:02Z",
});
let tables = [patientsTable, visitsTable];
let tableReads = 0;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => {
tableReads += 1;
return HttpResponse.json(tables);
}),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => {
tables = tables.map((table) => ({
...table,
generatedDescription: `Generated ${table.name}`,
version: table.version + 1,
}));
return HttpResponse.json(completedRun);
}),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
const { client } = renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const unrelatedTableQuery = ["catalog-tables", "other-database"] as const;
const columnsQuery = [
"catalog-columns",
patientsTable.databaseId,
patientsTable.id,
] as const;
client.setQueryData(unrelatedTableQuery, []);
client.setQueryData(columnsQuery, []);
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(within(await screen.findByRole("row", { name: /patients/ })).getByRole("checkbox"));
await user.click(within(await screen.findByRole("row", { name: /visits/ })).getByRole("checkbox"));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate table description" }));
expect(await screen.findByRole("heading", { name: "Completed" })).toBeVisible();
await waitFor(() => expect(tableReads).toBe(2));
expect(within(await screen.findByRole("row", { name: /patients/ }))
.getByText("Generated patients")).toBeVisible();
expect(client.getQueryState(unrelatedTableQuery)?.isInvalidated).toBe(false);
expect(client.getQueryState(columnsQuery)?.isInvalidated).toBe(true);
});
test("moves selected generated table descriptions and reports copied and skipped counts", async () => {
const user = userEvent.setup();
const visitsTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "visits",
description: "Keep curated visits",
generatedDescription: null,
};
let tables = [
{ ...patientsTable, description: "Old patients", generatedDescription: "Generated patients" },
visitsTable,
];
let consolidationBody: unknown;
let tableReads = 0;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => {
tableReads += 1;
return HttpResponse.json(tables);
}),
http.post("/api/catalog/databases/:databaseId/descriptions/consolidate", async ({ request }) => {
consolidationBody = await request.json();
tables = tables.map((table) => table.id === patientsTable.id
? { ...table, description: table.generatedDescription, version: table.version + 1 }
: table);
return HttpResponse.json({ copied: 1, skipped: 1 });
}),
);
const { client } = renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const unrelatedColumnQuery = [
"catalog-columns",
patientsTable.databaseId,
visitsTable.id,
] as const;
client.setQueryData(unrelatedColumnQuery, []);
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const patientsRow = await screen.findByRole("row", { name: /patients/ });
const visitsRow = await screen.findByRole("row", { name: /visits/ });
await user.click(within(patientsRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(visitsRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", {
name: "Copy generated descriptions to Description",
}));
await waitFor(() => expect(consolidationBody).toEqual({
target: "tables",
targetIds: [patientsTable.id, visitsTable.id],
}));
expect(await screen.findByText("Copied 1 description; skipped 1")).toBeVisible();
await waitFor(() => expect(tableReads).toBe(2));
expect(within(await screen.findByRole("row", { name: /patients/ }))
.getAllByText("Generated patients")).toHaveLength(2);
expect(within(await screen.findByRole("row", { name: /visits/ }))
.getByText("Keep curated visits")).toBeVisible();
expect(screen.queryByText("2 selected")).not.toBeInTheDocument();
expect(client.getQueryState(unrelatedColumnQuery)?.isInvalidated).toBe(false);
});
test("selects columns, moves generated descriptions, and refreshes only the affected column query", async () => {
const user = userEvent.setup();
const idColumn: CatalogColumn = {
id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
tableId: patientsTable.id,
name: "id",
ordinalPosition: 1,
dataType: "bigint",
isNullable: false,
defaultExpression: null,
primaryKeyPosition: 1,
isPrimaryKey: true,
isForeignKey: false,
foreignKeyCount: 0,
sourceComment: "Patient identifier",
description: "Old identifier",
generatedDescription: "Generated identifier",
sensitive: false,
lastSyncedDatabaseVersion: 3,
lastSyncedAt: "2026-08-27T10:00:00Z",
version: 1,
createdAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:00Z",
};
const nameColumn: CatalogColumn = {
...idColumn,
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
name: "name",
ordinalPosition: 2,
dataType: "text",
primaryKeyPosition: null,
isPrimaryKey: false,
description: "Keep curated patient name",
generatedDescription: "",
};
let columns = [idColumn, nameColumn];
let consolidationBody: unknown;
let columnReads = 0;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => {
columnReads += 1;
return HttpResponse.json(columns);
}),
http.post("/api/catalog/databases/:databaseId/descriptions/consolidate", async ({ request }) => {
consolidationBody = await request.json();
columns = columns.map((column) => column.id === idColumn.id
? { ...column, description: column.generatedDescription, version: column.version + 1 }
: column);
return HttpResponse.json({ copied: 1, skipped: 1 });
}),
);
const { client } = renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const tableQuery = ["catalog-tables", patientsTable.databaseId] as const;
const selectableRow = async (name: RegExp) => {
const rows = await screen.findAllByRole("row", { name });
const row = rows.find((candidate) => within(candidate).queryByRole("checkbox", { name: /toggle row selection/i }));
expect(row).toBeDefined();
return row!;
};
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const idRow = await selectableRow(/Patient identifier/);
const nameRow = await selectableRow(/Keep curated patient name/);
await user.click(within(idRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(nameRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Synchronize columns for this table" })).toBeEnabled();
await user.click(await screen.findByRole("menuitem", {
name: "Copy generated descriptions to Description",
}));
await waitFor(() => expect(consolidationBody).toEqual({
target: "columns",
targetIds: [idColumn.id, nameColumn.id],
}));
expect(await screen.findByText("Copied 1 description; skipped 1")).toBeVisible();
await waitFor(() => expect(columnReads).toBe(2));
expect(within(await selectableRow(/Patient identifier/))
.getAllByText("Generated identifier")).toHaveLength(2);
expect(within(await selectableRow(/Keep curated patient name/))
.getByText("Keep curated patient name")).toBeVisible();
expect(screen.queryByText("2 selected")).not.toBeInTheDocument();
expect(client.getQueryState(tableQuery)?.isInvalidated).toBe(false);
});
test("starts one selected column with the configured default model", async () => {
const user = userEvent.setup();
let startBody: unknown;
const queuedRun = makeDescriptionGenerationRun();
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [
{ id: "openai/mini", label: "OpenAI Mini" },
{ id: "local/qwen", label: "Local Qwen" },
],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([patientIdColumn])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
expect(screen.queryByRole("button", { name: "Sync columns" })).not.toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: /Select/i })).toBeVisible();
expect(await screen.findByRole("checkbox", { name: "Sensitive data for id" })).toHaveAttribute(
"title",
"Mark this column as sensitive; this does not select it for actions.",
);
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
expect(columnRow).toBeDefined();
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "selected_columns",
targetIds: [patientIdColumn.id],
}));
const drawer = await screen.findByRole("dialog", { name: "Description generation" });
expect(drawer).toBeVisible();
expect(within(drawer).getByRole("heading", { name: "Queued" })).toBeVisible();
expect(await screen.findByText("Description generation started for 1 column")).toBeVisible();
});
test("shows database sensitivity analysis only for a selection and rejects multiple databases clearly", async () => {
const user = userEvent.setup();
let suggestionCalls = 0;
const radiology = makeDatabase({
id: "44444444-4444-4444-8444-444444444444",
workspaceId: "radiology",
workspaceName: "Radiology",
});
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", () => {
suggestionCalls += 1;
return HttpResponse.json({ suggestions: [] });
}),
);
renderPage({ rows: [makeDatabase(), radiology] });
expect(screen.queryByRole("button", { name: "Analyze sensitive fields" })).not.toBeInTheDocument();
const psdRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
const radiologyRow = await screen.findByRole("row", { name: /Radiology/ });
await user.click(within(psdRow).getByRole("checkbox", { name: /toggle row selection/i }));
expect(screen.getByRole("button", { name: "Analyze sensitive fields" })).toBeVisible();
await user.click(within(radiologyRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
expect(await screen.findByText("Sensitivity analysis can run for only one database at a time. Select one database and try again.")).toBeVisible();
expect(suggestionCalls).toBe(0);
});
test("opens sensitivity analysis progress while the assessment request is still running", async () => {
const user = userEvent.setup();
let analysisStarted = false;
let releaseAnalysis!: () => void;
const analysisGate = new Promise<void>((resolve) => {
releaseAnalysis = resolve;
});
const runningRun = makeSensitivityAnalysisRun({
scope: "all",
status: "running",
total: 2,
suggestedSensitive: 0,
suggestedNonSensitive: 0,
unknown: 2,
finishedAt: null,
});
const completedRun = makeSensitivityAnalysisRun({ scope: "all", total: 2 });
let activityEvents = [{
runId: runningRun.id,
sequence: 1,
level: "info",
message: "Scanning source columns",
createdAt: "2026-08-28T11:00:00Z",
}];
server.use(
http.get("/api/catalog/sensitive-data-suggestion-runs", () => (
HttpResponse.json(analysisStarted ? [runningRun] : [])
)),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId", () => (
HttpResponse.json(runningRun)
)),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId/events-list", () => (
HttpResponse.json(activityEvents)
)),
http.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", async () => {
analysisStarted = true;
await analysisGate;
return HttpResponse.json({ run: completedRun, suggestions: [] });
}),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
const launch = user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
await waitFor(() => expect(analysisStarted).toBe(true));
const progress = await screen.findByRole("dialog", { name: "Sensitivity analysis history" });
expect(within(progress).getByRole("heading", { name: "Starting sensitivity analysis" })).toBeVisible();
expect(within(progress).getByRole("progressbar", { name: "Sensitivity analysis is starting" }))
.toBeVisible();
expect(within(progress).getByText("Preparing sensitivity analysis…")).toBeVisible();
expect(await within(progress).findByRole("heading", { name: "Running" }, { timeout: 2_000 })).toBeVisible();
expect(await within(progress).findByText("Scanning source columns")).toBeVisible();
const eventLog = within(progress).getByRole("log", { name: "Sensitivity analysis events" });
Object.defineProperties(eventLog, {
clientHeight: { configurable: true, value: 100 },
scrollHeight: { configurable: true, get: () => activityEvents.length * 100 },
scrollTop: { configurable: true, value: 0, writable: true },
});
activityEvents = [...activityEvents, {
runId: runningRun.id,
sequence: 2,
level: "info",
message: "Running local entity detection",
createdAt: "2026-08-28T11:00:01Z",
}];
expect(await within(progress).findByText("Running local entity detection", {}, { timeout: 2_000 })).toBeVisible();
await waitFor(() => expect(eventLog.scrollTop).toBe(eventLog.scrollHeight));
eventLog.scrollTop = 0;
fireEvent.scroll(eventLog);
const jumpToLatest = await within(progress).findByRole("button", { name: "Jump to latest" });
activityEvents = [...activityEvents, {
runId: runningRun.id,
sequence: 3,
level: "info",
message: "Classifying the next table",
createdAt: "2026-08-28T11:00:02Z",
}];
expect(await within(progress).findByText("Classifying the next table", {}, { timeout: 2_000 })).toBeVisible();
expect(eventLog.scrollTop).toBe(0);
await user.click(jumpToLatest);
expect(eventLog.scrollTop).toBe(eventLog.scrollHeight);
expect(within(progress).queryByRole("button", { name: "Jump to latest" })).not.toBeInTheDocument();
releaseAnalysis();
await launch;
expect(await screen.findByRole("dialog", { name: "Sensitive field review" })).toBeVisible();
});
test("requests database-level sensitivity analysis for the only selected database", async () => {
const user = userEvent.setup();
let suggestionBody: unknown;
let suggestionFinished = false;
let historyCalls = 0;
const run = makeSensitivityAnalysisRun({ scope: "all", total: 1 });
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/sensitive-data-suggestion-runs", () => {
historyCalls += 1;
return HttpResponse.json(suggestionFinished ? [run] : []);
}),
http.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", async ({ request }) => {
suggestionBody = await request.json();
suggestionFinished = true;
return HttpResponse.json({
run,
suggestions: [{
columnId: patientIdColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: patientIdColumn.name,
version: patientIdColumn.version,
currentSensitive: false,
sensitive: true,
assessment: "sensitive",
evidence: [{ kind: "content", ruleId: "pii.email" }],
observedValues: 1,
coverage: "sampled",
}],
});
}),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({ scope: "all" }));
expect(await screen.findByRole("dialog", { name: "Sensitive field review" })).toBeVisible();
await waitFor(() => expect(historyCalls).toBeGreaterThanOrEqual(2));
});
test("refetches sensitivity analysis history after a failed request", async () => {
const user = userEvent.setup();
let historyCalls = 0;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/sensitive-data-suggestion-runs", () => {
historyCalls += 1;
return HttpResponse.json([]);
}),
http.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", () => (
HttpResponse.json({
code: "sensitivity_source_scan_failed",
message: "Sensitivity analysis could not read the source. No assessments were applied.",
}, { status: 502 })
)),
);
renderPage({ rows: [makeDatabase()] });
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
await waitFor(() => expect(historyCalls).toBeGreaterThanOrEqual(2));
expect(screen.queryByRole("dialog", { name: "Sensitive field review" })).not.toBeInTheDocument();
});
test("requests sensitivity analysis only for selected tables", async () => {
const user = userEvent.setup();
const visitsTable: CatalogTable = {
...patientsTable,
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
name: "visits",
};
const visitColumn = {
...patientIdColumn,
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
tableId: visitsTable.id,
name: "clinical_note",
};
let suggestionBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([
patientsTable,
visitsTable,
])),
http.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", async ({ request }) => {
suggestionBody = await request.json();
return HttpResponse.json({
suggestions: [{
columnId: visitColumn.id,
tableId: visitsTable.id,
tableName: visitsTable.name,
columnName: visitColumn.name,
version: visitColumn.version,
currentSensitive: false,
sensitive: true,
assessment: "sensitive",
evidence: [{ kind: "metadata", ruleId: "metadata.health" }],
observedValues: 0,
coverage: "metadata",
}],
});
}),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const visitsRow = await screen.findByRole("row", { name: /visits/ });
await user.click(within(visitsRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({
scope: "selected_tables",
targetIds: [visitsTable.id],
}));
expect(await screen.findByRole("dialog", { name: "Sensitive field review" })).toBeVisible();
});
test("allows a human downgrade and saves only explicit sensitivity changes", async () => {
const user = userEvent.setup();
const idColumn = { ...patientIdColumn, sensitive: false };
const nameColumn = {
...patientIdColumn,
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
name: "name",
ordinalPosition: 2,
primaryKeyPosition: null,
isPrimaryKey: false,
description: "Patient name",
generatedDescription: "Name of the patient",
sensitive: true,
};
const unselectedColumn = {
...patientIdColumn,
id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
name: "address",
ordinalPosition: 3,
primaryKeyPosition: null,
isPrimaryKey: false,
description: "Patient address",
generatedDescription: "Address of the patient",
sensitive: false,
};
let columns = [idColumn, nameColumn, unselectedColumn];
let suggestionBody: unknown;
const patches: Array<{ columnId: string; body: unknown }> = [];
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get(
"/api/catalog/databases/:databaseId/tables/:tableId/columns",
() => HttpResponse.json(columns),
),
http.post(
"/api/catalog/databases/:databaseId/sensitive-data-suggestions",
async ({ request }) => {
suggestionBody = await request.json();
return HttpResponse.json({
suggestions: [
{
columnId: idColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: idColumn.name,
version: idColumn.version,
currentSensitive: false,
sensitive: true,
assessment: "sensitive",
evidence: [{ kind: "content", ruleId: "pii.email" }],
observedValues: 1,
coverage: "sampled",
},
{
columnId: nameColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: nameColumn.name,
version: nameColumn.version,
currentSensitive: true,
sensitive: false,
assessment: "non_sensitive",
evidence: [{ kind: "coverage", ruleId: "coverage.complete" }],
observedValues: 2,
coverage: "complete",
},
],
});
},
),
http.patch(
"/api/catalog/databases/:databaseId/tables/:tableId/columns/:columnId",
async ({ params, request }) => {
const body = await request.json() as {
version: number;
sensitive: boolean;
};
patches.push({ columnId: String(params.columnId), body });
const current = columns.find((column) => column.id === params.columnId)!;
const updated = { ...current, ...body, version: current.version + 1 };
columns = columns.map((column) => column.id === updated.id ? updated : column);
return HttpResponse.json(updated);
},
),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const idSensitive = await screen.findByRole("checkbox", { name: "Sensitive data for id" });
const nameSensitive = await screen.findByRole("checkbox", { name: "Sensitive data for name" });
expect(idSensitive).not.toBeChecked();
expect(nameSensitive).toBeChecked();
expect(screen.queryByRole("button", { name: "Analyze sensitive fields" })).not.toBeInTheDocument();
const selectableRow = async (name: RegExp) => {
const rows = await screen.findAllByRole("row", { name });
return rows.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }))!;
};
await user.click(within(await selectableRow(/Patient identifier/))
.getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(await selectableRow(/Patient name/))
.getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Analyze sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({
scope: "selected_columns",
targetIds: [idColumn.id, nameColumn.id],
}));
const review = await screen.findByRole("dialog", { name: "Sensitive field review" });
expect(within(review).getByRole("checkbox", { name: "Protect patients.id" })).toBeChecked();
expect(within(review).getByRole("checkbox", { name: "Protect patients.name" })).not.toBeChecked();
expect(patches).toHaveLength(0);
await user.click(within(review).getByRole("checkbox", { name: "Protect patients.id" }));
await user.click(within(review).getByRole("button", { name: "Save all 1 change" }));
await waitFor(() => expect(patches).toEqual([{
columnId: nameColumn.id,
body: {
version: nameColumn.version,
sensitive: false,
sensitivityReason: null,
},
}]));
expect(await screen.findByText("Saved 1 sensitive flag")).toBeVisible();
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Sensitive field review" })).not.toBeInTheDocument());
await waitFor(() => expect(screen.getByRole("checkbox", { name: "Sensitive data for id" })).not.toBeChecked());
expect(screen.getByRole("checkbox", { name: "Sensitive data for name" })).not.toBeChecked();
});
test("polls a description run and renders its events in sequence order", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun();
const runningRun = makeDescriptionGenerationRun({
status: "running",
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:01Z",
});
const completedRun = makeDescriptionGenerationRun({
status: "completed",
processed: 1,
generated: 1,
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:03Z",
finishedAt: "2026-08-28T10:00:03Z",
});
let runReads = 0;
const eventAfters: number[] = [];
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([patientIdColumn])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => {
runReads += 1;
return HttpResponse.json(runReads < 3 ? runningRun : completedRun);
}),
http.get("/api/catalog/description-generation-runs/:runId/events-list", ({ request }) => {
const after = Number(new URL(request.url).searchParams.get("after") ?? 0);
eventAfters.push(after);
if (after === 0) return HttpResponse.json([
{ sequence: 2, level: "info", message: "Calling the selected model", createdAt: "2026-08-28T10:00:02Z" },
{ sequence: 1, level: "info", message: "Run queued", createdAt: "2026-08-28T10:00:00Z" },
]);
if (after === 2 && runReads >= 3) return HttpResponse.json([
{ sequence: 3, level: "info", message: "Generated description saved", createdAt: "2026-08-28T10:00:03Z" },
]);
return HttpResponse.json([]);
}),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
const drawer = await screen.findByRole("dialog", { name: "Description generation" });
expect(within(drawer).getByText(/local[/]qwen/)).toBeVisible();
expect(within(drawer).getByText(/selected columns/)).toBeVisible();
expect(await within(drawer).findByRole("heading", { name: "Completed" }, { timeout: 4_000 })).toBeVisible();
expect(within(drawer).getByText("1", { selector: "[data-count='total']" })).toBeVisible();
expect(within(drawer).getByText("1", { selector: "[data-count='processed']" })).toBeVisible();
expect(within(drawer).getByText("1", { selector: "[data-count='generated']" })).toBeVisible();
expect(within(drawer).getByText("0", { selector: "[data-count='nonGeneratable']" })).toBeVisible();
expect(within(drawer).getByText("0", { selector: "[data-count='failed']" })).toBeVisible();
const timestamps = within(drawer).getByRole("region", {
name: "Description generation timestamps",
});
expect(within(timestamps).getByText("Created")).toBeVisible();
expect(within(timestamps).getByText("Started")).toBeVisible();
expect(within(timestamps).getByText("Updated")).toBeVisible();
expect(within(timestamps).getByText("Finished")).toBeVisible();
const log = within(drawer).getByRole("log", { name: "Description generation events" });
await waitFor(() => expect(log).toHaveTextContent("Generated description saved"), { timeout: 4_000 });
expect(log.textContent!.indexOf("Run queued")).toBeLessThan(log.textContent!.indexOf("Calling the selected model"));
expect(log.textContent!.indexOf("Calling the selected model")).toBeLessThan(log.textContent!.indexOf("Generated description saved"));
expect(eventAfters).toContain(2);
});
test("refreshes Catalog Tables and Catalog Columns after column generation completes", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun();
const completedRun = makeDescriptionGenerationRun({
status: "completed",
processed: 1,
generated: 1,
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:02Z",
finishedAt: "2026-08-28T10:00:02Z",
});
let columnReads = 0;
let tableReads = 0;
let columns = [patientIdColumn];
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => {
tableReads += 1;
return HttpResponse.json([patientsTable]);
}),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => {
columnReads += 1;
return HttpResponse.json(columns);
}),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => {
columns = [{
...patientIdColumn,
generatedDescription: "Stable patient identifier",
version: 2,
}];
return HttpResponse.json(completedRun);
}),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
const { client } = renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const unrelatedColumnsQuery = [
"catalog-columns",
patientsTable.databaseId,
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
] as const;
client.setQueryData(unrelatedColumnsQuery, []);
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
expect(await screen.findByRole("heading", { name: "Completed" })).toBeVisible();
await waitFor(() => expect(columnReads).toBe(2));
await waitFor(() => expect(tableReads).toBe(2));
expect(within((await screen.findAllByRole("row", { name: /Stable patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }))!).getByText("Stable patient identifier")).toBeVisible();
expect(client.getQueryState(unrelatedColumnsQuery)?.isInvalidated).toBe(true);
});
test("keeps the selected column and shows a safe message when generation cannot start", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([patientIdColumn])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => HttpResponse.json({
code: "description_generation_run_active",
message: "provider=private-provider key=sk-private-key",
}, { status: 409 })),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
expect(await screen.findByText("A description generation run is already active.")).toBeVisible();
expect(screen.getByText("1 selected")).toBeVisible();
expect(screen.queryByRole("dialog", { name: "Description generation" })).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent(/private-provider|sk-private-key/);
});
test("shows a basic failed run without exposing private model or provider fields", async () => {
const user = userEvent.setup();
let columnReads = 0;
const queuedRun = {
...makeDescriptionGenerationRun({ modelId: "approved/model" }),
provider: "private-provider",
apiKey: "sk-private-key",
};
const failedRun = {
...makeDescriptionGenerationRun({
modelId: "approved/model",
status: "failed",
processed: 1,
failed: 1,
startedAt: "2026-08-28T10:00:01Z",
updatedAt: "2026-08-28T10:00:02Z",
finishedAt: "2026-08-28T10:00:02Z",
errorSummary: "The model request failed before a description was saved.",
}),
provider: "private-provider",
upstreamModel: "private/upstream-model",
apiKey: "sk-private-key",
prompt: "full private prompt",
samples: ["private-sample-value"],
providerPayload: { diagnostic: "private provider payload" },
};
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{
id: "approved/model",
label: "Approved model",
provider: "private-provider",
upstreamModel: "private/upstream-model",
apiKeyEnv: "PRIVATE_MODEL_API_KEY",
}],
default: "approved/model",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => {
columnReads += 1;
return HttpResponse.json([patientIdColumn]);
}),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(failedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([
{ sequence: 1, level: "error", message: "Description generation failed", createdAt: "2026-08-28T10:00:02Z" },
])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
const drawer = await screen.findByRole("dialog", { name: "Description generation" });
expect(await within(drawer).findByRole("heading", { name: "Failed" })).toBeVisible();
expect(within(drawer).getByText(/Approved model/)).toBeVisible();
expect(within(drawer).getByText("The model request failed before a description was saved.")).toBeVisible();
expect(await within(drawer).findByText("Description generation failed")).toBeVisible();
await waitFor(() => expect(columnReads).toBe(2));
expect(document.body).not.toHaveTextContent(
/private-provider|private\/upstream-model|sk-private-key|full private prompt|private-sample-value|private provider payload|PRIVATE_MODEL_API_KEY/,
);
});
test("offers generation and consolidation for multiple selected columns", async () => {
const user = userEvent.setup();
const patientNameColumn: CatalogColumn = {
...patientIdColumn,
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
name: "name",
ordinalPosition: 2,
dataType: "text",
primaryKeyPosition: null,
isPrimaryKey: false,
sourceComment: "Patient name",
};
const queuedRun = makeDescriptionGenerationRun({ total: 2 });
let startBody: unknown;
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => (
HttpResponse.json([patientIdColumn, patientNameColumn])
)),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", async ({ request }) => {
startBody = await request.json();
return HttpResponse.json(queuedRun, { status: 202 });
}),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json(queuedRun)),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const idRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
const nameRow = (await screen.findAllByRole("row", { name: /Patient name/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(idRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate column description" })).toBeEnabled();
expect(screen.getByRole("menuitem", {
name: "Copy generated descriptions to Description",
})).toBeEnabled();
await user.keyboard("{Escape}");
await user.click(within(nameRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
const generate = await screen.findByRole("menuitem", { name: "Generate column descriptions" });
expect(generate).toBeEnabled();
expect(await screen.findByRole("menuitem", {
name: "Copy generated descriptions to Description",
})).toBeEnabled();
await user.click(generate);
await waitFor(() => expect(startBody).toEqual({
modelId: "local/qwen",
scope: "selected_columns",
targetIds: [patientIdColumn.id, patientNameColumn.id],
}));
expect(await screen.findByText("Description generation started for 2 columns")).toBeVisible();
expect(screen.getByText("2 selected")).toBeVisible();
});
test.each([
{ name: "database.manage is absent", canManage: false, models: [{ id: "local/qwen", label: "Local Qwen" }], configuredDefault: "local/qwen" },
{ name: "no usable model is configured", canManage: true, models: [], configuredDefault: null },
])("disables selected table and column generation when $name", async ({ canManage, models, configuredDefault }) => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models,
default: configuredDefault,
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([patientIdColumn])),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
canManage,
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate descriptions for all columns" }))
.toHaveAttribute("aria-disabled", "true");
await user.keyboard("{Escape}");
await user.click(screen.getByRole("button", { name: "Clear" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate column description" }))
.toHaveAttribute("aria-disabled", "true");
});
test("disables another generation start while the selected-column run is active", async () => {
const user = userEvent.setup();
const queuedRun = makeDescriptionGenerationRun();
server.use(
http.get("/api/catalog/metadata-generation/models", () => HttpResponse.json({
models: [{ id: "local/qwen", label: "Local Qwen" }],
default: "local/qwen",
})),
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([patientIdColumn])),
http.post("/api/catalog/databases/:databaseId/description-generation-runs", () => (
HttpResponse.json(queuedRun, { status: 202 })
)),
http.get("/api/catalog/description-generation-runs/:runId", () => HttpResponse.json({
...queuedRun,
status: "running",
startedAt: "2026-08-28T10:00:01Z",
})),
http.get("/api/catalog/description-generation-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
const columnRow = (await screen.findAllByRole("row", { name: /Patient identifier/ }))
.find((row) => within(row).queryByRole("checkbox", { name: /toggle row selection/i }));
await user.click(within(columnRow!).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Generate column description" }));
expect(await screen.findByRole("dialog", { name: "Description generation" })).toBeVisible();
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate column description" }))
.toHaveAttribute("aria-disabled", "true");
expect(screen.getByText("1 selected")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Back to tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", { name: "Generate descriptions for all columns" }))
.toHaveAttribute("aria-disabled", "true");
});
test("disables selected description consolidation without database.manage", async () => {
const user = userEvent.setup();
let consolidationCalls = 0;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([
{ ...patientsTable, generatedDescription: "Generated patients" },
])),
http.post("/api/catalog/databases/:databaseId/descriptions/consolidate", () => {
consolidationCalls += 1;
return HttpResponse.json({ copied: 1, skipped: 0 });
}),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
canManage: false,
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(await screen.findByRole("menuitem", {
name: "Copy generated descriptions to Description",
})).toHaveAttribute("aria-disabled", "true");
expect(consolidationCalls).toBe(0);
});
test("shows an operation conflict without clearing selected descriptions or refreshing tables", async () => {
const user = userEvent.setup();
let tableReads = 0;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => {
tableReads += 1;
return HttpResponse.json([{ ...patientsTable, generatedDescription: "Generated patients" }]);
}),
http.post("/api/catalog/databases/:databaseId/descriptions/consolidate", () => HttpResponse.json({
code: "database_operation_in_progress",
message: "A database operation is already in progress.",
}, { status: 409 })),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", {
name: "Copy generated descriptions to Description",
}));
expect(await screen.findByText("A database operation is already in progress.")).toBeVisible();
expect(screen.getByText("1 selected")).toBeVisible();
expect(tableReads).toBe(1);
});
test("selected table Actions do not expose relationship cleanup", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
const tableRow = await screen.findByRole("row", { name: /patients/ });
await user.click(within(tableRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
expect(screen.queryByRole("menuitem", { name: "Delete all relationships" })).not.toBeInTheDocument();
expect(screen.queryByRole("menuitem", { name: "Clear catalog relationships" })).not.toBeInTheDocument();
});
test("navigates purely from a database to its tables and edits review metadata", async () => {
const user = userEvent.setup();
let patchBody: unknown;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.patch("/api/catalog/databases/:databaseId/tables/:tableId", async ({ request }) => {
patchBody = await request.json();
return HttpResponse.json({
...patientsTable,
description: "Registry used for longitudinal patient analysis",
version: 2,
});
}),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
expect(await screen.findByRole("region", { name: "Tables for Policlinico San Donato" })).toBeVisible();
expect(screen.getByRole("tab", { name: "Tables" })).toHaveAttribute("aria-selected", "true");
expect(screen.queryByLabelText("Database filter")).not.toBeInTheDocument();
await user.click(await screen.findByRole("button", { name: "Edit description for patients" }));
expect(screen.getByLabelText("Physical table name")).toHaveValue("patients");
expect(screen.getByLabelText("Physical table name")).toHaveAttribute("readonly");
expect(screen.getByLabelText("Source comment")).toHaveAttribute("readonly");
await user.type(
screen.getByLabelText(/^Description/),
"Registry used for longitudinal patient analysis",
);
await user.click(screen.getByRole("button", { name: "Save metadata" }));
await waitFor(() => expect(patchBody).toEqual({
version: 1,
description: "Registry used for longitudinal patient analysis",
generatedDescription: null,
}));
await user.click(screen.getByRole("button", { name: "Back to tables" }));
await user.click(screen.getByRole("tab", { name: "Overview" }));
expect(await screen.findByRole("heading", { name: "Database details" })).toBeVisible();
});
test("navigates from a table to columns and from the database to physical relationships", async () => {
const user = userEvent.setup();
let columnPatch: unknown;
const idColumn: CatalogColumn = {
id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
tableId: patientsTable.id,
name: "id",
ordinalPosition: 1,
dataType: "bigint",
isNullable: false,
defaultExpression: null,
primaryKeyPosition: 1,
isPrimaryKey: true,
isForeignKey: true,
foreignKeyCount: 1,
sourceComment: "Patient identifier",
description: null,
generatedDescription: null,
sensitive: false,
lastSyncedDatabaseVersion: 3,
lastSyncedAt: "2026-08-27T10:00:00Z",
version: 1,
createdAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:00Z",
};
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.get("/api/catalog/databases/:databaseId/tables/:tableId/columns", () => HttpResponse.json([idColumn])),
http.patch("/api/catalog/databases/:databaseId/tables/:tableId/columns/:columnId", async ({ request }) => {
columnPatch = await request.json();
return HttpResponse.json({ ...idColumn, generatedDescription: "Generated identifier draft", version: 2 });
}),
http.get("/api/catalog/databases/:databaseId/relationships", () => HttpResponse.json([{
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
databaseId: patientsTable.databaseId,
constraintName: "visits_patient_id_fkey",
sourceTableId: "ffffffff-ffff-4fff-8fff-ffffffffffff",
sourceTableName: "visits",
targetTableId: patientsTable.id,
targetTableName: "patients",
updateRule: "NO ACTION",
deleteRule: "CASCADE",
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",
updatedAt: "2026-08-27T10:00:00Z",
}])),
);
renderPage({ rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })] });
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "View columns for patients" }));
expect(screen.getByRole("tab", { name: "Columns" })).toHaveAttribute("aria-selected", "true");
expect(await screen.findByText("PK")).toBeVisible();
expect(screen.getByText("FK")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Edit metadata for id" }));
await user.type(screen.getByLabelText("Generated description"), "Generated identifier draft");
await user.click(screen.getByRole("button", { name: "Save metadata" }));
await waitFor(() => expect(columnPatch).toEqual({
version: 1,
description: null,
generatedDescription: "Generated identifier draft",
}));
await user.click(screen.getByRole("tab", { name: "Relationships" }));
expect(await screen.findByText("visits_patient_id_fkey")).toBeVisible();
});
test("opens the durable job drawer and confirms its exact destructive plan", async () => {
const user = userEvent.setup();
const startBodies: unknown[] = [];
const confirmationBodies: unknown[] = [];
const queued: CatalogSyncRun = {
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
databaseId: "11111111-1111-4111-8111-111111111111",
scope: "tables",
tableIds: [],
state: "queued",
phase: "queued",
requestedDatabaseVersion: 3,
plannedDiff: null,
confirmationToken: null,
counts: {},
errorCode: null,
errorMessage: null,
cancelRequested: false,
createdAt: "2026-08-27T10:00:00Z",
startedAt: null,
updatedAt: "2026-08-27T10:00:00Z",
finishedAt: null,
heartbeatAt: null,
};
const waiting: CatalogSyncRun = {
...queued,
state: "awaiting_confirmation",
phase: "awaiting_confirmation",
confirmationToken: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
plannedDiff: { deletedTables: ["legacy_visits"], deletedColumns: [], deletedRelationships: [] },
counts: { tables: 1 },
startedAt: "2026-08-27T10:00:00Z",
updatedAt: "2026-08-27T10:00:01Z",
};
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([patientsTable])),
http.post("/api/catalog/databases/:databaseId/sync-runs", async ({ request }) => {
startBodies.push(await request.json());
return HttpResponse.json(queued, { status: 202 });
}),
http.get("/api/catalog/sync-runs/:runId", () => HttpResponse.json(waiting)),
http.get("/api/catalog/sync-runs/:runId/events-list", () => HttpResponse.json([])),
http.get("/api/catalog/databases/:databaseId/sync-runs", () => HttpResponse.json([waiting])),
http.post("/api/catalog/sync-runs/:runId/confirm", async ({ request }) => {
confirmationBodies.push(await request.json());
return HttpResponse.json({ ...waiting, state: "queued", phase: "queued", confirmationToken: null });
}),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(await screen.findByRole("menuitem", { name: "Synchronize tables" }));
const synchronizationDrawer = await screen.findByRole("dialog", {
name: "Schema synchronization",
});
expect(synchronizationDrawer).toHaveAttribute("data-catalog-panel");
expect(synchronizationDrawer.querySelector(
':scope > [data-catalog-panel-region="header"]',
)).toBeInTheDocument();
expect(synchronizationDrawer.querySelector(
':scope > [data-catalog-panel-region="body"]',
)).toBeInTheDocument();
expect(within(synchronizationDrawer).getByRole("listitem", {
name: "Confirmation required, in progress",
})).toHaveAttribute("aria-current", "step");
expect(await screen.findByText("table · legacy_visits")).toBeVisible();
expect(startBodies).toEqual([{ version: 3, scope: "tables", tableIds: [] }]);
await user.click(screen.getByRole("button", { name: "Confirm removals" }));
await waitFor(() => expect(confirmationBodies).toEqual([
{ confirmationToken: "cccccccc-cccc-4ccc-8ccc-cccccccccccc" },
]));
});
test("keeps a stale table draft but requires an explicit reload before another save", async () => {
const user = userEvent.setup();
const current = {
...patientsTable,
description: "Description saved by another editor",
version: 2,
};
let latest = patientsTable;
server.use(
http.get("/api/catalog/databases/:databaseId/tables", () => HttpResponse.json([latest])),
http.patch("/api/catalog/databases/:databaseId/tables/:tableId", () => {
latest = current;
return HttpResponse.json({
code: "table_stale",
message: "Table description changed. Reload and try again.",
}, { status: 409 });
}),
);
renderPage({
rows: [makeDatabase({ connectionStatus: "reachable", testedVersion: 3 })],
});
await user.click(await screen.findByRole("button", { name: "View Policlinico San Donato" }));
await user.click(screen.getByRole("tab", { name: "Tables" }));
await user.click(await screen.findByRole("button", { name: "Edit description for patients" }));
const description = screen.getByLabelText(/^Description/);
await user.type(description, "My unsaved draft");
await user.click(screen.getByRole("button", { name: "Save metadata" }));
expect(await screen.findByText("A newer table description is available.")).toBeVisible();
expect(description).toHaveValue("My unsaved draft");
expect(screen.getByRole("button", { name: "Save metadata" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Keep editing" }));
expect(screen.getByText("Reload the latest value before this description can be saved.")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Reload latest" }));
await waitFor(() => expect(description).toHaveValue("Description saved by another editor"));
expect(screen.getByRole("button", { name: "Save metadata" })).toBeDisabled();
});