milestone 19: hygiene sweep - dead ecs surface, single-source constants, tls classification, frontend state hazards, docker smoke network fix
CI / test (push) Successful in 1m46s
CI / test-aarch64 (push) Successful in 5m30s
CI / frontend (push) Successful in 46s
CI / cross (push) Successful in 8m12s
CI / docker (push) Successful in 3m46s

This commit is contained in:
2026-08-07 20:39:27 +02:00
parent 6f67940995
commit 6c507992e4
59 changed files with 1020 additions and 382 deletions
@@ -1,9 +1,10 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { clearRefreshStatus } from "@/features/blocklists/refreshStore";
const BLOCKLISTS = {
blocklists: [
@@ -42,6 +43,7 @@ const RESPONSES: Record<string, unknown> = {
let resolveUpdate: ((response: Response) => void) | null;
beforeEach(() => {
clearRefreshStatus();
resolveUpdate = null;
vi.stubGlobal(
"fetch",
@@ -66,18 +68,35 @@ afterEach(() => {
vi.unstubAllGlobals();
});
function renderBlocklistsRoute() {
const queryClient = createQueryClient();
function renderBlocklistsRoute(queryClient = createQueryClient()) {
const router = createAppRouter(createMemoryHistory({ initialEntries: ["/blocklists"] }), queryClient);
render(
const view = render(
<AuthProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</AuthProvider>,
);
return { queryClient, unmount: view.unmount };
}
const SNAPSHOT = {
sources: [
{
id: 1,
state: "loaded",
loaded: true,
last_attempt: 1700000100,
last_success: 1700000100,
url: "https://example.com/hosts.txt",
last_error: "",
domains: 1200,
wildcards: 12,
skipped_regex: 4,
},
],
};
test("renders the source table and the status empty state", async () => {
renderBlocklistsRoute();
await screen.findByRole("heading", { name: "Blocklists" });
@@ -149,7 +168,8 @@ test("update now disables the button, then replaces the status section from the
expect(screen.getByText("12")).toBeTruthy();
expect(screen.getByText("4")).toBeTruthy();
expect(screen.queryByText(/run .Update now. to fetch status/)).toBeNull();
expect(screen.getByText(/Update completed/)).toBeTruthy();
// The store notifies one flush before the mutation's success state lands.
await screen.findByText(/Update completed/);
await waitFor(() => {
const idle = screen.getByRole("button", { name: "Update now" }) as HTMLButtonElement;
@@ -175,3 +195,35 @@ test("update now shows a countdown when rate limited with Retry-After", async ()
const alert = await screen.findByRole("alert");
expect(alert.textContent).toBe("Rate limited. Try again in 7s.");
});
test("the refresh snapshot outlives the query cache's gcTime", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
try {
const { queryClient, unmount } = renderBlocklistsRoute();
await screen.findByRole("heading", { name: "Blocklists" });
fireEvent.click(screen.getByRole("button", { name: "Update now" }));
await screen.findByRole("button", { name: "Updating…" });
resolveUpdate!(
new Response(JSON.stringify(SNAPSHOT), {
status: 202,
headers: { "content-type": "application/json" },
}),
);
await screen.findByText("loaded");
unmount();
// Well past the default 5-minute gcTime: an unsubscribed cache entry is
// collected by now, which is what used to erase the snapshot.
await act(async () => {
await vi.advanceTimersByTimeAsync(6 * 60_000);
});
renderBlocklistsRoute(queryClient);
await screen.findByRole("heading", { name: "Blocklists" });
expect(await screen.findByText("loaded")).toBeTruthy();
expect(screen.queryByText(/run .Update now. to fetch status/)).toBeNull();
} finally {
vi.useRealTimers();
}
});