feat(interview): integrate contextual AI prep

This commit is contained in:
cesnimda
2026-08-28 12:53:35 +02:00
parent 3b4adb8281
commit 8e344379b4
3 changed files with 272 additions and 30 deletions
+37 -1
View File
@@ -1,9 +1,13 @@
import React from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";
import { ApplicationFollowUp, ApplicationInterviewPrep } from "./components/InterviewPrep";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
import { ConfirmProvider } from "./confirm";
const render = (ui: React.ReactElement) => rtlRender(<I18nProvider><ConfirmProvider>{ui}</ConfirmProvider></I18nProvider>);
jest.mock("./api", () => ({
api: {
@@ -48,6 +52,7 @@ const followUp = { followUpAt: "2026-07-26T00:00:00", nextAction: "Chase recruit
function routeGet(overrides: Record<string, any> = {}) {
mockedApi.get.mockImplementation((url: string) => {
if (url.endsWith("/follow-up")) return Promise.resolve({ data: overrides.followUp ?? followUp } as any);
if (url.endsWith("/cv")) return Promise.resolve({ data: overrides.cv ?? { attachedVariantId: 3, attachedVariantName: "Backend CV" } } as any);
return Promise.resolve({ data: overrides.board ?? board } as any);
});
}
@@ -116,6 +121,7 @@ test("deleting a prep item calls delete", async () => {
render(<ApplicationInterviewPrep jobId={7} />);
fireEvent.click(await screen.findByRole("button", { name: "Delete: Funding history" }));
fireEvent.click(await screen.findByRole("button", { name: "Delete" }));
await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith("/jobapplications/7/interview-prep/1"));
});
@@ -129,6 +135,36 @@ test("empty prep shows a useful empty state", async () => {
expect(screen.getByText(/has not reached an interview stage/i)).toBeInTheDocument();
});
test("AI preparation uses the linked CV and only saves after explicit acceptance", async () => {
routeGet();
mockedApi.post.mockImplementation((url: string) => {
if (url.endsWith("/ai/generate")) {
return Promise.resolve({ data: { result: { text: "## Technical\nExplain dependency injection." } } } as any);
}
return Promise.resolve({ data: board.groups[0].items[0] } as any);
});
render(<ApplicationInterviewPrep jobId={7} />);
expect(await screen.findByText("Backend CV")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Generate preparation" }));
expect(await screen.findByText(/Explain dependency injection/i)).toBeInTheDocument();
expect(mockedApi.post).toHaveBeenCalledWith(
"/jobapplications/7/ai/generate",
expect.objectContaining({ module: "interview", targetLanguage: "en" }),
);
expect(mockedApi.post).not.toHaveBeenCalledWith(
"/jobapplications/7/interview-prep",
expect.anything(),
);
fireEvent.click(screen.getByRole("button", { name: "Add to preparation" }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
"/jobapplications/7/interview-prep",
expect.objectContaining({ source: "ai", content: expect.stringContaining("dependency injection") }),
));
});
test("prep surfaces a load error", async () => {
mockedApi.get.mockRejectedValue(new Error("boom"));