3544 lines
157 KiB
TypeScript
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: "Catalog 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();
|
|
});
|