fix(cv): bind letter proposals to current sources and preserve document choices
This commit is contained in:
@@ -359,7 +359,7 @@ test("AI cover letter suggestions use the linked CV and require explicit apply",
|
||||
));
|
||||
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team");
|
||||
fireEvent.click(await screen.findByRole("button", { name: /Apply to editor/i }));
|
||||
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("A tailored, truthful suggestion.");
|
||||
await waitFor(() => expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("A tailored, truthful suggestion."));
|
||||
|
||||
mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "A tailored, truthful suggestion." } } as any);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
@@ -369,6 +369,77 @@ test("AI cover letter suggestions use the linked CV and require explicit apply",
|
||||
));
|
||||
});
|
||||
|
||||
test("letter AI revalidates its recorded source before applying and refuses a cross-tab source conflict", async () => {
|
||||
routeGet();
|
||||
mockedApi.post.mockImplementation(((url: string) => url.endsWith('/generate')
|
||||
? Promise.resolve({ data: { id: 91, result: { text: 'Old source proposal' } } })
|
||||
: Promise.reject({ response: { status: 409, data: { code: 'cover_letter_source_changed' } } })) as any);
|
||||
render(<ApplicationCoverLetterSection jobId={7} />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Generate' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Apply to editor/i }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/7/ai/history/91/validate-source'));
|
||||
expect(await screen.findByText(copy('coverAiSourceChanged'))).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Cover letter' })).toHaveValue('Dear team');
|
||||
expect(screen.queryByRole('button', { name: /Apply to editor/i })).not.toBeInTheDocument();
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("letter AI language follows the saved document and updates the same local language draft", async () => {
|
||||
routeGet({ coverLetter: { ...coverLetter, documentSettings: { ...letterSettings, language: 'nb-NO' } } });
|
||||
render(<ApplicationCoverLetterSection jobId={7} />);
|
||||
expect(await screen.findByRole('combobox', { name: 'Letter language' })).toHaveTextContent('Norsk bokmål');
|
||||
await waitFor(() => expect(screen.getByRole('combobox', { name: 'Document language' })).toHaveTextContent('Norsk bokmål'));
|
||||
fireEvent.mouseDown(screen.getByRole('combobox', { name: 'Document language' }));
|
||||
fireEvent.click(await screen.findByRole('option', { name: 'English' }));
|
||||
expect(screen.getByRole('combobox', { name: 'Letter language' })).toHaveTextContent('English');
|
||||
expect(screen.getByRole('textbox', { name: 'Cover letter' })).toHaveValue('Dear team');
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
mockedApi.put.mockResolvedValue({ data: { ...coverLetter, revision: 8, documentSettings: { ...letterSettings, language: 'en' } } } as any);
|
||||
fireEvent.click(section('workspaceCoverLetter').getByRole('button', { name: 'Save' }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/jobapplications/7/cover-letter', expect.objectContaining({ documentSettings: { ...letterSettings, language: 'en' }, expectedVersion: 7 })));
|
||||
});
|
||||
|
||||
test.each(['edit', 'cancel', 'job change'])("letter AI ignores a late source validation after local %s", async action => {
|
||||
routeGet();
|
||||
const validation = deferred<any>();
|
||||
mockedApi.post.mockImplementation(((url: string) => url.endsWith('/generate')
|
||||
? Promise.resolve({ data: { id: 92, result: { text: 'Previously reviewed proposal' } } })
|
||||
: validation.promise) as any);
|
||||
const view = render(<ApplicationCoverLetterSection jobId={7} />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Generate' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Apply to editor/i }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/7/ai/history/92/validate-source'));
|
||||
if (action === 'edit') fireEvent.change(screen.getByRole('textbox', { name: 'Cover letter' }), { target: { value: 'New local draft' } });
|
||||
else if (action === 'cancel') fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
else {
|
||||
routeGet({ coverLetter: { ...coverLetter, text: 'Different job draft' } });
|
||||
view.rerender(withProviders(<ApplicationCoverLetterSection jobId={8} />));
|
||||
await screen.findByDisplayValue('Different job draft');
|
||||
}
|
||||
await act(async () => { validation.resolve({ status: 204 }); await validation.promise; });
|
||||
expect(screen.getByRole('textbox', { name: 'Cover letter' })).toHaveValue(action === 'edit' ? 'New local draft' : action === 'cancel' ? 'Dear team' : 'Different job draft');
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("a held letter Save prevents an in-flight AI validation from replacing the submitted snapshot", async () => {
|
||||
routeGet();
|
||||
const validation = deferred<any>(); const save = deferred<any>();
|
||||
mockedApi.post.mockImplementation(((url: string) => url.endsWith('/generate')
|
||||
? Promise.resolve({ data: { id: 94, result: { text: 'Validated proposal' } } }) : validation.promise) as any);
|
||||
mockedApi.put.mockReturnValue(save.promise);
|
||||
render(<ApplicationCoverLetterSection jobId={7} />);
|
||||
fireEvent.change(await screen.findByRole('textbox', { name: 'Cover letter' }), { target: { value: 'Snapshot being saved' } });
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Generate' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Apply to editor/i }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/7/ai/history/94/validate-source'));
|
||||
fireEvent.click(section('workspaceCoverLetter').getByRole('button', { name: 'Save' }));
|
||||
expect(mockedApi.put).toHaveBeenCalledWith('/jobapplications/7/cover-letter', expect.objectContaining({ text: 'Snapshot being saved', expectedVersion: 7 }));
|
||||
await act(async () => { validation.resolve({ status: 204 }); await validation.promise; });
|
||||
expect(screen.getByRole('textbox', { name: 'Cover letter' })).toHaveValue('Snapshot being saved');
|
||||
await act(async () => { save.resolve({ data: { ...coverLetter, text: 'Snapshot being saved', revision: 8 } }); await save.promise; });
|
||||
expect(screen.getByRole('textbox', { name: 'Cover letter' })).toHaveValue('Snapshot being saved');
|
||||
});
|
||||
|
||||
test("[PRES-W06] rejecting an AI cover letter preserves the saved draft without a write", async () => {
|
||||
routeGet();
|
||||
mockedApi.post.mockResolvedValue({ data: { result: { text: "Unaccepted letter" } } } as never);
|
||||
@@ -601,14 +672,15 @@ test("held AI generation locks its controls and preserves captured action attrib
|
||||
expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/7/ai/generate", expect.objectContaining({ module: "cover-letter", action: "generate", mode: "professional", targetLanguage: "nb-NO", currentText: "Dear team", extraContext: "Use only recorded context" }));
|
||||
for (const name of ["Action", "Tone", "Document language"]) expect(screen.getByRole("combobox", { name })).toHaveAttribute("aria-disabled", "true");
|
||||
expect(screen.getByRole("textbox", { name: "Additional instructions" })).toBeDisabled();
|
||||
await act(async () => { generation.resolve({ data: { result: { text: "Captured generated suggestion" } } }); });
|
||||
await act(async () => { generation.resolve({ data: { id: 93, result: { text: "Captured generated suggestion" } } }); });
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "Action" }));
|
||||
fireEvent.click(await screen.findByRole("option", { name: "Improve" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /Apply to editor/i }));
|
||||
await waitFor(() => expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Captured generated suggestion"));
|
||||
mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "Captured generated suggestion" } } as any);
|
||||
fireEvent.click(section("workspaceCoverLetter").getByRole("button", { name: "Save" }));
|
||||
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/cover-letter", { text: "Captured generated suggestion", source: "ai", aiAction: "generate", expectedVersion: 7 }));
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockedApi.post).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("capture, detail and download failures keep owned frozen history and recover only through their original explicit actions", async () => {
|
||||
|
||||
Reference in New Issue
Block a user