mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Report how many documents the reindex actually queues
The index-all trigger returned Boolean(jobId) while the client typed the field as a number, so the toast read "Indexing true document(s)". The route now answers with the count of live documents still lacking a content index, computed from the same live-scoped counts the usage gauge reads, and an honest 0 when no job was created. OpenAPI schema updated to the integer shape; the hook's type carries a contract note so the wire and the toast cannot drift apart again. Route test pins the number, a component test pins that the toast renders the count and never the word "true". Refs #776
This commit is contained in:
1 parent
4543fa7192
commit
86a3d74187
6 files changed
+290
-8
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
/**
|
||||
* Refs #776 — the "index all" toast's count honesty.
|
||||
*
|
||||
* The reindex route reports `enqueued` as a NUMBER (documents still to
|
||||
* index); the toast interpolates it into "Indexing {count} document(s)".
|
||||
* Before the route fix the wire carried `Boolean(jobId)` and the toast said
|
||||
* "Indexing true document(s)". This pins the component end of that contract:
|
||||
* fed the real wire shape, the success toast renders the count and NEVER the
|
||||
* word "true"; a 0 says "everything is already indexed" instead of promising
|
||||
* queued work.
|
||||
*
|
||||
* House convention: `renderToStaticMarkup` + handler exercised via a mocked
|
||||
* child reference (the filter bar captures `onIndexAll`), no DOM events.
|
||||
*/
|
||||
|
||||
let mockSearch = "";
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/documents",
|
||||
useSearchParams: () => new URLSearchParams(mockSearch),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { id: "u1", modules: { inboundDocuments: true } },
|
||||
isLoading: false,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/api-fetch", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/api/api-fetch")>(
|
||||
"@/lib/api/api-fetch",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
apiGet: () => new Promise(() => {}),
|
||||
apiPatch: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
apiDelete: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const toastSuccess = vi.fn();
|
||||
const toastError = vi.fn();
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]) => toastSuccess(...args),
|
||||
error: (...args: unknown[]) => toastError(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
// Capture the view's index-all handler through the filter bar's prop — the
|
||||
// static render cannot click, so the mocked child is the seam.
|
||||
let capturedOnIndexAll: (() => void) | undefined;
|
||||
vi.mock("../document-filter-bar", () => ({
|
||||
DocumentFilterBar: (props: { onIndexAll?: () => void }) => {
|
||||
capturedOnIndexAll = props.onIndexAll;
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
// The reindex mutation resolves synchronously with a configurable wire
|
||||
// payload — exactly what `apiPost` would unwrap from the route envelope.
|
||||
let wirePayload: { enqueued: number };
|
||||
vi.mock("../use-content-index", () => ({
|
||||
useIndexDocument: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useReindexAll: () => ({
|
||||
mutate: (
|
||||
_vars: void,
|
||||
opts?: { onSuccess?: (result: { enqueued: number }) => void },
|
||||
) => {
|
||||
opts?.onSuccess?.(wirePayload);
|
||||
},
|
||||
isPending: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { DocumentsView } from "../documents-view";
|
||||
|
||||
function render() {
|
||||
mockSearch = "";
|
||||
capturedOnIndexAll = undefined;
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
renderToStaticMarkup(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<DocumentsView />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("<DocumentsView> index-all toast (#776)", () => {
|
||||
it("renders the remaining count — never the word 'true'", () => {
|
||||
wirePayload = { enqueued: 3 };
|
||||
render();
|
||||
expect(capturedOnIndexAll).toBeDefined();
|
||||
|
||||
capturedOnIndexAll!();
|
||||
|
||||
expect(toastSuccess).toHaveBeenCalledTimes(1);
|
||||
const message = String(toastSuccess.mock.calls[0]![0]);
|
||||
expect(message).toContain("3");
|
||||
expect(message).not.toContain("true");
|
||||
expect(toastError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("says everything is indexed on an honest 0, not 'indexing 0 documents'", () => {
|
||||
wirePayload = { enqueued: 0 };
|
||||
render();
|
||||
|
||||
capturedOnIndexAll!();
|
||||
|
||||
expect(toastSuccess).toHaveBeenCalledTimes(1);
|
||||
const message = String(toastSuccess.mock.calls[0]![0]);
|
||||
expect(message).toContain("already indexed");
|
||||
expect(message).not.toContain("true");
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,16 @@ export function useIndexDocument() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Fire the corpus backfill; resolves with how many docs were enqueued. */
|
||||
/**
|
||||
* Fire the corpus backfill; resolves with how many docs were enqueued.
|
||||
*
|
||||
* Wire contract (#776): `enqueued` is the NUMBER of live documents the
|
||||
* backfill still has to index — the reindex route computes it from the same
|
||||
* live-scoped counts the usage gauge reads, and returns an honest 0 when no
|
||||
* job was created. The toast interpolates it as a count, so this type is
|
||||
* load-bearing: it must match the route's shape exactly (a boolean here once
|
||||
* rendered as "Indexing true document(s)").
|
||||
*/
|
||||
export function useReindexAll() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{ enqueued: number }, Error, void>({
|
||||
|
||||
Reference in new issue
Block a user