Files
ThothII/frontend/src/shell/database-management/SensitiveDataSuggestionHistoryDrawer.test.tsx
T

138 lines
5.4 KiB
TypeScript

import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { http, HttpResponse } from "msw";
import { useState } from "react";
import type { SensitiveDataSuggestionRun } from "../../api/catalog-databases";
import { server } from "../../test/msw";
import { SensitiveDataSuggestionHistoryDrawer } from "./SensitiveDataSuggestionHistoryDrawer";
const runningRun: SensitiveDataSuggestionRun = {
id: "99999999-9999-4999-8999-999999999999",
databaseId: "11111111-1111-4111-8111-111111111111",
scope: "selected_columns",
modelId: "local-qwen",
status: "running",
total: 4,
suggestedSensitive: 1,
suggestedNonSensitive: 1,
createdAt: "2026-08-28T09:00:00Z",
startedAt: "2026-08-28T09:00:00Z",
updatedAt: "2026-08-28T09:00:01Z",
finishedAt: null,
errorSummary: null,
};
function renderDrawer(initialRun: SensitiveDataSuggestionRun | null) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
});
function Harness() {
const [run, setRun] = useState(initialRun);
return (
<SensitiveDataSuggestionHistoryDrawer
open
run={run}
modelLabel="Local Qwen"
onClose={() => undefined}
onRunUpdate={setRun}
/>
);
}
return render(
<QueryClientProvider client={client}>
<Harness />
</QueryClientProvider>,
);
}
test("shows an explicit empty state before any sensitive suggestion runs exist", async () => {
server.use(http.get(
"/api/catalog/sensitive-data-suggestion-runs",
() => HttpResponse.json([]),
));
renderDrawer(null);
const drawer = await screen.findByRole("dialog", { name: "Sensitive suggestion history" });
expect(within(drawer).getByRole("heading", { name: "Suggestion run history" })).toBeVisible();
expect(await within(drawer).findByText("No sensitive suggestion runs yet.")).toBeVisible();
});
test("shows sensitive suggestion results, safe events, and lets operators inspect an older run", async () => {
const completedRun: SensitiveDataSuggestionRun = {
...runningRun,
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
scope: "all",
status: "completed",
total: 6,
suggestedSensitive: 2,
suggestedNonSensitive: 4,
createdAt: "2026-08-28T08:00:00Z",
startedAt: "2026-08-28T08:00:00Z",
updatedAt: "2026-08-28T08:00:01Z",
finishedAt: "2026-08-28T08:00:01Z",
};
server.use(
http.get("/api/catalog/sensitive-data-suggestion-runs", () => (
HttpResponse.json([runningRun, completedRun])
)),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId", ({ params }) => (
HttpResponse.json(params.runId === completedRun.id ? completedRun : runningRun)
)),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId/events-list", ({ params }) => (
HttpResponse.json([{
runId: params.runId,
sequence: 1,
level: "info",
message: params.runId === completedRun.id ? "Classified 6 columns" : "Classifying columns",
createdAt: "2026-08-28T09:00:01Z",
}])
)),
);
const user = userEvent.setup();
renderDrawer(runningRun);
const drawer = await screen.findByRole("dialog", { name: "Sensitive suggestion history" });
expect(within(drawer).getByRole("heading", { name: "Running" })).toBeVisible();
const progress = within(drawer).getByRole("table", { name: "Sensitive suggestion progress" });
expect(within(progress).getAllByRole("columnheader")).toHaveLength(3);
expect(within(drawer).getByRole("log", { name: "Sensitive suggestion events" })).toHaveClass(
"thot-catalog-drawer__event-log",
);
expect(await within(drawer).findByText("Classifying columns")).toBeVisible();
expect(within(drawer).queryByRole("button", { name: "Stop" })).not.toBeInTheDocument();
expect(within(drawer).queryByRole("button", { name: "Unlock stale run" })).not.toBeInTheDocument();
const history = within(drawer).getByRole("region", { name: "Sensitive suggestion run history" });
await user.click(within(history).getByRole("button", { name: /Completed.*all/i }));
expect(await within(drawer).findByRole("heading", { name: "Completed" })).toBeVisible();
expect(within(drawer).getByText("2", { selector: "[data-count='suggestedSensitive']" })).toBeVisible();
expect(within(drawer).getByText("4", { selector: "[data-count='suggestedNonSensitive']" })).toBeVisible();
await waitFor(() => expect(within(drawer).getByText("Classified 6 columns")).toBeVisible());
});
test("does not expose unsafe provider details from a final error summary", async () => {
const failedRun: SensitiveDataSuggestionRun = {
...runningRun,
status: "failed",
finishedAt: "2026-08-28T09:00:02Z",
errorSummary: "stack trace at provider.ts:42 private source sample sk-secret",
};
server.use(
http.get("/api/catalog/sensitive-data-suggestion-runs", () => HttpResponse.json([failedRun])),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId", () => HttpResponse.json(failedRun)),
http.get("/api/catalog/sensitive-data-suggestion-runs/:runId/events-list", () => HttpResponse.json([])),
);
renderDrawer(failedRun);
expect(await screen.findByText(
"The run ended before all columns were classified. Review the event log for safe details.",
)).toBeVisible();
expect(screen.queryByText(/sk-secret/i)).not.toBeInTheDocument();
});