fix: scope and batch sensitive suggestions

This commit is contained in:
Codex
2026-08-30 17:12:20 +02:00
parent 0736983bc5
commit cb40c09d9a
17 changed files with 1179 additions and 139 deletions
@@ -1571,6 +1571,125 @@ test("starts one selected column with the configured default model", async () =>
expect(await screen.findByText("Description generation started for 1 column")).toBeVisible();
});
test("shows database sensitive suggestions 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: "Suggest 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: "Suggest sensitive fields" })).toBeVisible();
await user.click(within(radiologyRow).getByRole("checkbox", { name: /toggle row selection/i }));
await user.click(screen.getByRole("button", { name: "Suggest sensitive fields" }));
expect(await screen.findByText("Sensitive-field suggestions can be requested for only one database at a time. Select one database and try again.")).toBeVisible();
expect(suggestionCalls).toBe(0);
});
test("requests database-level sensitive suggestions for the only selected database", async () => {
const user = userEvent.setup();
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.post("/api/catalog/databases/:databaseId/sensitive-data-suggestions", async ({ request }) => {
suggestionBody = await request.json();
return HttpResponse.json({
suggestions: [{
columnId: patientIdColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: patientIdColumn.name,
version: patientIdColumn.version,
currentSensitive: false,
sensitive: true,
}],
});
}),
);
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: "Suggest sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({ modelId: "local-qwen", scope: "all" }));
expect(await screen.findByRole("complementary", { name: "Sensitive field review" })).toBeVisible();
});
test("requests sensitive suggestions 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,
}],
});
}),
);
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: "Suggest sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({
modelId: "local-qwen",
scope: "selected_tables",
targetIds: [visitsTable.id],
}));
expect(await screen.findByRole("complementary", { name: "Sensitive field review" })).toBeVisible();
});
test("reviews AI-sensitive-field suggestions as an editable draft and saves only changed columns", async () => {
const user = userEvent.setup();
const idColumn = { ...patientIdColumn, sensitive: false };
@@ -1585,7 +1704,18 @@ test("reviews AI-sensitive-field suggestions as an editable draft and saves only
generatedDescription: "Name of the patient",
sensitive: false,
};
let columns = [idColumn, nameColumn];
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(
@@ -1604,9 +1734,24 @@ test("reviews AI-sensitive-field suggestions as an editable draft and saves only
suggestionBody = await request.json();
return HttpResponse.json({
suggestions: [
{ columnId: idColumn.id, sensitive: true },
{ columnId: nameColumn.id, sensitive: true },
{ columnId: "ffffffff-ffff-4fff-8fff-ffffffffffff", sensitive: true },
{
columnId: idColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: idColumn.name,
version: idColumn.version,
currentSensitive: false,
sensitive: true,
},
{
columnId: nameColumn.id,
tableId: patientsTable.id,
tableName: patientsTable.name,
columnName: nameColumn.name,
version: nameColumn.version,
currentSensitive: false,
sensitive: true,
},
],
});
},
@@ -1616,8 +1761,6 @@ test("reviews AI-sensitive-field suggestions as an editable draft and saves only
async ({ params, request }) => {
const body = await request.json() as {
version: number;
description: string | null;
generatedDescription: string | null;
sensitive: boolean;
};
patches.push({ columnId: String(params.columnId), body });
@@ -1638,31 +1781,41 @@ test("reviews AI-sensitive-field suggestions as an editable draft and saves only
const nameSensitive = await screen.findByRole("checkbox", { name: "Sensitive data for name" });
expect(idSensitive).not.toBeChecked();
expect(nameSensitive).not.toBeChecked();
expect(screen.queryByRole("button", { name: "Suggest 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: "Suggest sensitive fields" }));
await waitFor(() => expect(suggestionBody).toEqual({ modelId: "local-qwen" }));
await waitFor(() => {
expect(screen.getByRole("checkbox", { name: "Sensitive data for id" })).toBeChecked();
expect(screen.getByRole("checkbox", { name: "Sensitive data for name" })).toBeChecked();
});
await waitFor(() => expect(suggestionBody).toEqual({
modelId: "local-qwen",
scope: "selected_columns",
targetIds: [idColumn.id, nameColumn.id],
}));
const review = await screen.findByRole("complementary", { name: "Sensitive field review" });
expect(within(review).getByRole("checkbox", { name: "Protect patients.id" })).toBeChecked();
expect(within(review).getByRole("checkbox", { name: "Protect patients.name" })).toBeChecked();
expect(patches).toHaveLength(0);
await user.click(screen.getByRole("checkbox", { name: "Sensitive data for name" }));
expect(screen.getByRole("checkbox", { name: "Sensitive data for name" })).not.toBeChecked();
await user.click(screen.getByRole("button", { name: "Save sensitive fields" }));
await user.click(within(review).getByRole("checkbox", { name: "Protect patients.name" }));
await user.click(within(review).getByRole("button", { name: "Save 1" }));
await waitFor(() => expect(patches).toEqual([{
columnId: idColumn.id,
body: {
version: idColumn.version,
description: idColumn.description,
generatedDescription: idColumn.generatedDescription,
sensitive: true,
},
}]));
expect(await screen.findByText("Sensitive fields saved")).toBeVisible();
expect(screen.getByRole("checkbox", { name: "Sensitive data for id" })).toBeChecked();
expect(await screen.findByText("Saved 1 sensitive flag")).toBeVisible();
await waitFor(() => expect(screen.queryByRole("complementary", { name: "Sensitive field review" })).not.toBeInTheDocument());
await waitFor(() => expect(screen.getByRole("checkbox", { name: "Sensitive data for id" })).toBeChecked());
expect(screen.getByRole("checkbox", { name: "Sensitive data for name" })).not.toBeChecked();
});